additional new face stuff
This commit is contained in:
parent
c296d4e950
commit
34756be2de
|
|
@ -3,271 +3,112 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover" />
|
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover" />
|
||||||
<title>Face</title>
|
<title>Helva Face Display</title>
|
||||||
<style>
|
<link rel="stylesheet" href="/static/style.css" />
|
||||||
:root {
|
|
||||||
--bg: #000;
|
|
||||||
--fg: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
html, body {
|
|
||||||
height: 100%;
|
|
||||||
margin: 0;
|
|
||||||
background: var(--bg);
|
|
||||||
overflow: hidden;
|
|
||||||
touch-action: manipulation;
|
|
||||||
user-select: none;
|
|
||||||
-webkit-tap-highlight-color: transparent;
|
|
||||||
font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Fullscreen stage */
|
|
||||||
#stage {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
background: var(--bg);
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Face canvas-ish container */
|
|
||||||
#face {
|
|
||||||
width: min(92vw, 92vh);
|
|
||||||
height: min(92vw, 92vh);
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Controls: hidden until touch/move */
|
|
||||||
#overlay {
|
|
||||||
position: fixed;
|
|
||||||
left: 12px;
|
|
||||||
top: 12px;
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
opacity: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
transition: opacity 180ms ease;
|
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
#overlay.visible {
|
|
||||||
opacity: 1;
|
|
||||||
pointer-events: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
color: var(--fg);
|
|
||||||
text-decoration: none;
|
|
||||||
font-size: 14px;
|
|
||||||
padding: 10px 12px;
|
|
||||||
border: 1px solid rgba(255,255,255,0.25);
|
|
||||||
border-radius: 12px;
|
|
||||||
background: rgba(0,0,0,0.35);
|
|
||||||
backdrop-filter: blur(6px);
|
|
||||||
-webkit-backdrop-filter: blur(6px);
|
|
||||||
line-height: 1;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
.btn:active { transform: translateY(1px); }
|
|
||||||
|
|
||||||
/* --- Simple face elements (SVG) --- */
|
|
||||||
svg { width: 100%; height: 100%; display: block; }
|
|
||||||
|
|
||||||
/* Keep strokes crisp */
|
|
||||||
.stroke {
|
|
||||||
fill: none;
|
|
||||||
stroke: #fff;
|
|
||||||
stroke-linecap: round;
|
|
||||||
stroke-linejoin: round;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="stage">
|
<div class="stage" id="stage">
|
||||||
<div id="overlay" aria-hidden="true">
|
<div class="hint">Tippe, um Drive & Control zu sehen</div>
|
||||||
<a class="btn" id="btnControl" href="/control">neutral</a>
|
|
||||||
<a class="btn" id="btnDrive" href="/drive">/drive</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="face" aria-label="Robot Face">
|
<div class="wrap">
|
||||||
<!-- Minimal clean face in SVG (eyes + mouth); driven by state below -->
|
<!-- ONLY the screen / display -->
|
||||||
<svg viewBox="0 0 100 100" role="img" aria-hidden="true">
|
<svg id="faceSvg" class="screen" viewBox="0 0 800 460" role="img" aria-label="Robot face display">
|
||||||
<!-- Eyes -->
|
<defs>
|
||||||
<g id="eyes" class="stroke" stroke-width="6">
|
<!-- screen glass gradient -->
|
||||||
<!-- left eye -->
|
<linearGradient id="glass" x1="0" x2="1" y1="0" y2="1">
|
||||||
<path id="eyeL" d="M22 40 C 30 30, 40 30, 48 40" />
|
<stop offset="0" stop-color="#0b0f1a" />
|
||||||
<!-- right eye -->
|
<stop offset="1" stop-color="#03050a" />
|
||||||
<path id="eyeR" d="M52 40 C 60 30, 70 30, 78 40" />
|
</linearGradient>
|
||||||
|
|
||||||
|
<!-- glossy highlight -->
|
||||||
|
<linearGradient id="gloss" x1="0" x2="1">
|
||||||
|
<stop offset="0" stop-color="rgba(255,255,255,0.18)" />
|
||||||
|
<stop offset="1" stop-color="rgba(255,255,255,0.00)" />
|
||||||
|
</linearGradient>
|
||||||
|
|
||||||
|
<!-- neon glow -->
|
||||||
|
<filter id="neonGlow" x="-50%" y="-50%" width="200%" height="200%">
|
||||||
|
<feGaussianBlur stdDeviation="6" result="b"/>
|
||||||
|
<feMerge>
|
||||||
|
<feMergeNode in="b"/>
|
||||||
|
<feMergeNode in="SourceGraphic"/>
|
||||||
|
</feMerge>
|
||||||
|
</filter>
|
||||||
|
|
||||||
|
<!-- stronger glow (intensity) -->
|
||||||
|
<filter id="neonGlowStrong" x="-80%" y="-80%" width="260%" height="260%">
|
||||||
|
<feGaussianBlur stdDeviation="10" result="b2"/>
|
||||||
|
<feMerge>
|
||||||
|
<feMergeNode in="b2"/>
|
||||||
|
<feMergeNode in="SourceGraphic"/>
|
||||||
|
</feMerge>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- outer screen frame -->
|
||||||
|
<rect x="35" y="35" width="730" height="390" rx="70"
|
||||||
|
fill="#05070c" stroke="rgba(255,255,255,0.10)" stroke-width="6"/>
|
||||||
|
|
||||||
|
<!-- inner glass -->
|
||||||
|
<rect x="75" y="75" width="650" height="310" rx="52"
|
||||||
|
fill="url(#glass)" stroke="rgba(255,255,255,0.06)" stroke-width="4"/>
|
||||||
|
|
||||||
|
<!-- big gloss sweep (like the sticker example) -->
|
||||||
|
<path d="M115,110 C220,85 420,85 620,125
|
||||||
|
C590,160 430,180 250,165
|
||||||
|
C170,158 125,142 115,110Z"
|
||||||
|
fill="url(#gloss)" opacity="0.55"/>
|
||||||
|
|
||||||
|
<!-- top light bar -->
|
||||||
|
<g id="topbar">
|
||||||
|
<rect x="260" y="92" width="280" height="34" rx="17"
|
||||||
|
fill="rgba(120,255,235,0.20)"/>
|
||||||
|
<rect id="topbarLit" x="275" y="98" width="250" height="22" rx="11"
|
||||||
|
fill="rgba(120,255,235,0.95)" filter="url(#neonGlow)"/>
|
||||||
</g>
|
</g>
|
||||||
|
|
||||||
<!-- Pupils (small dots) -->
|
<!-- FACE LAYER (cyan line art) -->
|
||||||
<g id="pupils" fill="#fff">
|
<g id="face" filter="url(#neonGlow)">
|
||||||
<circle id="pupilL" cx="35" cy="40" r="2.2" />
|
<!-- eyes container (we move this for look.x/y) -->
|
||||||
<circle id="pupilR" cx="65" cy="40" r="2.2" />
|
<g id="eyesGroup">
|
||||||
|
<!-- Left eye -->
|
||||||
|
<path id="eyeL" d="" />
|
||||||
|
<!-- Right eye -->
|
||||||
|
<path id="eyeR" d="" />
|
||||||
</g>
|
</g>
|
||||||
|
|
||||||
<!-- Mouth -->
|
<!-- mouth -->
|
||||||
<g class="stroke" stroke-width="6">
|
<path id="mouth" d="" />
|
||||||
<path id="mouth" d="M30 68 C 40 76, 60 76, 70 68" />
|
</g>
|
||||||
|
|
||||||
|
<!-- subtle sparkle dots (optional, like the ref image) -->
|
||||||
|
<g id="sparkles" opacity="0.45" filter="url(#neonGlow)">
|
||||||
|
<circle cx="150" cy="210" r="3" fill="rgba(120,255,235,0.9)"/>
|
||||||
|
<circle cx="165" cy="240" r="2" fill="rgba(120,255,235,0.8)"/>
|
||||||
|
<circle cx="650" cy="215" r="3" fill="rgba(120,255,235,0.9)"/>
|
||||||
|
<circle cx="635" cy="245" r="2" fill="rgba(120,255,235,0.8)"/>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
|
<!-- Touch-only overlay -->
|
||||||
|
<div class="overlay" id="overlay">
|
||||||
|
<div class="left">
|
||||||
|
<a class="btn" href="/drive">🚗 Drive</a>
|
||||||
|
<a class="btn" href="/control" id="controlLink">🎛️ Control: <span id="emotionLabel">neutral</span></a>
|
||||||
|
<span class="pill">
|
||||||
|
<span class="dot on" id="dotConn"></span>
|
||||||
|
<span id="pillText">ready</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="right">
|
||||||
|
<button class="btn" id="btnSpeak" type="button">🗣️ Sprechen: <strong id="speakState">aus</strong></button>
|
||||||
|
<button class="btn" id="btnEyes" type="button">👀 Augen: <strong id="eyesState">an</strong></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script src="/static/face.js"></script>
|
||||||
// --- State (matches your face-server structure) ---
|
|
||||||
const state = {
|
|
||||||
emotion: "neutral",
|
|
||||||
intensity: 0.7, // 0..1
|
|
||||||
look: null, // {"x": -1..1, "y": -1..1} or null
|
|
||||||
mouth: { open: false, amount: 0.0, duration_ms: 0 },
|
|
||||||
talk: { enabled: false, rate_hz: 3.2, amount: 0.9, jitter: 0.25 },
|
|
||||||
};
|
|
||||||
|
|
||||||
// --- UI refs ---
|
|
||||||
const overlay = document.getElementById("overlay");
|
|
||||||
const btnControl = document.getElementById("btnControl");
|
|
||||||
|
|
||||||
const pupilL = document.getElementById("pupilL");
|
|
||||||
const pupilR = document.getElementById("pupilR");
|
|
||||||
const mouth = document.getElementById("mouth");
|
|
||||||
const eyeL = document.getElementById("eyeL");
|
|
||||||
const eyeR = document.getElementById("eyeR");
|
|
||||||
|
|
||||||
// --- Overlay show-on-touch/move ---
|
|
||||||
let overlayTimer = null;
|
|
||||||
function showOverlayBriefly() {
|
|
||||||
overlay.classList.add("visible");
|
|
||||||
overlay.setAttribute("aria-hidden", "false");
|
|
||||||
if (overlayTimer) clearTimeout(overlayTimer);
|
|
||||||
overlayTimer = setTimeout(() => {
|
|
||||||
overlay.classList.remove("visible");
|
|
||||||
overlay.setAttribute("aria-hidden", "true");
|
|
||||||
}, 2500);
|
|
||||||
}
|
|
||||||
window.addEventListener("touchstart", showOverlayBriefly, { passive: true });
|
|
||||||
window.addEventListener("mousemove", showOverlayBriefly, { passive: true });
|
|
||||||
window.addEventListener("keydown", showOverlayBriefly);
|
|
||||||
|
|
||||||
// --- Face rendering helpers ---
|
|
||||||
function clamp(v, a, b) { return Math.max(a, Math.min(b, v)); }
|
|
||||||
|
|
||||||
function applyLook(look) {
|
|
||||||
// Pupils range in SVG coords
|
|
||||||
const maxX = 3.2, maxY = 2.2;
|
|
||||||
const x = look ? clamp(look.x, -1, 1) * maxX : 0;
|
|
||||||
const y = look ? clamp(look.y, -1, 1) * maxY : 0;
|
|
||||||
|
|
||||||
pupilL.setAttribute("cx", (35 + x).toFixed(2));
|
|
||||||
pupilL.setAttribute("cy", (40 + y).toFixed(2));
|
|
||||||
pupilR.setAttribute("cx", (65 + x).toFixed(2));
|
|
||||||
pupilR.setAttribute("cy", (40 + y).toFixed(2));
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyMouth(emotion, mouthState) {
|
|
||||||
// mouthState.amount: 0..1, open toggles
|
|
||||||
const amt = clamp(mouthState?.amount ?? 0, 0, 1);
|
|
||||||
const open = !!mouthState?.open;
|
|
||||||
|
|
||||||
// Base mouth by emotion (simple, clean)
|
|
||||||
// neutral: slight smile, happy: bigger smile, sad: frown, angry: flat, surprised: open O
|
|
||||||
if (emotion === "sad") {
|
|
||||||
mouth.setAttribute("d", "M30 72 C 42 62, 58 62, 70 72");
|
|
||||||
} else if (emotion === "angry") {
|
|
||||||
mouth.setAttribute("d", "M32 70 C 44 70, 56 70, 68 70");
|
|
||||||
} else if (emotion === "happy") {
|
|
||||||
mouth.setAttribute("d", "M28 66 C 40 80, 60 80, 72 66");
|
|
||||||
} else if (emotion === "surprised") {
|
|
||||||
// fake an "O" using a short curve; (keeps it minimal)
|
|
||||||
const r = 6 + amt * 6;
|
|
||||||
mouth.setAttribute("d", `M50 ${70-r} C ${50+r} ${70-r}, ${50+r} ${70+r}, 50 ${70+r} C ${50-r} ${70+r}, ${50-r} ${70-r}, 50 ${70-r}`);
|
|
||||||
} else {
|
|
||||||
// neutral
|
|
||||||
mouth.setAttribute("d", "M30 68 C 40 76, 60 76, 70 68");
|
|
||||||
}
|
|
||||||
|
|
||||||
// If mouth.open => exaggerate a bit (except surprised which is already open-ish)
|
|
||||||
if (open && emotion !== "surprised") {
|
|
||||||
// slightly "deeper" smile/frown/line
|
|
||||||
// We just scale stroke width a touch and nudge curve visually by intensity
|
|
||||||
// (simple, avoids layout thrash)
|
|
||||||
const sw = 6 + amt * 2.5;
|
|
||||||
mouth.parentElement.setAttribute("stroke-width", sw.toFixed(2));
|
|
||||||
} else {
|
|
||||||
mouth.parentElement.setAttribute("stroke-width", "6");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyEyes(emotion) {
|
|
||||||
// optional: angry eyebrows effect via eye curve shape
|
|
||||||
if (emotion === "angry") {
|
|
||||||
eyeL.setAttribute("d", "M22 42 C 32 30, 42 34, 48 42");
|
|
||||||
eyeR.setAttribute("d", "M52 42 C 58 34, 68 30, 78 42");
|
|
||||||
} else if (emotion === "sad") {
|
|
||||||
eyeL.setAttribute("d", "M22 40 C 30 34, 40 30, 48 40");
|
|
||||||
eyeR.setAttribute("d", "M52 40 C 60 30, 70 34, 78 40");
|
|
||||||
} else {
|
|
||||||
// default
|
|
||||||
eyeL.setAttribute("d", "M22 40 C 30 30, 40 30, 48 40");
|
|
||||||
eyeR.setAttribute("d", "M52 40 C 60 30, 70 30, 78 40");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function render() {
|
|
||||||
btnControl.textContent = state.emotion; // /control link text = aktuelle Emotion
|
|
||||||
applyLook(state.look);
|
|
||||||
applyEyes(state.emotion);
|
|
||||||
applyMouth(state.emotion, state.mouth);
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- WebSocket hookup (adjust WS_URL to your setup) ---
|
|
||||||
// Typical options:
|
|
||||||
// 1) Direct: ws://<pi-ip>:8001/ws
|
|
||||||
// 2) Via nginx: wss://<host>/face-ws
|
|
||||||
const WS_URL = (() => {
|
|
||||||
// Default: same host, path /ws
|
|
||||||
const proto = (location.protocol === "https:") ? "wss:" : "ws:";
|
|
||||||
return `${proto}//${location.host}/ws`;
|
|
||||||
})();
|
|
||||||
|
|
||||||
let ws = null;
|
|
||||||
let wsBackoffMs = 300;
|
|
||||||
|
|
||||||
function connectWs() {
|
|
||||||
try { ws?.close(); } catch {}
|
|
||||||
ws = new WebSocket(WS_URL);
|
|
||||||
|
|
||||||
ws.onopen = () => { wsBackoffMs = 300; };
|
|
||||||
ws.onmessage = (ev) => {
|
|
||||||
try {
|
|
||||||
const msg = JSON.parse(ev.data);
|
|
||||||
|
|
||||||
// accept either:
|
|
||||||
// - {type:"state", state:{...}}
|
|
||||||
// - {state:{...}}
|
|
||||||
// - direct state object
|
|
||||||
const incoming = msg?.state ?? (msg?.type === "state" ? msg.state : msg);
|
|
||||||
|
|
||||||
if (incoming && typeof incoming === "object") {
|
|
||||||
Object.assign(state, incoming);
|
|
||||||
render();
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// ignore malformed messages
|
|
||||||
}
|
|
||||||
};
|
|
||||||
ws.onclose = () => {
|
|
||||||
setTimeout(connectWs, wsBackoffMs);
|
|
||||||
wsBackoffMs = Math.min(wsBackoffMs * 1.7, 8000);
|
|
||||||
};
|
|
||||||
ws.onerror = () => {
|
|
||||||
try { ws.close(); } catch {}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
render();
|
|
||||||
connectWs();
|
|
||||||
</script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue