additional new face stuff
This commit is contained in:
parent
afca03954c
commit
a7300eba9b
|
|
@ -1,10 +1,3 @@
|
||||||
// /var/www/html/face.js
|
|
||||||
// Minimal: black background, filled cyan sticker-style face elements only.
|
|
||||||
// Touch-only overlay (links) is handled in HTML/CSS.
|
|
||||||
// Updates via SSE /events (nginx -> 127.0.0.1:8001/events)
|
|
||||||
// Payload example:
|
|
||||||
// {"emotion":"angry","intensity":0.9,"look":{"x":0.9,"y":0.0},"speaking":true,"eyesMoving":true}
|
|
||||||
|
|
||||||
(() => {
|
(() => {
|
||||||
const OVERLAY_TIMEOUT_MS = 2500;
|
const OVERLAY_TIMEOUT_MS = 2500;
|
||||||
|
|
||||||
|
|
@ -26,7 +19,6 @@
|
||||||
eyesMoving: true,
|
eyesMoving: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
// DOM
|
|
||||||
const stage = document.getElementById("stage");
|
const stage = document.getElementById("stage");
|
||||||
const overlay = document.getElementById("overlay");
|
const overlay = document.getElementById("overlay");
|
||||||
|
|
||||||
|
|
@ -38,259 +30,257 @@
|
||||||
// Touch overlay show
|
// Touch overlay show
|
||||||
let overlayTimer = null;
|
let overlayTimer = null;
|
||||||
function showOverlay(){
|
function showOverlay(){
|
||||||
if (!overlay) return;
|
|
||||||
overlay.classList.add("show");
|
overlay.classList.add("show");
|
||||||
clearTimeout(overlayTimer);
|
clearTimeout(overlayTimer);
|
||||||
overlayTimer = setTimeout(() => overlay.classList.remove("show"), OVERLAY_TIMEOUT_MS);
|
overlayTimer = setTimeout(() => overlay.classList.remove("show"), OVERLAY_TIMEOUT_MS);
|
||||||
}
|
}
|
||||||
if (stage) stage.addEventListener("pointerdown", () => showOverlay(), { passive: true });
|
stage.addEventListener("pointerdown", () => showOverlay(), { passive:true });
|
||||||
|
|
||||||
// Tuned to match your reference image more closely
|
// STYLE: closer to sticker art (thin edge, almost no glow)
|
||||||
const BASE = {
|
function applyStyle(el) {
|
||||||
L: { x: 385, y: 265 },
|
const a = 0.90 + 0.10 * state.intensity;
|
||||||
R: { x: 615, y: 265 },
|
|
||||||
lookMaxPx: 18,
|
|
||||||
mouthCx: 500,
|
|
||||||
mouthY: 388,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Sticker colors: filled cyan with light edge
|
|
||||||
function setStickerStyle(el, edge = 5) {
|
|
||||||
const a = 0.86 + 0.14 * state.intensity;
|
|
||||||
el.setAttribute("fill", `rgba(120,235,255,${a})`);
|
el.setAttribute("fill", `rgba(120,235,255,${a})`);
|
||||||
el.setAttribute("stroke", `rgba(210,255,255,${0.55 + 0.35 * state.intensity})`);
|
el.setAttribute("stroke", `rgba(210,255,255,${0.45 + 0.35 * state.intensity})`);
|
||||||
el.setAttribute("stroke-width", String(edge));
|
el.setAttribute("stroke-width", "4.5");
|
||||||
el.setAttribute("stroke-linejoin", "round");
|
el.setAttribute("stroke-linejoin", "round");
|
||||||
el.setAttribute("stroke-linecap", "round");
|
el.setAttribute("stroke-linecap", "round");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----- EYES (filled) -----
|
// Helpers: Ellipse path (absolute, clean)
|
||||||
function eyeOval(cx, cy) {
|
function ellipsePath(cx, cy, rx, ry) {
|
||||||
const rx = 40,
|
return `M ${cx-rx},${cy}
|
||||||
ry = 74;
|
C ${cx-rx},${cy-ry} ${cx},${cy-ry} ${cx},${cy-ry}
|
||||||
return `M ${cx},${cy - ry}
|
C ${cx+rx},${cy-ry} ${cx+rx},${cy} ${cx+rx},${cy}
|
||||||
C ${cx + rx},${cy - ry} ${cx + rx},${cy + ry} ${cx},${cy + ry}
|
C ${cx+rx},${cy+ry} ${cx},${cy+ry} ${cx},${cy+ry}
|
||||||
C ${cx - rx},${cy + ry} ${cx - rx},${cy - ry} ${cx},${cy - ry} Z`;
|
C ${cx-rx},${cy+ry} ${cx-rx},${cy} ${cx-rx},${cy} Z`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function eyeSurprised(cx, cy) {
|
// ICON SHAPES (hand-tuned proportions similar to your reference)
|
||||||
const rx = 44,
|
// Coordinate system: viewBox 0..1000 x 0..600
|
||||||
ry = 82;
|
|
||||||
return `M ${cx},${cy - ry}
|
const ICONS = {
|
||||||
C ${cx + rx},${cy - ry} ${cx + rx},${cy + ry} ${cx},${cy + ry}
|
neutral: {
|
||||||
C ${cx - rx},${cy + ry} ${cx - rx},${cy - ry} ${cx},${cy - ry} Z`;
|
// friendly neutral eyes: a bit wider + slightly shorter (feels warm, not surprised)
|
||||||
|
eyeL: ellipsePath(395, 270, 50, 90),
|
||||||
|
eyeR: ellipsePath(605, 270, 50, 90),
|
||||||
|
|
||||||
|
// friendly micro-smile: more curve + a bit wider
|
||||||
|
mouth: `M 400,442
|
||||||
|
Q 500,505 600,442
|
||||||
|
Q 560,478 500,478
|
||||||
|
Q 440,478 400,442 Z`,
|
||||||
|
|
||||||
|
// speaking visemes (keep "smile family" so it stays friendly while talking)
|
||||||
|
visemes: [
|
||||||
|
`M 400,442
|
||||||
|
Q 500,505 600,442
|
||||||
|
Q 560,478 500,478
|
||||||
|
Q 440,478 400,442 Z`,
|
||||||
|
|
||||||
|
`M 390,432
|
||||||
|
Q 500,520 610,432
|
||||||
|
Q 565,492 500,492
|
||||||
|
Q 435,492 390,432 Z`,
|
||||||
|
|
||||||
|
`M 375,418
|
||||||
|
Q 500,540 625,418
|
||||||
|
Q 570,510 500,510
|
||||||
|
Q 430,510 375,418 Z`,
|
||||||
|
],
|
||||||
|
|
||||||
|
allowLook: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
happy: {
|
||||||
|
// filled caps (thick arcs) like sticker
|
||||||
|
eyeL: `M 338,248
|
||||||
|
Q 395,318 452,248
|
||||||
|
Q 430,340 395,340
|
||||||
|
Q 360,340 338,248 Z`,
|
||||||
|
eyeR: `M 548,248
|
||||||
|
Q 605,318 662,248
|
||||||
|
Q 640,340 605,340
|
||||||
|
Q 570,340 548,248 Z`,
|
||||||
|
mouth: `M 330,360
|
||||||
|
Q 500,540 670,360
|
||||||
|
Q 610,520 500,520
|
||||||
|
Q 390,520 330,360 Z`,
|
||||||
|
visemes: [
|
||||||
|
`M 330,360 Q 500,540 670,360 Q 610,520 500,520 Q 390,520 330,360 Z`,
|
||||||
|
`M 320,350 Q 500,560 680,350 Q 615,535 500,535 Q 385,535 320,350 Z`,
|
||||||
|
`M 305,338 Q 500,585 695,338 Q 620,555 500,555 Q 380,555 305,338 Z`,
|
||||||
|
],
|
||||||
|
allowLook: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
sad: {
|
||||||
|
// thin sleepy-ish eyes like reference bottom-right
|
||||||
|
eyeL: `M 330,270
|
||||||
|
Q 395,250 460,270
|
||||||
|
Q 460,294 395,294
|
||||||
|
Q 330,294 330,270 Z`,
|
||||||
|
eyeR: `M 540,270
|
||||||
|
Q 605,250 670,270
|
||||||
|
Q 670,294 605,294
|
||||||
|
Q 540,294 540,270 Z`,
|
||||||
|
mouth: `M 330,490
|
||||||
|
Q 500,350 670,490
|
||||||
|
Q 610,420 500,420
|
||||||
|
Q 390,420 330,490 Z`,
|
||||||
|
visemes: [
|
||||||
|
`M 330,490 Q 500,350 670,490 Q 610,420 500,420 Q 390,420 330,490 Z`,
|
||||||
|
`M 350,500 Q 500,360 650,500 Q 600,440 500,440 Q 400,440 350,500 Z`,
|
||||||
|
`M 365,510 Q 500,380 635,510 Q 590,460 500,460 Q 410,460 365,510 Z`,
|
||||||
|
],
|
||||||
|
allowLook: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
sleepy: {
|
||||||
|
// even flatter than sad
|
||||||
|
eyeL: `M 320,270
|
||||||
|
Q 395,258 470,270
|
||||||
|
Q 470,292 395,304
|
||||||
|
Q 320,292 320,270 Z`,
|
||||||
|
eyeR: `M 530,270
|
||||||
|
Q 605,258 680,270
|
||||||
|
Q 680,292 605,304
|
||||||
|
Q 530,292 530,270 Z`,
|
||||||
|
mouth: `M 335,495
|
||||||
|
Q 500,360 665,495
|
||||||
|
Q 610,435 500,435
|
||||||
|
Q 390,435 335,495 Z`,
|
||||||
|
visemes: [
|
||||||
|
`M 335,495 Q 500,360 665,495 Q 610,435 500,435 Q 390,435 335,495 Z`,
|
||||||
|
`M 355,505 Q 500,380 645,505 Q 595,455 500,455 Q 405,455 355,505 Z`,
|
||||||
|
`M 370,515 Q 500,400 630,515 Q 585,470 500,470 Q 415,470 370,515 Z`,
|
||||||
|
],
|
||||||
|
allowLook: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
angry: {
|
||||||
|
// wedges like top-left reference
|
||||||
|
eyeL: `M 300,245
|
||||||
|
L 430,285
|
||||||
|
L 495,230
|
||||||
|
L 360,195 Z`,
|
||||||
|
eyeR: `M 700,245
|
||||||
|
L 570,285
|
||||||
|
L 505,230
|
||||||
|
[O L 640,195 Z`,
|
||||||
|
mouth: `M 380,360
|
||||||
|
L 620,360
|
||||||
|
L 720,520
|
||||||
|
L 280,520 Z`,
|
||||||
|
visemes: [
|
||||||
|
`M 380,360 L 620,360 L 720,520 L 280,520 Z`,
|
||||||
|
`M 395,350 L 605,350 L 705,530 L 295,530 Z`,
|
||||||
|
`M 410,340 L 590,340 L 690,540 L 310,540 Z`,
|
||||||
|
],
|
||||||
|
allowLook: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
surprised: {
|
||||||
|
eyeL: ellipsePath(395, 270, 50, 100),
|
||||||
|
eyeR: ellipsePath(605, 270, 50, 100),
|
||||||
|
mouth: `M 450,382
|
||||||
|
Q 500,340 550,382
|
||||||
|
Q 585,450 550,518
|
||||||
|
Q 500,560 450,518
|
||||||
|
Q 415,450 450,382 Z`,
|
||||||
|
visemes: [
|
||||||
|
`M 450,382 Q 500,340 550,382 Q 585,450 550,518 Q 500,560 450,518 Q 415,450 450,382 Z`,
|
||||||
|
`M 440,370 Q 500,320 560,370 Q 600,450 560,530 Q 500,580 440,530 Q 400,450 440,370 Z`,
|
||||||
|
`M 430,360 Q 500,300 570,360 Q 615,450 570,540 Q 500,600 430,540 Q 385,450 430,360 Z`,
|
||||||
|
],
|
||||||
|
allowLook: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
excited: {
|
||||||
|
// excited: big surprised eyes, big grin
|
||||||
|
eyeL: ellipsePath(395, 270, 54, 108),
|
||||||
|
eyeR: ellipsePath(605, 270, 54, 108),
|
||||||
|
mouth: `M 315,350
|
||||||
|
Q 500,560 685,350
|
||||||
|
Q 620,545 500,545
|
||||||
|
Q 380,545 315,350 Z`,
|
||||||
|
visemes: [
|
||||||
|
`M 315,350 Q 500,560 685,350 Q 620,545 500,545 Q 380,545 315,350 Z`,
|
||||||
|
`M 305,340 Q 500,580 695,340 Q 625,560 500,560 Q 375,560 305,340 Z`,
|
||||||
|
`M 290,330 Q 500,600 710,330 Q 630,575 500,575 Q 370,575 290,330 Z`,
|
||||||
|
],
|
||||||
|
allowLook: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Look movement only for allowedLook emotions (otherwise it destroys icon-eyes)
|
||||||
|
function applyLook() {
|
||||||
|
const cfg = ICONS[state.emotion] ?? ICONS.neutral;
|
||||||
|
if (!cfg.allowLook) {
|
||||||
|
eyesGroup.setAttribute("transform", "");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const dx = clamp(state.look.x, -1, 1) * 16;
|
||||||
|
const dy = clamp(state.look.y, -1, 1) * 12;
|
||||||
|
eyesGroup.setAttribute("transform", `translate(${dx},${dy})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function eyeHappy(cx, cy) {
|
// Speaking: use viseme cycling instead of scaling (keeps shapes “sticker clean”)
|
||||||
// thick cap-like filled arc
|
|
||||||
const w = 100,
|
|
||||||
h = 74;
|
|
||||||
const x1 = cx - w / 2;
|
|
||||||
const x2 = cx + w / 2;
|
|
||||||
const yTop = cy - 40;
|
|
||||||
const yBot = yTop + 38;
|
|
||||||
return `M ${x1},${yBot}
|
|
||||||
Q ${cx},${yTop + h} ${x2},${yBot}
|
|
||||||
Q ${cx},${yTop + h * 0.62} ${x1},${yBot} Z`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function eyeSleep(cx, cy) {
|
|
||||||
// longer thin pill
|
|
||||||
const w = 128,
|
|
||||||
h = 20;
|
|
||||||
const x1 = cx - w / 2;
|
|
||||||
const x2 = cx + w / 2;
|
|
||||||
const y1 = cy - h / 2;
|
|
||||||
const y2 = cy + h / 2;
|
|
||||||
return `M ${x1},${y1}
|
|
||||||
Q ${cx},${y1 - 6} ${x2},${y1}
|
|
||||||
L ${x2},${y2}
|
|
||||||
Q ${cx},${y2 + 6} ${x1},${y2} Z`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function eyeAngryL(cx, cy) {
|
|
||||||
// wedge, slanting down toward center
|
|
||||||
return `M ${cx - 92},${cy - 58}
|
|
||||||
L ${cx + 20},${cy - 18}
|
|
||||||
L ${cx + 64},${cy - 84}
|
|
||||||
L ${cx - 60},${cy - 96} Z`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function eyeAngryR(cx, cy) {
|
|
||||||
return `M ${cx + 92},${cy - 58}
|
|
||||||
L ${cx - 20},${cy - 18}
|
|
||||||
L ${cx - 64},${cy - 84}
|
|
||||||
L ${cx + 60},${cy - 96} Z`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----- MOUTHS (filled) -----
|
|
||||||
function mouthDot(cx, y) {
|
|
||||||
const w = 28,
|
|
||||||
h = 12;
|
|
||||||
return `M ${cx - w / 2},${y - h / 2}
|
|
||||||
L ${cx + w / 2},${y - h / 2}
|
|
||||||
L ${cx + w / 2},${y + h / 2}
|
|
||||||
L ${cx - w / 2},${y + h / 2} Z`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function mouthGrin(cx, y) {
|
|
||||||
// wide grin like sticker
|
|
||||||
const w = 400,
|
|
||||||
h = 190;
|
|
||||||
const x1 = cx - w / 2;
|
|
||||||
const x2 = cx + w / 2;
|
|
||||||
const yTop = y - 78;
|
|
||||||
return `M ${x1},${yTop}
|
|
||||||
Q ${cx},${yTop + h} ${x2},${yTop}
|
|
||||||
Q ${cx},${yTop + h * 0.58} ${x1},${yTop} Z`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function mouthSmile(cx, y) {
|
|
||||||
// narrower fallback
|
|
||||||
const w = 340,
|
|
||||||
h = 170;
|
|
||||||
const x1 = cx - w / 2;
|
|
||||||
const x2 = cx + w / 2;
|
|
||||||
const yTop = y - 70;
|
|
||||||
return `M ${x1},${yTop}
|
|
||||||
Q ${cx},${yTop + h} ${x2},${yTop}
|
|
||||||
Q ${cx},${yTop + h * 0.58} ${x1},${yTop} Z`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function mouthFrown(cx, y) {
|
|
||||||
const w = 360,
|
|
||||||
h = 170;
|
|
||||||
const x1 = cx - w / 2;
|
|
||||||
const x2 = cx + w / 2;
|
|
||||||
const yBot = y + 78;
|
|
||||||
return `M ${x1},${yBot}
|
|
||||||
Q ${cx},${yBot - h} ${x2},${yBot}
|
|
||||||
Q ${cx},${yBot - h * 0.58} ${x1},${yBot} Z`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function mouthShout(cx, y) {
|
|
||||||
// trapezoid open mouth (less tall)
|
|
||||||
const wTop = 160,
|
|
||||||
wBot = 320,
|
|
||||||
h = 120;
|
|
||||||
const x1 = cx - wTop / 2;
|
|
||||||
const x2 = cx + wTop / 2;
|
|
||||||
const x3 = cx + wBot / 2;
|
|
||||||
const x4 = cx - wBot / 2;
|
|
||||||
const y1 = y - 70;
|
|
||||||
const y2 = y1 + h;
|
|
||||||
return `M ${x1},${y1} L ${x2},${y1} L ${x3},${y2} L ${x4},${y2} Z`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Speaking animation: scale mouth vertically (works for all mouth shapes)
|
|
||||||
let speakTimer = null;
|
let speakTimer = null;
|
||||||
|
let visemeIndex = 0;
|
||||||
|
|
||||||
function startSpeaking() {
|
function startSpeaking() {
|
||||||
stopSpeaking();
|
stopSpeaking();
|
||||||
let open = false;
|
visemeIndex = 0;
|
||||||
const base = state.emotion === "excited" ? 85 : state.emotion === "sleepy" ? 170 : 115;
|
|
||||||
|
|
||||||
|
const base = state.emotion === "excited" ? 90 : (state.emotion === "sleepy" ? 180 : 120);
|
||||||
speakTimer = setInterval(() => {
|
speakTimer = setInterval(() => {
|
||||||
if (!state.speaking) return;
|
if (!state.speaking) return;
|
||||||
open = !open;
|
visemeIndex = (visemeIndex + 1) % 3;
|
||||||
const amp = 0.10 + 0.18 * state.intensity;
|
render(); // will pick viseme
|
||||||
const sy = 1 + amp * (open ? 1 : -0.35);
|
|
||||||
const ty = open ? 5 : 0;
|
|
||||||
mouth.setAttribute("transform", `translate(0,${ty}) scale(1,${sy})`);
|
|
||||||
}, base);
|
}, base);
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopSpeaking() {
|
function stopSpeaking() {
|
||||||
if (speakTimer) clearInterval(speakTimer);
|
if (speakTimer) clearInterval(speakTimer);
|
||||||
speakTimer = null;
|
speakTimer = null;
|
||||||
|
visemeIndex = 0;
|
||||||
mouth.setAttribute("transform", "");
|
mouth.setAttribute("transform", "");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Eye drift when no locked look and eyesMoving enabled
|
// Optional drift (only if allowLook and not locked)
|
||||||
let driftTimer = null;
|
let driftTimer = null;
|
||||||
function scheduleDrift() {
|
function scheduleDrift() {
|
||||||
clearTimeout(driftTimer);
|
clearTimeout(driftTimer);
|
||||||
if (state.lookLock || !state.eyesMoving) return;
|
const cfg = ICONS[state.emotion] ?? ICONS.neutral;
|
||||||
|
|
||||||
// do not drift for icon-locked emotions
|
if (state.lookLock || !state.eyesMoving || !cfg.allowLook) return;
|
||||||
const iconLocked = state.emotion === "angry" || state.emotion === "happy" || state.emotion === "sleepy";
|
|
||||||
if (iconLocked) return;
|
|
||||||
|
|
||||||
state.look = {
|
state.look = {
|
||||||
x: (Math.random() * 2 - 1) * 0.5,
|
x: (Math.random() * 2 - 1) * 0.5,
|
||||||
y: (Math.random() * 2 - 1) * 0.35,
|
y: (Math.random() * 2 - 1) * 0.35,
|
||||||
};
|
};
|
||||||
|
|
||||||
render();
|
render();
|
||||||
driftTimer = setTimeout(scheduleDrift, 900 + Math.random() * 900);
|
driftTimer = setTimeout(scheduleDrift, 900 + Math.random() * 900);
|
||||||
}
|
}
|
||||||
|
|
||||||
function render() {
|
function render() {
|
||||||
if (!eyeL || !eyeR || !mouth || !eyesGroup) return;
|
const cfg = ICONS[state.emotion] ?? ICONS.neutral;
|
||||||
|
|
||||||
// subtle look shift (eyesGroup translate)
|
applyStyle(eyeL);
|
||||||
const lookX = clamp(state.look.x, -1, 1);
|
applyStyle(eyeR);
|
||||||
const lookY = clamp(state.look.y, -1, 1);
|
applyStyle(mouth);
|
||||||
|
|
||||||
const iconLocked = state.emotion === "angry" || state.emotion === "happy" || state.emotion === "sleepy";
|
eyeL.setAttribute("d", cfg.eyeL);
|
||||||
const scale = iconLocked ? 0.18 : 1.0;
|
eyeR.setAttribute("d", cfg.eyeR);
|
||||||
|
|
||||||
eyesGroup.setAttribute(
|
const mouthPath = (state.speaking && cfg.visemes) ? cfg.visemes[visemeIndex] : cfg.mouth;
|
||||||
"transform",
|
mouth.setAttribute("d", mouthPath);
|
||||||
`translate(${lookX * BASE.lookMaxPx * scale}, ${lookY * BASE.lookMaxPx * scale})`
|
|
||||||
);
|
|
||||||
|
|
||||||
// style
|
applyLook();
|
||||||
setStickerStyle(eyeL, 5);
|
|
||||||
setStickerStyle(eyeR, 5);
|
|
||||||
setStickerStyle(mouth, 5);
|
|
||||||
|
|
||||||
// mapping
|
|
||||||
switch (state.emotion) {
|
|
||||||
case "angry":
|
|
||||||
eyeL.setAttribute("d", eyeAngryL(BASE.L.x, BASE.L.y));
|
|
||||||
eyeR.setAttribute("d", eyeAngryR(BASE.R.x, BASE.R.y));
|
|
||||||
mouth.setAttribute("d", mouthShout(BASE.mouthCx, BASE.mouthY + 50));
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "happy":
|
|
||||||
eyeL.setAttribute("d", eyeHappy(BASE.L.x, BASE.L.y));
|
|
||||||
eyeR.setAttribute("d", eyeHappy(BASE.R.x, BASE.R.y));
|
|
||||||
mouth.setAttribute("d", mouthGrin(BASE.mouthCx, BASE.mouthY + 14));
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "excited":
|
|
||||||
eyeL.setAttribute("d", eyeSurprised(BASE.L.x, BASE.L.y + 2));
|
|
||||||
eyeR.setAttribute("d", eyeSurprised(BASE.R.x, BASE.R.y + 2));
|
|
||||||
mouth.setAttribute("d", mouthGrin(BASE.mouthCx, BASE.mouthY + 4));
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "surprised":
|
|
||||||
eyeL.setAttribute("d", eyeSurprised(BASE.L.x, BASE.L.y + 2));
|
|
||||||
eyeR.setAttribute("d", eyeSurprised(BASE.R.x, BASE.R.y + 2));
|
|
||||||
mouth.setAttribute("d", mouthShout(BASE.mouthCx, BASE.mouthY + 34));
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "sleepy":
|
|
||||||
eyeL.setAttribute("d", eyeSleep(BASE.L.x, BASE.L.y + 28));
|
|
||||||
eyeR.setAttribute("d", eyeSleep(BASE.R.x, BASE.R.y + 28));
|
|
||||||
mouth.setAttribute("d", mouthFrown(BASE.mouthCx, BASE.mouthY + 18));
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "sad":
|
|
||||||
eyeL.setAttribute("d", eyeSleep(BASE.L.x, BASE.L.y + 32));
|
|
||||||
eyeR.setAttribute("d", eyeSleep(BASE.R.x, BASE.R.y + 32));
|
|
||||||
mouth.setAttribute("d", mouthFrown(BASE.mouthCx, BASE.mouthY + 24));
|
|
||||||
break;
|
|
||||||
|
|
||||||
default: // neutral
|
|
||||||
eyeL.setAttribute("d", eyeOval(BASE.L.x, BASE.L.y));
|
|
||||||
eyeR.setAttribute("d", eyeOval(BASE.R.x, BASE.R.y));
|
|
||||||
mouth.setAttribute("d", mouthDot(BASE.mouthCx, BASE.mouthY + 42));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Public API for incoming state
|
// Public API
|
||||||
window.applyFaceState = (payload) => {
|
window.applyFaceState = (payload) => {
|
||||||
if (!payload || typeof payload !== "object") return;
|
if (!payload || typeof payload !== "object") return;
|
||||||
|
|
||||||
|
|
@ -314,41 +304,38 @@
|
||||||
if (typeof payload.eyesMoving === "boolean") state.eyesMoving = payload.eyesMoving;
|
if (typeof payload.eyesMoving === "boolean") state.eyesMoving = payload.eyesMoving;
|
||||||
|
|
||||||
render();
|
render();
|
||||||
|
|
||||||
if (state.speaking) startSpeaking();
|
if (state.speaking) startSpeaking();
|
||||||
else stopSpeaking();
|
else stopSpeaking();
|
||||||
|
|
||||||
if (!state.lookLock) scheduleDrift();
|
if (!state.lookLock) scheduleDrift();
|
||||||
};
|
};
|
||||||
|
|
||||||
// SSE /events (supports event: state and plain message)
|
// SSE /events
|
||||||
(function connectSSE(){
|
(function connectSSE(){
|
||||||
let es;
|
let es;
|
||||||
|
|
||||||
function open(){
|
function open(){
|
||||||
es = new EventSource("/events");
|
es = new EventSource("/events");
|
||||||
|
|
||||||
const handle = (ev) => {
|
const handle = (ev) => {
|
||||||
try {
|
try { window.applyFaceState(JSON.parse(ev.data)); } catch (_) {}
|
||||||
window.applyFaceState(JSON.parse(ev.data));
|
|
||||||
} catch (_) {}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
es.addEventListener("state", handle);
|
es.addEventListener("state", handle);
|
||||||
es.onmessage = handle;
|
es.onmessage = handle;
|
||||||
|
|
||||||
es.onerror = () => {
|
es.onerror = () => {
|
||||||
try {
|
try { es.close(); } catch (_) {}
|
||||||
es.close();
|
|
||||||
} catch (_) {}
|
|
||||||
setTimeout(open, 1200);
|
setTimeout(open, 1200);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
open();
|
open();
|
||||||
})();
|
})();
|
||||||
|
|
||||||
// Init
|
// init
|
||||||
render();
|
render();
|
||||||
scheduleDrift();
|
scheduleDrift();
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue