const label = document.getElementById("label"); const eyes = Array.from(document.querySelectorAll(".eye")); const mouthShape = document.querySelector(".mouth-shape"); let current = "neutral"; let wanderEnabled = true; let wanderTimer = null; let talkEnabled = false; let talkCfg = { rate_hz: 3.2, amount: 0.9, jitter: 0.25 }; let talkTimer = null; function clamp(v, lo, hi){ return Math.max(lo, Math.min(hi, v)); } function setEmotion(name) { const safe = String(name || "neutral") .toLowerCase() .replace(/[^a-z0-9_-]/g, ""); current = safe; document.body.className = `emotion-${safe}`; label.textContent = safe; // surprised -> O-mouth if (safe === "surprised") document.body.classList.add("has-omouth"); else document.body.classList.remove("has-omouth"); // frown for sad/angry if (safe === "sad" || safe === "angry") mouthShape.classList.add("frown"); else mouthShape.classList.remove("frown"); } function blinkOnce() { eyes.forEach(e => e.classList.add("blink")); setTimeout(() => eyes.forEach(e => e.classList.remove("blink")), 120); } function setLook(x, y) { // x/y in -1..1 -> px offsets const pxX = Math.round(clamp(x, -1, 1) * 12); const pxY = Math.round(clamp(y, -1, 1) * 10); document.documentElement.style.setProperty("--pupil-x", `${pxX}px`); document.documentElement.style.setProperty("--pupil-y", `${pxY}px`); } function setIntensity(v) { const val = clamp(Number(v ?? 0.7), 0, 1); document.documentElement.style.setProperty("--intensity", String(val)); } function setMouthOpen(v) { const val = clamp(Number(v ?? 0), 0, 1); document.documentElement.style.setProperty("--mouth-open", String(val)); } function speak(amount = 0.8, durationMs = 600) { if (talkEnabled) return; // Talk-Mode steuert Mund setMouthOpen(amount); setTimeout(() => setMouthOpen(0), Math.max(0, durationMs)); } function stopTalk() { talkEnabled = false; if (talkTimer) { clearTimeout(talkTimer); talkTimer = null; } setMouthOpen(0); } function startTalk(cfg) { talkEnabled = true; talkCfg = { rate_hz: clamp(Number(cfg?.rate_hz ?? 3.2), 0.5, 10), amount: clamp(Number(cfg?.amount ?? 0.9), 0, 1), jitter: clamp(Number(cfg?.jitter ?? 0.25), 0, 1), }; if (talkTimer) clearTimeout(talkTimer); // “sprech”-Animation: Mund öffnet/schließt schnell, mit bisschen Zufall const tick = () => { if (!talkEnabled) return; const base = talkCfg.amount; const j = talkCfg.jitter; // random-ish open amount between ~0.2..1.0, scaled const r = (0.35 + Math.random() * 0.65); const open = clamp(base * r * (1 - j + Math.random() * j), 0, 1); setMouthOpen(open); // timing from rate_hz (Hz -> ms) const interval = Math.max(60, Math.round(1000 / talkCfg.rate_hz)); // add a bit of jitter to cadence const next = interval + Math.round((Math.random() * 2 - 1) * interval * 0.25); talkTimer = setTimeout(tick, next); }; tick(); } /* Pupillen-Wandern nur wenn kein look gesetzt ist */ function startWander() { if (wanderTimer) clearTimeout(wanderTimer); const tick = () => { if (!wanderEnabled) return; const x = (Math.random() * 2 - 1) * 0.7; const y = (Math.random() * 2 - 1) * 0.6; setLook(x, y); const next = 600 + Math.random() * 900; wanderTimer = setTimeout(tick, next); }; tick(); } function applyState(s) { if (!s || typeof s !== "object") return; if (s.emotion) setEmotion(s.emotion); if (s.intensity !== undefined) setIntensity(s.intensity); // one-shot blink if (s.blink) blinkOnce(); // look: if null -> enable wander. if object -> fixed look if ("look" in s) { if (s.look === null) { wanderEnabled = true; startWander(); } else if (typeof s.look === "object") { wanderEnabled = false; setLook(s.look.x ?? 0, s.look.y ?? 0); } } // mouth command if (s.mouth && typeof s.mouth === "object") { const open = !!s.mouth.open; const amount = clamp(Number(s.mouth.amount ?? 0.8), 0, 1); const dur = Number(s.mouth.duration_ms ?? 600); if (open) speak(amount, dur); else setMouthOpen(0); } // talk mode if (s.talk && typeof s.talk === "object") { const enabled = !!s.talk.enabled; if (enabled) startTalk(s.talk); else stopTalk(); } } function connect() { const es = new EventSource("/events"); es.addEventListener("state", (e) => { try { applyState(JSON.parse(e.data)); } catch {} }); es.onmessage = (e) => { // fallback: treat as state json try { applyState(JSON.parse(e.data)); } catch {} }; es.onerror = () => { es.close(); setTimeout(connect, 1000); }; } connect(); startWander(); /* zufälliges Blinzeln unabhängig vom Push */ (function startBlinkLoop(){ const loop = () => { let base = 3500; if (current === "sleepy") base = 2200; if (current === "surprised") base = 4200; const next = base + Math.random() * 2200; setTimeout(() => { blinkOnce(); if (Math.random() < 0.12) setTimeout(blinkOnce, 220); loop(); }, next); }; loop(); })();