diff --git a/public_html/index.html b/public_html/index.html index 0e44042..b2415ea 100644 --- a/public_html/index.html +++ b/public_html/index.html @@ -226,7 +226,36 @@ body::after{ .tb-logo{ font-family:var(--font-display);font-size:1rem;font-weight:900;letter-spacing:4px; color:var(--cyan);text-shadow:0 0 10px var(--cyan);display:flex;align-items:center;gap:10px; + transition:filter 0.4s ease; + will-change:transform; } +.tb-logo.face-tracking{ + filter:drop-shadow(0 0 12px rgba(0,212,255,0.9)) drop-shadow(0 0 24px rgba(0,212,255,0.4)); +} +/* Face scan crosshair overlay */ +#faceScanOverlay{ + position:fixed;pointer-events:none;z-index:9; + width:60px;height:60px; + display:none; +} +#faceScanOverlay::before,#faceScanOverlay::after{ + content:'';position:absolute; + border-color:rgba(0,212,255,0.7);border-style:solid; +} +#faceScanOverlay::before{ + top:0;left:0;width:16px;height:16px; + border-width:2px 0 0 2px; +} +#faceScanOverlay::after{ + bottom:0;right:0;width:16px;height:16px; + border-width:0 2px 2px 0; +} +#faceScanOverlay .fso-tr{position:absolute;top:0;right:0;width:16px;height:16px;border:2px solid rgba(0,212,255,0.7);border-left:0;border-bottom:0} +#faceScanOverlay .fso-bl{position:absolute;bottom:0;left:0;width:16px;height:16px;border:2px solid rgba(0,212,255,0.7);border-right:0;border-top:0} +#faceScanOverlay .fso-dot{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:4px;height:4px;border-radius:50%;background:rgba(0,212,255,0.6);box-shadow:0 0 6px var(--cyan)} +#faceScanOverlay .fso-label{position:absolute;bottom:-18px;left:50%;transform:translateX(-50%);font-family:var(--font-mono);font-size:0.45rem;color:rgba(0,212,255,0.7);letter-spacing:1px;white-space:nowrap} +@keyframes fsoSpin{from{transform:rotate(0deg)}to{transform:rotate(360deg)}} +#faceScanOverlay .fso-ring{position:absolute;top:50%;left:50%;width:40px;height:40px;margin:-20px;border-radius:50%;border:1px solid rgba(0,212,255,0.3);border-top-color:rgba(0,212,255,0.7);animation:fsoSpin 1.2s linear infinite} .tb-logo-dot{width:8px;height:8px;border-radius:50%;background:var(--cyan);box-shadow:0 0 8px var(--cyan);animation:corePulse 1.5s infinite} .tb-center{ display:flex;gap:24px;align-items:center; @@ -735,6 +764,13 @@ body::after{
+
+
+
+
+
+
TRACKING
+
@@ -1186,6 +1222,83 @@ function setAlertState(hasAlerts) { ov.style.display = hasAlerts ? 'block' : 'none'; } +// ── FACE TRACKING — reactor follows face position ───────────────────── +let _faceTargetX = 0, _faceTargetY = 0; // normalized -0.5 to 0.5 +let _faceCurrX = 0, _faceCurrY = 0; +let _faceVisible = false; +let _faceTrackRaf = null; +const FACE_MAX_X = 48; // max px left/right travel +const FACE_MAX_Y = 10; // max px up/down travel +const FACE_LERP = 0.07; // smoothing (lower = slower/smoother) + +function _faceTrackLoop() { + _faceTrackRaf = requestAnimationFrame(_faceTrackLoop); + + // Lerp toward target (or back to 0 when no face) + const targetX = _faceVisible ? _faceTargetX : 0; + const targetY = _faceVisible ? _faceTargetY : 0; + _faceCurrX += (targetX - _faceCurrX) * FACE_LERP; + _faceCurrY += (targetY - _faceCurrY) * FACE_LERP; + + const tx = _faceCurrX * FACE_MAX_X; + const ty = _faceCurrY * FACE_MAX_Y; + + const logo = document.querySelector('.tb-logo'); + if (logo) logo.style.transform = `translateX(${tx.toFixed(2)}px) translateY(${ty.toFixed(2)}px)`; +} + +function updateFaceTarget(box, videoW, videoH) { + // Face center, normalized — flip X because front camera is mirrored + const cx = box.x + box.width / 2; + const cy = box.y + box.height / 2; + _faceTargetX = 0.5 - (cx / videoW); // flipped: move right when face is left + _faceTargetY = (cy / videoH) - 0.5; // positive = face low = logo drops slightly + _faceVisible = true; + + // Position the scan overlay over the detected face in the viewport. + // The video feed is 320×240 but hidden; map to viewport coords. + const scaleX = window.innerWidth / videoW; + const scaleY = window.innerHeight / videoH; + const faceVx = box.x * scaleX; + const faceVy = box.y * scaleY; + const faceVw = box.width * scaleX; + const faceVh = box.height * scaleY; + const ov = document.getElementById('faceScanOverlay'); + if (ov) { + ov.style.display = 'block'; + // Center overlay on face, flipped X to match mirror + const ovX = window.innerWidth - faceVx - faceVw / 2 - 30; + const ovY = faceVy + faceVh / 2 - 30; + ov.style.left = Math.max(0, Math.min(window.innerWidth - 60, ovX)) + 'px'; + ov.style.top = Math.max(0, Math.min(window.innerHeight - 80, ovY)) + 'px'; + } +} + +function clearFaceTarget() { + _faceVisible = false; + const ov = document.getElementById('faceScanOverlay'); + if (ov) ov.style.display = 'none'; +} + +function startFaceTracking() { + const logo = document.querySelector('.tb-logo'); + if (logo) logo.classList.add('face-tracking'); + if (!_faceTrackRaf) _faceTrackLoop(); +} + +function stopFaceTracking() { + clearFaceTarget(); + const logo = document.querySelector('.tb-logo'); + if (logo) { logo.classList.remove('face-tracking'); logo.style.transform = ''; } + // Let the loop coast to zero naturally rather than snapping — cancel after settling + setTimeout(() => { + if (!_faceVisible && _faceTrackRaf) { + cancelAnimationFrame(_faceTrackRaf); + _faceTrackRaf = null; + } + }, 1500); +} + // ── GLITCH EFFECT ───────────────────────────────────────────────────── (function initGlitch() { function triggerGlitch() { @@ -1422,7 +1535,8 @@ async function startCamera() { cameraActive = true; btn.classList.add('cam-active'); btn.textContent = '◉ SENSING'; - addMessage('system', 'Face detection active — JARVIS will auto-engage mic when you approach.'); + startFaceTracking(); + addMessage('system', 'Face detection active — reactor tracking engaged.'); faceLoopId = setInterval(async () => { if (!cameraActive || isSpeaking) return; @@ -1434,16 +1548,21 @@ async function startCamera() { const now = Date.now(); if (detection) { lastFaceSeen = now; - const {width, height} = detection.box; + const {x, y, width, height} = detection.box; const ratio = (width * height) / (320 * 240); - // Trigger if face fills >3% of frame and mic not already on and cooldown passed + + // Drive the reactor to follow the face + updateFaceTarget(detection.box, 320, 240); + + // Trigger voice if face fills >3% of frame if (ratio > 0.03 && !voiceMode && now > autoMicCooldown) { - autoMicCooldown = now + 9000; // 9s between auto-triggers + autoMicCooldown = now + 9000; document.getElementById('cameraBtn').classList.add('cam-sensing'); enterVoiceMode(); } } else { - // No face — exit voice mode if camera-triggered and face gone >3s + clearFaceTarget(); + // No face — exit voice mode if face gone >3s if (voiceMode && now - lastFaceSeen > 3000) { exitVoiceMode(); } @@ -1476,6 +1595,7 @@ function stopCamera() { btn.classList.remove('cam-active', 'cam-sensing'); btn.textContent = '◉ CAMERA'; } + stopFaceTracking(); } function toggleCamera() {