/** Easing and timing helpers shared by every motion template. All pure. */ export function clamp(value: number, min = 0, max = 1): number { return Math.min(max, Math.max(min, value)); } export function lerp(from: number, to: number, amount: number): number { return from + (to - from) * amount; } /** Linear progress 0..1 of `t` through the window [start, end]. */ export function progress(t: number, start: number, end: number): number { if (end <= start) return t >= end ? 1 : 0; return clamp((t - start) / (end - start)); } export function easeInCubic(x: number): number { return x * x * x; } export function easeOutCubic(x: number): number { return 1 - (1 - x) ** 3; } export function easeInOutCubic(x: number): number { return x < 0.5 ? 4 * x * x * x : 1 - (-2 * x + 2) ** 3 / 2; } export function easeOutExpo(x: number): number { return x >= 1 ? 1 : 1 - 2 ** (-10 * x); } /** Overshoots slightly before settling; for pop-in reveals. */ export function easeOutBack(x: number): number { const c1 = 1.70158; const c3 = c1 + 1; return 1 + c3 * (x - 1) ** 3 + c1 * (x - 1) ** 2; } /** * Opacity envelope for a clip of `durationMs`: fades in over `inMs` and out * over the last `outMs`. */ export function fadeEnvelope(t: number, durationMs: number, inMs: number, outMs: number): number { const fadeIn = inMs > 0 ? easeOutCubic(progress(t, 0, inMs)) : 1; const fadeOut = outMs > 0 ? 1 - easeInCubic(progress(t, durationMs - outMs, durationMs)) : 1; return clamp(Math.min(fadeIn, fadeOut)); } /** Deterministic pseudo-random value in [0, 1) for integer seeds. */ export function hash01(a: number, b = 0): number { let h = Math.imul(a ^ 0x9e3779b9, 0x85ebca6b) ^ Math.imul(b + 0x632be5ab, 0xc2b2ae35); h = Math.imul(h ^ (h >>> 15), 0x2c1b3c6d); h = Math.imul(h ^ (h >>> 12), 0x297a2d39); return ((h ^ (h >>> 15)) >>> 0) / 4294967296; }