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{
+
@@ -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() {