// @ds-adherence-ignore -- omelette starter scaffold (raw elements/hex/px by design) // Copied omelette starter. Re-running copy_starter_component with this kind overwrites this file with the latest version (page content is unaffected). /* BEGIN USAGE */ // animations.jsx — timeline engine. Exports (on window): Stage, Sprite, // TextSprite, ImageSprite, RectSprite, VideoSprite, PlaybackBar, // useTime, useTimeline, useSprite, Easing, interpolate, animate, clamp. // // // // // // // // // // // Stage({width,height,duration,background,fps,loop,autoplay}) — auto-scales to // viewport; scrubber + play/pause + ←/→ seek + space + 0-reset; persists // playhead. The canvas is an , export-ready: Share → // Export → Video (or the PlaybackBar's download button) renders it to .mp4. // Stage OWNS the exportable-video contract (the // data-om-exportable-video-with-duration-secs attribute + seek listener + // font inlining) — NEVER put that attribute on any other element; a second // nested "exportable root" makes export and the host timeline bind to the // wrong element and silently breaks playback control. // Screenshot tools DOM-rerender (not pixel-capture) and unwrap this wrapper // so captures should work — but if one comes back black, that's a capture // artifact, not a render bug; trust the live preview. // Sprite({start,end,keepMounted}) — mounts children only while playhead is in // [start,end]. Children read {localTime, progress, duration} via useSprite(). // useTime() → seconds; useTimeline() → {time,duration,playing,setTime,setPlaying}. // TextSprite({text,x,y,size,color,font,weight,align,entryDur,exitDur}) — fades/scales in+out. // ImageSprite({src,x,y,width,height,fit,radius,kenBurns,placeholder}) — same, with optional ken-burns. // RectSprite({x,y,width,height,color,radius}) — solid box with entry/exit. // VideoSprite({src,start,end,speed,style}) — looped clip synced to the // timeline; its audio is mixed into the exported video. // Easing.{linear,easeIn/Out/InOut Quad/Cubic/Quart/Quint/Expo/Back, …} // interpolate([t0,t1,…],[v0,v1,…],ease?) → (t)=>v — piecewise tween. // animate({from,to,start,end,ease}) → (t)=>v — single tween. // // Build scenes by composing Sprites inside Stage. Absolutely-position elements. // // In a .dc.html project, put your scene in a sibling my-scene.jsx (reading // {Stage, Sprite, useTime, Easing, …} from window is safe) and mount BOTH: // // The two files in from= load in order, so my-scene.jsx can use the globals // animations.jsx set. /* END USAGE */ // ───────────────────────────────────────────────────────────────────────────── // ── Easing functions (hand-rolled, Popmotion-style) ───────────────────────── // All easings take t ∈ [0,1] and return eased t ∈ [0,1] (may overshoot for back/elastic). const Easing = { linear: (t) => t, // Quad easeInQuad: (t) => t * t, easeOutQuad: (t) => t * (2 - t), easeInOutQuad: (t) => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t), // Cubic 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), // Quart 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), // Expo 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); }, // Sine 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, // Back (overshoot) 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; }, // Elastic 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; }, }; // ── Core interpolation helpers ────────────────────────────────────────────── // Clamp a value to [min, max] const clamp = (v, min, max) => Math.max(min, Math.min(max, v)); // interpolate([0, 0.5, 1], [0, 100, 50], ease?) -> fn(t) // Popmotion-style: linearly maps t across input keyframes to output values, // with optional easing per segment (single fn or array of fns). 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]; }; } // animate({from, to, start, end, ease})(t) — simpler single-segment tween. // Returns `from` before `start`, `to` after `end`. 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); }; } // ── Timeline context ──────────────────────────────────────────────────────── const TimelineContext = React.createContext({ time: 0, duration: 10, playing: false }); const useTime = () => React.useContext(TimelineContext).time; const useTimeline = () => React.useContext(TimelineContext); // ── Sprite ────────────────────────────────────────────────────────────────── // Renders children only when the playhead is inside [start, end]. Provides // a sub-context with `localTime` (seconds since start) and `progress` (0..1). // // // {({ localTime, progress }) => } // // // Or as a plain wrapper — children can call useSprite() themselves. 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} ); } // ── Sample sprite components ──────────────────────────────────────────────── // TextSprite: fades/slides text in on entry, holds, then fades out on exit. // Props: text, x, y, size, color, font, entryDur, exitDur, align 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; let 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} ); } // ImageSprite: scales + fades in; optional Ken Burns drift during hold. 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, // {label: string} for striped placeholder }) { const { localTime, duration } = useSprite(); const exitStart = Math.max(0, duration - exitDur); let opacity = 1; let 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} ); } // RectSprite: simple rectangle that animates position/size/color via props. // Useful demo primitive — takes a `render` fn for per-frame customization. function RectSprite({ x = 0, y = 0, width = 100, height = 100, color = '#111', radius = 8, entryDur = 0.4, exitDur = 0.3, render, // optional: (ctx) => style overrides }) { const spriteCtx = useSprite(); const { localTime, duration } = spriteCtx; const exitStart = Math.max(0, duration - exitDur); let opacity = 1; let 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 ( ); } // ── Font inlining ─────────────────────────────────────────────────────────── // Copy every @font-face rule from the page into a