/* EIPME Handwerker-Spot — 1200×1200, ~11.5s, comic style.
Reads Stage/Sprite/useTime/Easing/interpolate/animate from window (animations.jsx). */
const { Stage, Sprite, useTime, useSprite, Easing, interpolate, animate, 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";
/* ============================================================
HANDWERKER — comic character, slightly exaggerated proportions.
expression: 'grin' | 'wink' | 'shock' | 'smile'
pose: 'crossed' (arms crossed) | 'up' (thumbs-up-ish open pose)
Drawn in a 360×520 viewBox, anchored bottom-center.
============================================================ */
function Handwerker({ expression = "grin", pose = "crossed", sweat = 0, bob = 0, blink = 0 }) {
const shocked = expression === "shock";
// head bobs gently; on shock it jumps a bit
const headY = bob;
/* --- eyes --- */
let eyes;
if (expression === "wink") {
eyes = (
{/* left eye open */}
{/* right eye winking: arc */}
);
} else if (shocked) {
eyes = (
);
} else {
const closed = blink > 0.5;
eyes = closed ? (
) : (
);
}
/* --- mouth --- */
let mouth;
if (shocked) {
mouth = ;
} else if (expression === "smile") {
mouth = ;
} else {
// confident grin (open)
mouth = (
);
}
/* --- brows --- */
const brows = shocked ? (
) : (
);
/* --- arms --- */
const arms =
pose === "crossed" ? (
{/* crossed arms over chest */}
{/* fists */}
) : (
{/* open, hands slightly raised — friendly */}
);
return (
);
}
function SweatDrop({ x, y, p, dir, delay }) {
const lp = clamp((p - delay) / 0.6, 0, 1);
if (lp <= 0) return null;
const fly = Easing.easeOutCubic(lp);
const dx = dir * 46 * fly;
const dy = -30 * fly + 26 * fly * fly;
return (
);
}
/* ============================================================
WERKSTATT — flat comic facade. Includes door + window w/ screen.
Drawn 1200×1200 backdrop.
screen: 'ok' | 'alarm' ; alarmT: 0..1 pulse driver
============================================================ */
function Werkstatt({ screen = "ok", alarmT = 0, doorSign = null }) {
const alarmOn = screen === "alarm";
const pulse = alarmOn ? 0.75 + 0.25 * Math.sin(alarmT * Math.PI * 6) : 0;
return (
);
}
/* ---------- speech bubble ---------- */
function SpeechBubble({ x, y, w, pop, text, sub }) {
const s = Easing.easeOutBack(clamp(pop, 0, 1));
return (
);
}
/* ---------- EIPME shield ---------- */
function Shield({ pop }) {
const s = Easing.easeOutBack(clamp(pop, 0, 1));
return (
);
}
/* ---------- EIPME logo lockup (symbol svg + Geist wordmark) ---------- */
function LogoLockup({ size = 64 }) {
return (
EIPME
);
}
/* ============================================================
SCENES
============================================================ */
const T1 = 0, T1E = 3.4; // Szene 1
const T2 = 3.4, T2E = 6.6; // Szene 2
const T3 = 6.6, T3E = 11.0; // Szene 3 (+2s für Statistiken)
const T4 = 11.0, DUR = 13.6; // Szene 4
/* camera helper: returns transform for the 1200×1200 world */
function useCam(segs) {
const t = useTime();
const sc = interpolate(segs.t, segs.scale, Easing.easeInOutCubic)(t);
const cx = interpolate(segs.t, segs.x, Easing.easeInOutCubic)(t);
const cy = interpolate(segs.t, segs.y, Easing.easeInOutCubic)(t);
return { transform: `scale(${sc}) translate(${-cx}px, ${-cy}px)`, transformOrigin: "600px 600px" };
}
function SceneWorkshop() {
const t = useTime();
const local1 = t - T1;
const inS2 = t >= T2;
const local2 = Math.max(0, t - T2);
// camera: S1 slow push-in on character; S2 whip to the window/screen, then pull to face
const cam = useCam({
t: [0, 3.0, T2, T2 + 0.55, T2 + 1.7, T2 + 2.2, T2E],
scale: [1.0, 1.12, 1.12, 1.85, 1.85, 1.45, 1.42],
x: [0, 30, 30, 195, 195, 60, 55],
y: [0, 40, 40, -9, -9, 41, 41],
});
// character states
const wink = local1 > 2.1 && local1 < 2.75;
const blink = local1 > 1.1 && local1 < 1.28 ? 1 : 0;
const shockStart = T2 + 0.9;
const shocked = t >= shockStart;
const sweat = clamp((t - shockStart - 0.15) / 1.1, 0, 1);
const bob = Math.sin(t * 2.1) * 4 + (shocked ? -6 * Easing.easeOutBack(clamp((t - shockStart) / 0.4, 0, 1)) : 0);
const jitter = shocked ? Math.sin(t * 26) * 2.4 * clamp(1 - (t - shockStart) / 2.4, 0.25, 1) : 0;
// alarm flash overlay on whole frame
const alarmFlash = inS2 ? 0.10 + 0.08 * Math.sin(local2 * Math.PI * 5) : 0;
const alarmRamp = clamp((t - (T2 + 0.35)) / 0.25, 0, 1);
const bubblePop = clamp(local1 / 0.55, 0, 1) * (t < T2 - 0.25 ? 1 : clamp((T2 - t) / 0.25, 0, 1));
// exclamation "!" pop over head in S2
const bangPop = Easing.easeOutBack(clamp((t - shockStart) / 0.35, 0, 1));
return (
= T2 + 0.35 ? "alarm" : "ok"} alarmT={local2} />
{/* character standing in front, left of door */}
{/* speech bubble S1 */}
{t < T2 && (
)}
{/* shock "!" S2 */}
{shocked && t < T2E - 0.1 && (
)}
{/* red alarm wash */}
{inS2 && (
)}
{/* red vignette in S2 */}
{inS2 && (
)}
);
}
/* Szene 3 — Tür + Schild "Heute geschlossen" + Zahlen */
function SceneClosed() {
const { localTime: lt } = useSprite();
// camera: tight on door
const sc = interpolate([0, 4.4], [1.9, 2.02], Easing.easeInOutCubic)(lt);
// sign drops + swings
const drop = Easing.easeOutBounce ? Easing.easeOutBounce(clamp(lt / 0.8, 0, 1)) : Easing.easeOutBack(clamp(lt / 0.7, 0, 1));
const dropY = -520 + 520 * drop;
const swing = Math.sin(Math.max(0, lt - 0.75) * 4.2) * 7 * Math.exp(-Math.max(0, lt - 0.75) * 1.6);
const statsIn = Easing.easeOutCubic(clamp((lt - 0.95) / 0.4, 0, 1));
const statsPop = Easing.easeOutBack(clamp((lt - 0.95) / 0.55, 0, 1));
const countP = Easing.easeOutCubic(clamp((lt - 1.0) / 1.5, 0, 1));
const doorSign = (
Heute
geschlossen
);
return (
);
}
function StatChip({ value, label, p }) {
const shown = Math.round(value * p);
return (
);
}
/* Szene 4 — hell, freundlich: Handwerker + Schild + Frage + Logo */
function SceneFinal() {
const { localTime: lt } = useSprite();
const inWipe = Easing.easeOutCubic(clamp(lt / 0.5, 0, 1));
const charIn = Easing.easeOutBack(clamp((lt - 0.15) / 0.5, 0, 1));
const shieldPop = clamp((lt - 0.5) / 0.5, 0, 1);
const textIn = Easing.easeOutCubic(clamp((lt - 0.85) / 0.5, 0, 1));
const logoIn = Easing.easeOutCubic(clamp((lt - 1.3) / 0.5, 0, 1)); const bob = Math.sin(lt * 2.2) * 4;
const blink = (lt % 2.6) > 2.44 ? 1 : 0;
const breathe = 1 + 0.006 * Math.sin(lt * 1.6);
return (
{/* soft blue arc backdrop */}
{/* circular wipe reveal */}
{/* character bottom-left */}
{/* shield next to him */}
{/* headline + logo right block */}
Ist es Ihnen das wert?
IT-Sicherheit für den Mittelstand
);
}
/* ============================================================ */
function HandwerkerSpot() {
// stamp timestamp label for comments
const ref = R.useRef(null);
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.HandwerkerSpot = HandwerkerSpot;