Animation Utils
animation-utilsThe curve layer. Every value that moves in a VanillaSky video is some function of scene progress, and this is where those functions live.
EASE.outExpoEASE.outBackEASE.inOutCubicEASE.anticipateEASE.popEASE.editorialSPRING_SMOOTH · peak 1.00SPRING_SNAPPY · peak 1.11SPRING_CRISP · peak 1.25SPRING_BOUNCY · peak 1.28Sampled from the real functions. Easings run the full 0→1; springs are windowed to the first 30% because that is where they move — all four have settled by roughly t=0.2. Only SPRING_SMOOTH arrives without overshoot; the peak on each plot is how far past 1 it travels before settling.
When to use it
Reach for this whenever you need a number to change over a scene — position, opacity, scale, rotation. It is deliberately tiny and has no React dependency, so it also works in a plain render loop. Do not hand-roll easing values next to it; a scene that invents its own curve is the one that looks off next to the others.
What it exports
Mapping progress to values
interpolatestaggerinterpolate maps one range onto another with optional easing and clamping. stagger turns a shared progress into a per-item progress so a list arrives in sequence.
Curves
EasingcubicBezierEasing carries the standard in/out/inOut families. cubicBezier builds a custom curve when none of them fit.
Springs
springSPRING_SMOOTHSPRING_CRISPSPRING_SNAPPYSPRING_BOUNCYSpringConfigFour presets covering the useful range. Prefer a preset over a bespoke damping/stiffness pair — they are what the templates are tuned against.
Example — a card that slides up and settles
import { interpolate, spring, stagger, Easing, SPRING_CRISP } from "@/vanillasky/react-animations/animation-utils";
function Card({ progress, index, total }: { progress: number; index: number; total: number }) {
// Each card gets its own 0→1 window carved out of the shared progress.
const local = stagger(progress, index, total);
// Springs land with a little overshoot; interpolate is linear-with-easing.
const y = interpolate(spring(local, SPRING_CRISP), [0, 1], [64, 0]);
const opacity = interpolate(local, [0, 0.35], [0, 1], {
easing: Easing.out(Easing.cubic),
extrapolateRight: "clamp",
});
return <div style={{ transform: `translateY(${y}px)`, opacity }} />;
}Worth knowing
interpolate extends past its input range by default. Pass extrapolateLeft/extrapolateRight: "clamp" for anything that must not overshoot — opacity above 1 renders fine and then clips oddly in the SVG export path.