add face
This commit is contained in:
Executable
+81
@@ -0,0 +1,81 @@
|
||||
const label = document.getElementById("label");
|
||||
const eyes = Array.from(document.querySelectorAll(".eye"));
|
||||
const mouthShape = document.querySelector(".mouth-shape");
|
||||
|
||||
let current = "neutral";
|
||||
|
||||
function setEmotion(name) {
|
||||
const safe = String(name || "neutral")
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9_-]/g, "");
|
||||
|
||||
current = safe;
|
||||
document.body.className = `emotion-${safe}`;
|
||||
label.textContent = safe;
|
||||
|
||||
// Sonderfall: surprised -> O-Mund aktivieren
|
||||
if (safe === "surprised") document.body.classList.add("has-omouth");
|
||||
else document.body.classList.remove("has-omouth");
|
||||
|
||||
// sad -> frown pseudo aktivieren (eigene Klasse)
|
||||
if (safe === "sad" || safe === "angry") mouthShape.classList.add("frown");
|
||||
else mouthShape.classList.remove("frown");
|
||||
}
|
||||
|
||||
/** Pupillen bewegen sich sanft, damit es "lebendig" wirkt */
|
||||
function startPupilWander() {
|
||||
const tick = () => {
|
||||
// kleine zufällige Offsets in Pixel (auf großen Displays reicht das)
|
||||
const x = Math.round((Math.random() * 2 - 1) * 10);
|
||||
const y = Math.round((Math.random() * 2 - 1) * 8);
|
||||
|
||||
document.documentElement.style.setProperty("--pupil-x", `${x}px`);
|
||||
document.documentElement.style.setProperty("--pupil-y", `${y}px`);
|
||||
|
||||
// alle 600-1400ms neu
|
||||
const next = 600 + Math.random() * 800;
|
||||
setTimeout(tick, next);
|
||||
};
|
||||
tick();
|
||||
}
|
||||
|
||||
/** Blinzeln: in zufälligen Abständen, manchmal doppelt */
|
||||
function blinkOnce() {
|
||||
eyes.forEach(e => e.classList.add("blink"));
|
||||
setTimeout(() => eyes.forEach(e => e.classList.remove("blink")), 120);
|
||||
}
|
||||
|
||||
function startBlinking() {
|
||||
const loop = () => {
|
||||
// sleepy blinzelt öfter/langsamer, angry etwas "härter"
|
||||
let base = 3500;
|
||||
if (current === "sleepy") base = 2200;
|
||||
if (current === "surprised") base = 4200;
|
||||
|
||||
const next = base + Math.random() * 2200;
|
||||
setTimeout(() => {
|
||||
blinkOnce();
|
||||
|
||||
// 15% chance auf Doppelt-Blink
|
||||
if (Math.random() < 0.15) setTimeout(blinkOnce, 220);
|
||||
|
||||
loop();
|
||||
}, next);
|
||||
};
|
||||
loop();
|
||||
}
|
||||
|
||||
function connect() {
|
||||
const es = new EventSource("/events");
|
||||
es.addEventListener("emotion", (e) => setEmotion(e.data));
|
||||
es.onmessage = (e) => setEmotion(e.data); // fallback
|
||||
es.onerror = () => {
|
||||
es.close();
|
||||
setTimeout(connect, 1000);
|
||||
};
|
||||
}
|
||||
|
||||
connect();
|
||||
startPupilWander();
|
||||
startBlinking();
|
||||
|
||||
Executable
+26
@@ -0,0 +1,26 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>Robot Face</title>
|
||||
<link rel="stylesheet" href="/style.css?v=2" />
|
||||
</head>
|
||||
<body class="emotion-neutral">
|
||||
<div id="face" aria-label="Robot face">
|
||||
<div class="eyes">
|
||||
<div class="eye"></div>
|
||||
<div class="eye"></div>
|
||||
</div>
|
||||
|
||||
<div class="mouth">
|
||||
<div class="mouth-shape"></div>
|
||||
</div>
|
||||
|
||||
<div class="label" id="label">neutral</div>
|
||||
</div>
|
||||
|
||||
<script src="/app.js?v=2"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Executable
+261
@@ -0,0 +1,261 @@
|
||||
:root {
|
||||
--bg: #0b0f14;
|
||||
--panel: #121925;
|
||||
--fg: #d7e3f2;
|
||||
|
||||
/* Glow / Stimmung */
|
||||
--glow: rgba(0, 255, 180, 0.22);
|
||||
|
||||
/* Pupillen-Offset (wird via JS verändert) */
|
||||
--pupil-x: 0px;
|
||||
--pupil-y: 0px;
|
||||
|
||||
/* Mund-Parameter (Default = neutral) */
|
||||
--mouth-w: 38vw;
|
||||
--mouth-h: 10vh;
|
||||
--mouth-radius: 999px;
|
||||
--mouth-line-y: 50%;
|
||||
--mouth-line-h: 10px;
|
||||
--mouth-line-opacity: 0.85;
|
||||
|
||||
/* „Smile“-Bogen (0 = aus) */
|
||||
--smile: 0;
|
||||
/* „Frown“-Bogen (0 = aus) */
|
||||
--frown: 0;
|
||||
|
||||
/* „O“-Mund (0 = aus, sonst Größe) */
|
||||
--omouth: 0;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
background: var(--bg);
|
||||
overflow: hidden;
|
||||
font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
|
||||
}
|
||||
|
||||
#face {
|
||||
height: 100%;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
gap: 3.5vh;
|
||||
}
|
||||
|
||||
.eyes {
|
||||
display: flex;
|
||||
gap: 8vw;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.eye {
|
||||
width: 14vw;
|
||||
height: 14vw;
|
||||
max-width: 220px;
|
||||
max-height: 220px;
|
||||
border-radius: 999px;
|
||||
background: var(--panel);
|
||||
box-shadow: 0 0 40px var(--glow);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: border-radius 220ms ease, transform 220ms ease, height 220ms ease;
|
||||
}
|
||||
|
||||
/* Pupille */
|
||||
.eye::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 28%;
|
||||
border-radius: 999px;
|
||||
background: var(--fg);
|
||||
opacity: 0.9;
|
||||
transform: translate(var(--pupil-x), var(--pupil-y));
|
||||
transition: transform 180ms ease;
|
||||
}
|
||||
|
||||
/* Blinzeln: wir "quetschen" das Auge kurz */
|
||||
.eye.blink {
|
||||
transform: scaleY(0.12);
|
||||
}
|
||||
|
||||
/* Mund-Container */
|
||||
.mouth {
|
||||
width: var(--mouth-w);
|
||||
height: var(--mouth-h);
|
||||
max-width: 600px;
|
||||
max-height: 120px;
|
||||
border-radius: var(--mouth-radius);
|
||||
background: var(--panel);
|
||||
box-shadow: 0 0 40px var(--glow);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: width 220ms ease, height 220ms ease, border-radius 220ms ease;
|
||||
}
|
||||
|
||||
/* Mund-Shape (Linie + Bögen + O-Mund) */
|
||||
.mouth-shape {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
/* Mund-Linie */
|
||||
.mouth-shape::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 12%;
|
||||
right: 12%;
|
||||
top: var(--mouth-line-y);
|
||||
height: var(--mouth-line-h);
|
||||
transform: translateY(-50%);
|
||||
background: var(--fg);
|
||||
border-radius: 999px;
|
||||
opacity: var(--mouth-line-opacity);
|
||||
transition: all 220ms ease;
|
||||
}
|
||||
|
||||
/* Smile-Bogen */
|
||||
.mouth-shape::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 16%;
|
||||
right: 16%;
|
||||
top: 38%;
|
||||
height: 55%;
|
||||
border: calc(6px + 6px * var(--smile)) solid rgba(215,227,242,0.85);
|
||||
border-top: none;
|
||||
border-left-color: transparent;
|
||||
border-right-color: transparent;
|
||||
border-bottom-left-radius: 999px;
|
||||
border-bottom-right-radius: 999px;
|
||||
opacity: calc(0.10 + 0.60 * var(--smile));
|
||||
transition: opacity 220ms ease, border-width 220ms ease;
|
||||
}
|
||||
|
||||
/* Frown-Bogen als extra Element über box-shadow Trick */
|
||||
.mouth-shape {
|
||||
filter: drop-shadow(0 0 0 rgba(0,0,0,0));
|
||||
}
|
||||
.mouth-shape.frown::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 16%;
|
||||
right: 16%;
|
||||
bottom: 38%;
|
||||
height: 55%;
|
||||
border: calc(6px + 6px * var(--frown)) solid rgba(215,227,242,0.85);
|
||||
border-bottom: none;
|
||||
border-left-color: transparent;
|
||||
border-right-color: transparent;
|
||||
border-top-left-radius: 999px;
|
||||
border-top-right-radius: 999px;
|
||||
opacity: calc(0.10 + 0.60 * var(--frown));
|
||||
}
|
||||
|
||||
/* O-Mund: wir machen aus dem Mund-Container einen Kreis und verstecken Linie */
|
||||
body.has-omouth .mouth {
|
||||
width: calc(18vw + 8vw * var(--omouth));
|
||||
height: calc(18vw + 8vw * var(--omouth));
|
||||
max-width: 260px;
|
||||
max-height: 260px;
|
||||
border-radius: 999px;
|
||||
}
|
||||
body.has-omouth .mouth-shape::after {
|
||||
left: 28%;
|
||||
right: 28%;
|
||||
top: 50%;
|
||||
height: 42%;
|
||||
border-radius: 999px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
body.has-omouth .mouth-shape::before {
|
||||
opacity: 0; /* Smile aus */
|
||||
}
|
||||
|
||||
/* Label */
|
||||
.label {
|
||||
position: fixed;
|
||||
bottom: 18px;
|
||||
left: 18px;
|
||||
padding: 10px 14px;
|
||||
background: rgba(18, 25, 37, 0.75);
|
||||
color: var(--fg);
|
||||
border-radius: 14px;
|
||||
backdrop-filter: blur(8px);
|
||||
border: 1px solid rgba(255,255,255,0.08);
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* ===== Emotionen über Variablen ===== */
|
||||
|
||||
body.emotion-neutral {
|
||||
--glow: rgba(0, 255, 180, 0.22);
|
||||
--smile: 0;
|
||||
--frown: 0;
|
||||
--omouth: 0;
|
||||
--mouth-line-opacity: 0.85;
|
||||
--mouth-line-h: 10px;
|
||||
}
|
||||
body.emotion-neutral .mouth-shape { }
|
||||
body.emotion-neutral .mouth-shape.frown { } /* no-op */
|
||||
|
||||
body.emotion-happy {
|
||||
--glow: rgba(0, 255, 120, 0.32);
|
||||
--smile: 1;
|
||||
--frown: 0;
|
||||
--omouth: 0;
|
||||
--mouth-line-y: 58%;
|
||||
--mouth-line-h: 12px;
|
||||
}
|
||||
body.emotion-happy .mouth-shape { }
|
||||
body.emotion-happy .mouth-shape.frown { } /* no-op */
|
||||
|
||||
body.emotion-sad {
|
||||
--glow: rgba(120, 180, 255, 0.32);
|
||||
--smile: 0;
|
||||
--frown: 1;
|
||||
--omouth: 0;
|
||||
--mouth-line-y: 42%;
|
||||
--mouth-line-h: 12px;
|
||||
}
|
||||
body.emotion-sad .mouth-shape { }
|
||||
body.emotion-sad .mouth-shape.frown { } /* no-op */
|
||||
|
||||
body.emotion-angry {
|
||||
--glow: rgba(255, 70, 70, 0.32);
|
||||
--smile: 0;
|
||||
--frown: 0.35;
|
||||
--omouth: 0;
|
||||
--mouth-line-opacity: 0.95;
|
||||
--mouth-line-h: 16px;
|
||||
}
|
||||
body.emotion-angry .eye {
|
||||
border-radius: 26% 74% 60% 40% / 55% 45% 55% 45%;
|
||||
transform: rotate(-2deg);
|
||||
}
|
||||
|
||||
body.emotion-surprised {
|
||||
--glow: rgba(255, 220, 90, 0.34);
|
||||
--smile: 0;
|
||||
--frown: 0;
|
||||
--omouth: 1;
|
||||
--mouth-line-opacity: 0.95;
|
||||
}
|
||||
body.emotion-surprised { }
|
||||
body.emotion-surprised.has-omouth { } /* marker in JS */
|
||||
|
||||
body.emotion-sleepy {
|
||||
--glow: rgba(180, 180, 255, 0.22);
|
||||
--smile: 0;
|
||||
--frown: 0;
|
||||
--omouth: 0;
|
||||
--mouth-line-opacity: 0.55;
|
||||
--mouth-line-h: 8px;
|
||||
}
|
||||
body.emotion-sleepy .eye {
|
||||
height: 6vw;
|
||||
max-height: 90px;
|
||||
}
|
||||
|
||||
/* Smooth transition for everything */
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
Reference in New Issue
Block a user