/* EIPME KI-Spot — 1200×1200, ~14.4s, comic style. Gleiche Figur/Werkstatt wie Cyber-Spot.
Reads Stage/Sprite/useTime/useSprite/Easing/interpolate/clamp from window (animations.jsx). */
const { Stage, Sprite, useTime, useSprite, Easing, interpolate, clamp } = window;
const R = React;
/* ---------- palette (EIPME tokens, literal values) ---------- */
const C = {
blue: "#1F5EFF", blue600: "#0A47E0", blue300: "#8AA8FF", blue100: "#D9E4FF", blue50: "#EEF3FF",
orange: "#FF7A1A", orange300: "#FFA15C", orange100: "#FFDEC2",
red: "#FF3B47", red600: "#E51F2D", red100: "#FFD6DA",
ink: "#0C0E13", ink700: "#1C202A", ink500: "#3D434F", ink300: "#858B98", ink100: "#D6D9DF",
paper: "#FAFBFC", white: "#FFFFFF", sky: "#EEF3FF",
skin: "#FFD9B8", skinShade: "#F2BE93", cheek: "#FFB08A",
};
const INK = C.ink;
const GEIST = "'Geist', system-ui, sans-serif";
const MONO = "'Geist Mono', ui-monospace, monospace";
/* ============================================================
KOPF — shared comic head, origin = neck-base center.
expression: grin | smile | wink | tired | talk | relaxed | shock
============================================================ */
function Kopf({ expression = "grin", blink = 0, talkPhase = 0 }) {
const shocked = expression === "shock";
let eyes;
if (expression === "tired") {
eyes = (
);
} else if (expression === "relaxed") {
eyes = (
);
} else if (expression === "wink") {
eyes = (
);
} else if (shocked) {
eyes = (
);
} else {
const closed = blink > 0.5;
eyes = closed ? (
) : (
);
}
let mouth;
if (expression === "tired") {
mouth = ;
} else if (expression === "talk") {
const open = 5 + 11 * Math.abs(Math.sin(talkPhase));
mouth = ;
} else if (expression === "smile" || expression === "relaxed") {
mouth = ;
} else if (shocked) {
mouth = ;
} else {
mouth = ;
}
const brows =
expression === "tired" ? (
) : shocked ? (
) : (
);
return (
{!shocked && }
{!shocked && }
{brows}
{eyes}
{mouth}
{/* Mütze */}
E
);
}
/* ============================================================
STEHENDE FIGUR — für Outro (pose 'wave'), 360×520 viewBox.
============================================================ */
function StandHandwerker({ expression = "grin", blink = 0, bob = 0, waveA = 0 }) {
return (
);
}
/* ============================================================
INNENRAUM — Werkstatt von innen, 1200×1200.
sky: 'night' | 'day' — Uhr, Fenster, Pegboard, Lampe, Tisch,
Papierstapel (stackP skaliert), sitzende Figur.
============================================================ */
function Stapel({ x, w, h, p, t, seed }) {
const n = Math.max(2, Math.round(h / 12));
const sheets = [];
for (let i = 0; i < n; i++) {
sheets.push(
);
}
const wob = Math.sin(t * 2 + seed) * 0.9 * p;
return (
{sheets}
);
}
function FlySheet({ x0, y0, dir, p }) {
if (p <= 0) return null;
const f = Easing.easeOutCubic(clamp(p, 0, 1));
const dx = dir * (120 + 180 * f);
const dy = -160 * f + 330 * f * f;
return (
);
}
function SitzChar({ expression, blink = 0, talkPhase = 0, bob = 0, slumpP = 0, leanP = 0, armPose = "table", phoneUpP = 0 }) {
const lean = -9 * leanP;
const phoneRot = 76 * (1 - Easing.easeOutBack(clamp(phoneUpP, 0, 1)));
let arms;
if (armPose === "phone") {
arms = (
0.05 ? 1 : 0}>
);
} else if (armPose === "lean") {
arms = (
);
} else {
arms = (
);
}
return { lean, arms };
}
function Innenraum({ sky = "night", clockH = 275, clockM = 60, stackP = 1, floorPaperO = 1, wobT = 0, char, sheets = null }) {
const night = sky === "night";
const wall = night ? "#DCE4F6" : C.blue50;
const floor = night ? "#C6CBD8" : C.ink100;
const { lean, arms } = SitzChar(char);
const headTilt = 7 * (char.slumpP || 0) - 3 * (char.leanP || 0);
return (
);
}
/* ---------- Sprechblase ---------- */
function SpeechBubble({ x, y, w, pop, text, tailX = 64, fontSize = 54 }) {
const s = Easing.easeOutBack(clamp(pop, 0, 1));
return (
);
}
/* ---------- KI-Orb — freundlicher Assistent ---------- */
function Orb({ pop, t, size = 130 }) {
const s = Easing.easeOutBack(clamp(pop, 0, 1));
if (s <= 0) return null;
const bobY = Math.sin(t * 2.4) * 8;
const blinkO = (t % 2.3) > 2.16 ? 1 : 0;
return (
);
}
function Sparkle({ x, y, p, size = 26, color = C.orange }) {
const lp = clamp(p, 0, 1);
if (lp <= 0) return null;
const s = Easing.easeOutBack(Math.min(1, lp * 1.6));
const o = 1 - Math.max(0, lp - 0.6) / 0.4;
return (
);
}
/* ---------- Angebot-Dokument ---------- */
function AngebotDoc({ x, y, rot, scale, typeP }) {
const widths = [88, 72, 82, 58];
return (
{widths.map((w, i) => {
const lp = Easing.easeOutCubic(clamp((typeP - i * 0.16) / 0.2, 0, 1));
return
;
})}
);
}
/* ============================================================
SZENE 1+2 — Abends am Tisch → KI-Orb + Handy + Angebot fliegt.
0 … 6.6 (lokal)
============================================================ */
function SceneAbend() {
const { localTime: lt } = useSprite();
const TT = [0, 3.0, 3.2, 3.75, 4.9, 5.5, 6.6];
const sc = interpolate(TT, [1.02, 1.1, 1.1, 1.3, 1.3, 1.16, 1.14], Easing.easeInOutCubic)(lt);
const cx = interpolate(TT, [0, 8, 8, 50, 50, 28, 26], Easing.easeInOutCubic)(lt);
const cy = interpolate(TT, [8, 20, 20, -22, -22, 4, 6], Easing.easeInOutCubic)(lt);
const orbPop = clamp((lt - 3.3) / 0.4, 0, 1);
const phoneUpP = clamp((lt - 3.5) / 0.35, 0, 1);
const talking = lt > 3.9 && lt < 4.85;
const expression = lt < 3.35 ? "tired" : lt < 3.9 ? "smile" : lt < 4.85 ? "talk" : lt > 5.8 && lt < 6.35 ? "wink" : "grin";
const blink = (lt > 1.5 && lt < 1.72) || (lt > 2.7 && lt < 2.9) || (lt > 5.2 && lt < 5.36) ? 1 : 0;
const hop = -12 * Easing.easeOutBack(clamp((lt - 3.32) / 0.3, 0, 1)) * (1 - clamp((lt - 3.75) / 0.35, 0, 1));
const bob = Math.sin(lt * 1.7) * 4 + hop;
const slumpP = 1 - Easing.easeInOutCubic(clamp((lt - 3.3) / 0.5, 0, 1));
const bubblePop = clamp((lt - 0.4) / 0.5, 0, 1) * (lt < 2.85 ? 1 : clamp((3.1 - lt) / 0.25, 0, 1));
const flyP = Easing.easeInOutCubic(clamp((lt - 4.15) / 0.85, 0, 1));
const docX = 790 + (400 - 790) * flyP;
const docY = 500 + (430 - 500) * flyP - Math.sin(flyP * Math.PI) * 190;
const docRot = 16 - 20 * flyP;
const docS = interpolate([0, 0.6, 1], [0.15, 0.4, 1], Easing.easeInOutCubic)(flyP);
const typeP = clamp((lt - 5.0) / 1.2, 0, 1);
return (
{/* Sprechblase S1 — rechts oben, Uhr bleibt sichtbar */}
{lt < 3.2 &&
}
{/* KI-Orb */}
{lt >= 3.25 && (
)}
{/* Sprechwellen Richtung Handy */}
{talking && (
)}
{/* Angebot fliegt aus dem Handy */}
{lt >= 4.15 &&
}
{/* Funkeln bei Landung */}
{lt >= 5.0 && (
)}
);
}
/* ============================================================
SZENE 3 — Dreierschnitt: Mail / Foto→Bericht / Kalender.
6.6 … 10.2 (je 1.2s)
============================================================ */
function PanelMail({ p }) {
const widths = [92, 78, 88, 62, 40];
const planeP = Easing.easeInOutCubic(clamp((p - 0.74) / 0.2, 0, 1));
const idx = Math.min(4, Math.max(0, Math.floor((p - 0.14) / 0.1)));
const curP = Easing.easeOutCubic(clamp((p - 0.14 - idx * 0.1) / 0.11, 0, 1));
return (
{widths.map((w, i) => {
const lp = Easing.easeOutCubic(clamp((p - 0.14 - i * 0.1) / 0.11, 0, 1));
return
;
})}
-0.3 ? 1 : 0 }} />
{/* Papierflieger */}
{p > 0.74 && (
)}
{p > 0.8 && (
)}
);
}
function PanelBericht({ p }) {
const flipP = clamp((p - 0.42) / 0.22, 0, 1);
const showPhoto = flipP < 0.5;
const sxPhoto = Math.max(0, 1 - flipP * 2);
const sxDoc = Math.max(0, flipP * 2 - 1);
const checkS = Easing.easeOutBack(clamp((p - 0.78) / 0.14, 0, 1));
return (
{showPhoto ? (
Baustelle.jpg
) : (
{[90, 76, 84, 64, 78, 46].map((w, i) => (
))}
)}
{/* Funkeln beim Flip */}
);
}
function PanelKalender({ p }) {
const days = ["M", "D", "M", "D", "F", "S", "S"];
const pills = { 2: 0, 5: 1, 9: 2, 12: 3, 16: 4, 19: 5, 24: 6 };
const checks = { 5: 0, 12: 1, 19: 2 };
const cells = [];
for (let i = 0; i < 28; i++) {
const pi = pills[i];
const pillS = pi !== undefined ? Easing.easeOutBack(clamp((p - 0.14 - pi * 0.08) / 0.14, 0, 1)) : 0;
const ci = checks[i];
const checkS = ci !== undefined ? Easing.easeOutBack(clamp((p - 0.5 - ci * 0.1) / 0.14, 0, 1)) : 0;
cells.push(
{i + 1}
{pi !== undefined && (
)}
{ci !== undefined && (
)}
);
}
return (
{days.map((d, i) => (
{d}
))}
{cells}
);
}
function SceneCuts({ showCaptions }) {
const { localTime: lt } = useSprite();
const seg = Math.min(2, Math.floor(lt / 1.2));
const segT = lt - seg * 1.2;
const p = clamp(segT / 1.2, 0, 1);
const enter = Easing.easeOutCubic(clamp(segT / 0.2, 0, 1));
const flash = Math.max(0, 1 - segT / 0.1);
const bgs = [C.blue50, C.orange100, "#E7EDFB"];
const captions = ["E-Mail · schreibt sich selbst", "Foto → fertiger Bericht", "Termine · geplant"];
const capPop = Easing.easeOutBack(clamp((segT - 0.12) / 0.3, 0, 1));
return (
{seg === 0 &&
}
{seg === 1 &&
}
{seg === 2 &&
}
{/* Mini-Orb arbeitet mit */}
{/* Schrittzähler */}
{seg + 1} / 3
{showCaptions && (
)}
);
}
/* ============================================================
SZENE 4 — Stapel schrumpft, Uhr springt zurück, Lehnen + Lächeln.
10.2 … 12.2
============================================================ */
function SceneRelief() {
const { localTime: lt } = useSprite();
const sc = interpolate([0, 2.0], [1.05, 1.1], Easing.easeInOutCubic)(lt);
const shrinkP = Easing.easeInOutCubic(clamp((lt - 0.2) / 0.6, 0, 1));
const stackP = 1 - 0.82 * shrinkP;
const spinP = Easing.easeInOutCubic(clamp((lt - 0.45) / 0.9, 0, 1));
const clockH = 275 + (150 - 275) * spinP;
const clockM = 60 - 720 * spinP;
const leanP = Easing.easeOutBack(clamp((lt - 1.05) / 0.45, 0, 1));
const expression = lt < 1.15 ? "smile" : "relaxed";
const bob = Math.sin(lt * 1.9) * 3;
const sheets = (
);
return (
);
}
/* ============================================================
SZENE 5 — Feierabend + Claim + Logo. 12.2 … 14.4
============================================================ */
function LogoLockup({ size = 64 }) {
return (
EIPME
);
}
function SceneOutro({ headline }) {
const { localTime: lt } = useSprite();
const wipe = Easing.easeOutCubic(clamp(lt / 0.5, 0, 1));
const walkP = Easing.easeOutCubic(clamp(lt / 0.75, 0, 1));
const x = -430 + 430 * walkP;
const bob = Math.sin(lt * 13) * (7 * (1 - walkP) + 2);
const waveRamp = Easing.easeOutBack(clamp((lt - 0.85) / 0.3, 0, 1));
const waveA = Math.sin(Math.max(0, lt - 0.85) * 7) * 15 * waveRamp;
const textIn = Easing.easeOutCubic(clamp((lt - 0.5) / 0.5, 0, 1));
const logoIn = Easing.easeOutCubic(clamp((lt - 0.95) / 0.5, 0, 1));
const blink = (lt % 2.4) > 2.26 ? 1 : 0;
const breathe = 1 + 0.006 * Math.sin(lt * 1.6);
return (
);
}
/* ============================================================ */
function HandwerkerKISpot({ headline, showCaptions }) {
const ref = R.useRef(null);
const HL = headline || "KI, die Ihnen Zeit schenkt.";
const caps = showCaptions !== false;
return (
);
}
function TimestampLabel({ rootRef }) {
const t = useTime();
const sec = Math.floor(t);
R.useEffect(() => {
const el = rootRef.current && rootRef.current.querySelector("[data-om-exportable-video-with-duration-secs]");
(el || rootRef.current) && (el || rootRef.current).setAttribute("data-screen-label", "t=" + sec + "s");
}, [sec]);
return null;
}
window.HandwerkerKISpot = HandwerkerKISpot;