// 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 (
);
}
Object.assign(window, {
Easing, interpolate, animate, clamp,
TimelineContext, useTime, useTimeline,
Sprite, SpriteContext, useSprite,
TextSprite, ImageSprite, RectSprite,
Stage,
});