// animations.jsx — Embedded version (no PlaybackBar) // Provides: Stage, Timeline, Sprite, easing helpers for hero animation. const Easing = { linear: (t) => t, easeInQuad: (t) => t * t, easeOutQuad: (t) => t * (2 - t), easeInOutQuad: (t) => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t), easeInCubic: (t) => t * t * t, easeOutCubic: (t) => (--t) * t * t + 1, easeInOutCubic: (t) => (t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1), easeInQuart: (t) => t * t * t * t, easeOutQuart: (t) => 1 - (--t) * t * t * t, easeInOutQuart: (t) => (t < 0.5 ? 8 * t * t * t * t : 1 - 8 * (--t) * t * t * t), easeInExpo: (t) => (t === 0 ? 0 : Math.pow(2, 10 * (t - 1))), easeOutExpo: (t) => (t === 1 ? 1 : 1 - Math.pow(2, -10 * t)), easeInOutExpo: (t) => { if (t === 0) return 0; if (t === 1) return 1; if (t < 0.5) return 0.5 * Math.pow(2, 20 * t - 10); return 1 - 0.5 * Math.pow(2, -20 * t + 10); }, easeInSine: (t) => 1 - Math.cos((t * Math.PI) / 2), easeOutSine: (t) => Math.sin((t * Math.PI) / 2), easeInOutSine: (t) => -(Math.cos(Math.PI * t) - 1) / 2, easeOutBack: (t) => { const c1 = 1.70158, c3 = c1 + 1; return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2); }, easeInBack: (t) => { const c1 = 1.70158, c3 = c1 + 1; return c3 * t * t * t - c1 * t * t; }, easeInOutBack: (t) => { const c1 = 1.70158, c2 = c1 * 1.525; return t < 0.5 ? (Math.pow(2 * t, 2) * ((c2 + 1) * 2 * t - c2)) / 2 : (Math.pow(2 * t - 2, 2) * ((c2 + 1) * (t * 2 - 2) + c2) + 2) / 2; }, easeOutElastic: (t) => { const c4 = (2 * Math.PI) / 3; if (t === 0) return 0; if (t === 1) return 1; return Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * c4) + 1; }, }; const clamp = (v, min, max) => Math.max(min, Math.min(max, v)); function interpolate(input, output, ease = Easing.linear) { return (t) => { if (t <= input[0]) return output[0]; if (t >= input[input.length - 1]) return output[output.length - 1]; for (let i = 0; i < input.length - 1; i++) { if (t >= input[i] && t <= input[i + 1]) { const span = input[i + 1] - input[i]; const local = span === 0 ? 0 : (t - input[i]) / span; const easeFn = Array.isArray(ease) ? (ease[i] || Easing.linear) : ease; const eased = easeFn(local); return output[i] + (output[i + 1] - output[i]) * eased; } } return output[output.length - 1]; }; } function animate({ from = 0, to = 1, start = 0, end = 1, ease = Easing.easeInOutCubic }) { return (t) => { if (t <= start) return from; if (t >= end) return to; const local = (t - start) / (end - start); return from + (to - from) * ease(local); }; } const TimelineContext = React.createContext({ time: 0, duration: 10, playing: false }); const useTime = () => React.useContext(TimelineContext).time; const useTimeline = () => React.useContext(TimelineContext); const SpriteContext = React.createContext({ localTime: 0, progress: 0, duration: 0 }); const useSprite = () => React.useContext(SpriteContext); function Sprite({ start = 0, end = Infinity, children, keepMounted = false }) { const { time } = useTimeline(); const visible = time >= start && time <= end; if (!visible && !keepMounted) return null; const duration = end - start; const localTime = Math.max(0, time - start); const progress = duration > 0 && isFinite(duration) ? clamp(localTime / duration, 0, 1) : 0; const value = { localTime, progress, duration, visible }; return ( {typeof children === 'function' ? children(value) : children} ); } function TextSprite({ text, x = 0, y = 0, size = 48, color = '#111', font = 'Inter, system-ui, sans-serif', weight = 600, entryDur = 0.45, exitDur = 0.35, entryEase = Easing.easeOutBack, exitEase = Easing.easeInCubic, align = 'left', letterSpacing = '-0.01em', }) { const { localTime, duration } = useSprite(); const exitStart = Math.max(0, duration - exitDur); let opacity = 1, ty = 0; if (localTime < entryDur) { const t = entryEase(clamp(localTime / entryDur, 0, 1)); opacity = t; ty = (1 - t) * 16; } else if (localTime > exitStart) { const t = exitEase(clamp((localTime - exitStart) / exitDur, 0, 1)); opacity = 1 - t; ty = -t * 8; } const translateX = align === 'center' ? '-50%' : align === 'right' ? '-100%' : '0'; return (
{text}
); } function ImageSprite({ src, x = 0, y = 0, width = 400, height = 300, entryDur = 0.6, exitDur = 0.4, kenBurns = false, kenBurnsScale = 1.08, radius = 12, fit = 'cover', placeholder = null, }) { const { localTime, duration } = useSprite(); const exitStart = Math.max(0, duration - exitDur); let opacity = 1, scale = 1; if (localTime < entryDur) { const t = Easing.easeOutCubic(clamp(localTime / entryDur, 0, 1)); opacity = t; scale = 0.96 + 0.04 * t; } else if (localTime > exitStart) { const t = Easing.easeInCubic(clamp((localTime - exitStart) / exitDur, 0, 1)); opacity = 1 - t; scale = (kenBurns ? kenBurnsScale : 1) + 0.02 * t; } else if (kenBurns) { const holdSpan = exitStart - entryDur; const holdT = holdSpan > 0 ? (localTime - entryDur) / holdSpan : 0; scale = 1 + (kenBurnsScale - 1) * holdT; } const content = placeholder ? (
{placeholder.label || 'image'}
) : ( ); return (
{content}
); } function RectSprite({ x = 0, y = 0, width = 100, height = 100, color = '#111', radius = 8, entryDur = 0.4, exitDur = 0.3, render, }) { const spriteCtx = useSprite(); const { localTime, duration } = spriteCtx; const exitStart = Math.max(0, duration - exitDur); let opacity = 1, scale = 1; if (localTime < entryDur) { const t = Easing.easeOutBack(clamp(localTime / entryDur, 0, 1)); opacity = clamp(localTime / entryDur, 0, 1); scale = 0.4 + 0.6 * t; } else if (localTime > exitStart) { const t = Easing.easeInQuad(clamp((localTime - exitStart) / exitDur, 0, 1)); opacity = 1 - t; scale = 1 - 0.15 * t; } const overrides = render ? render(spriteCtx) : {}; return (
); } // Embedded Stage — no playback bar, auto-scales to fill container function Stage({ width = 1600, height = 900, duration = 12, background = '#F8F4EA', loop = true, children, }) { const [time, setTime] = React.useState(0); const [scale, setScale] = React.useState(1); const containerRef = React.useRef(null); const rafRef = React.useRef(null); const lastTsRef = React.useRef(null); React.useEffect(() => { if (!containerRef.current) return; const el = containerRef.current; const measure = () => { const s = Math.min(el.clientWidth / width, el.clientHeight / height); setScale(Math.max(0.05, s)); }; measure(); const ro = new ResizeObserver(measure); ro.observe(el); return () => ro.disconnect(); }, [width, height]); React.useEffect(() => { const step = (ts) => { if (lastTsRef.current == null) lastTsRef.current = ts; const dt = (ts - lastTsRef.current) / 1000; lastTsRef.current = ts; setTime((t) => { let next = t + dt; if (next >= duration) { next = loop ? next % duration : duration; } return next; }); rafRef.current = requestAnimationFrame(step); }; rafRef.current = requestAnimationFrame(step); return () => { if (rafRef.current) cancelAnimationFrame(rafRef.current); lastTsRef.current = null; }; }, [duration, loop]); const ctxValue = React.useMemo( () => ({ time, duration, playing: true }), [time, duration] ); return (
{children}
); } Object.assign(window, { Easing, interpolate, animate, clamp, TimelineContext, useTime, useTimeline, Sprite, SpriteContext, useSprite, TextSprite, ImageSprite, RectSprite, Stage, });