Spaces:
Sleeping
Sleeping
| // @ts-nocheck | |
| // Chronixel motion core — every new scene composes from this vocabulary. | |
| // All timing APIs take SECONDS (converted via fps), never raw frames. | |
| import { | |
| Easing, | |
| interpolate, | |
| spring, | |
| useCurrentFrame, | |
| useVideoConfig, | |
| } from "remotion"; | |
| import { | |
| evolvePath, | |
| getLength, | |
| getPointAtLength, | |
| getTangentAtLength, | |
| } from "@remotion/paths"; | |
| /* ---------- easing vocabulary ---------- */ | |
| export const EASE = { | |
| out: Easing.bezier(0.16, 1, 0.3, 1), // house default | |
| inOut: Easing.bezier(0.65, 0, 0.35, 1), | |
| expoOut: Easing.bezier(0.19, 1, 0.22, 1), // fast arrival, long settle | |
| backOut: Easing.bezier(0.34, 1.56, 0.64, 1), // slight overshoot | |
| anticipate: Easing.bezier(0.36, 0, 0.66, -0.56), // pulls back, then launches | |
| // cinematic curves — the slow, weighted moves top studios use for camera/holds | |
| cine: Easing.bezier(0.22, 1, 0.36, 1), // luxurious arrival, very long settle | |
| drift: Easing.bezier(0.45, 0, 0.55, 1), // symmetric, glassy — for camera dollies | |
| snap: Easing.bezier(0.5, 0, 0, 1), // hard launch, instant settle (impacts) | |
| } as const; | |
| // smootherstep (Ken Perlin's quintic) — zero 1st & 2nd derivative at both ends, | |
| // so continuous loops/camera moves never show a velocity seam. | |
| export const smootherstep = (t: number) => { | |
| const x = clamp01(t); | |
| return x * x * x * (x * (x * 6 - 15) + 10); | |
| }; | |
| /* ---------- spring presets ---------- */ | |
| export const SPRING = { | |
| GENTLE: { damping: 200, mass: 0.9 }, // house default — no overshoot | |
| SNAPPY: { damping: 20, mass: 0.55, stiffness: 170 }, // quick, tiny overshoot | |
| BOUNCY: { damping: 12, mass: 0.8, stiffness: 150 }, // playful settle | |
| HEAVY: { damping: 28, mass: 1.5, stiffness: 95 }, // weighty lock-in | |
| } as const; | |
| export type SpringPreset = | |
| | (typeof SPRING)[keyof typeof SPRING] | |
| | { damping?: number; mass?: number; stiffness?: number }; | |
| /* ---------- helpers ---------- */ | |
| export const clamp01 = (v: number) => Math.min(1, Math.max(0, v)); | |
| export const mix = (a: number, b: number, p: number) => a + (b - a) * p; | |
| export const stagger = (i: number, baseSec: number, stepSec: number) => | |
| baseSec + i * stepSec; | |
| // deterministic pseudo-random in [0,1) — safe for Remotion (no Math.random) | |
| export const rand = (seed: number) => { | |
| const x = Math.sin(seed * 127.1 + 311.7) * 43758.5453; | |
| return x - Math.floor(x); | |
| }; | |
| /* ---------- beats: declare scene timing in seconds ---------- */ | |
| // Non-hook versions (safe inside .map() loops): | |
| export const beat = ( | |
| frame: number, | |
| fps: number, | |
| fromSec: number, | |
| toSec: number, | |
| easing: (t: number) => number = EASE.out, | |
| ) => | |
| interpolate(frame, [fromSec * fps, toSec * fps], [0, 1], { | |
| extrapolateLeft: "clamp", | |
| extrapolateRight: "clamp", | |
| easing, | |
| }); | |
| export const springAt = ( | |
| frame: number, | |
| fps: number, | |
| atSec: number, | |
| config: SpringPreset = SPRING.SNAPPY, | |
| ) => spring({ frame: frame - Math.round(atSec * fps), fps, config }); | |
| // Hook version: const b = useBeats({ title: [0.4, 1.2], cards: [1.0, 2.2] }) | |
| export type BeatSpec = Record<string, readonly [number, number]>; | |
| export const useBeats = <T extends BeatSpec>( | |
| spec: T, | |
| easing: (t: number) => number = EASE.out, | |
| ): { [K in keyof T]: number } => { | |
| const frame = useCurrentFrame(); | |
| const { fps } = useVideoConfig(); | |
| const out = {} as { [K in keyof T]: number }; | |
| for (const k in spec) { | |
| out[k] = beat(frame, fps, spec[k][0], spec[k][1], easing); | |
| } | |
| return out; | |
| }; | |
| // seconds elapsed in the scene | |
| export const useT = () => { | |
| const frame = useCurrentFrame(); | |
| const { fps } = useVideoConfig(); | |
| return frame / fps; | |
| }; | |
| /* ---------- duration & continuous motion ---------- */ | |
| // VO-proportional scene duration: ~2.3 words/sec + 1s of air, clamped 6–24s. | |
| // If a beat's VO would exceed ~25s, split it into more scenes at draft time. | |
| export const voSeconds = (words: number) => | |
| Math.min(24, Math.max(6, Math.round(words / 2.3 + 1))); | |
| // sine oscillator in scene-seconds — drives idle pulses/bobs so long scenes never freeze | |
| export const osc = (t: number, period = 4, phase = 0) => | |
| Math.sin((t / period) * Math.PI * 2 + phase); | |
| /* ---------- reveal styles (take a 0..1 progress) ---------- */ | |
| export const revealUp = (p: number, rise = 26): React.CSSProperties => ({ | |
| opacity: p, | |
| transform: `translateY(${(1 - p) * rise}px)`, | |
| }); | |
| export const revealBlur = (p: number, rise = 18): React.CSSProperties => ({ | |
| opacity: p, | |
| transform: `translateY(${(1 - p) * rise}px)`, | |
| filter: `blur(${clamp01(1 - p) * 12}px)`, | |
| }); | |
| export const revealScale = (p: number, from = 0.92): React.CSSProperties => ({ | |
| opacity: p, | |
| transform: `scale(${mix(from, 1, p)})`, | |
| }); | |
| // editorial mask reveal — text rises out from behind a hard edge (the move every | |
| // premium title sequence uses). Pair with a wrapper that has overflow:hidden. | |
| export const revealMask = (p: number, rise = 1): React.CSSProperties => { | |
| const q = clamp01(p); | |
| return { | |
| display: "inline-block", | |
| transform: `translateY(${(1 - q) * 100 * rise}%)`, | |
| opacity: q < 0.04 ? 0 : 1, | |
| }; | |
| }; | |
| // wipe reveal, left → right. | |
| // Script fonts (Caveat etc.) overhang their layout box — clipping exactly at the | |
| // border box shaves the last glyph's flourish. So: no clip at all once fully | |
| // revealed, and generous outsets during the wipe so ascenders/descenders survive. | |
| export const revealClip = (p: number): React.CSSProperties => { | |
| const q = clamp01(p); | |
| if (q >= 1) return {}; | |
| return { clipPath: `inset(-22% ${Math.max(-14, (1 - q) * 116 - 14)}% -22% -14%)` }; | |
| }; | |
| /* ============================================================================ | |
| CHOREOGRAPHY — one call returns a fully composed entrance: fade + directional | |
| slide + scale settle + de-blur, all spring-driven so the element ARRIVES with | |
| weight and overshoots into place instead of just popping on. This is the move | |
| that separates "things faded in" from "things were choreographed". | |
| ========================================================================== */ | |
| export type EnterDir = "up" | "down" | "left" | "right" | "scale" | "none"; | |
| export type EnterOpts = { | |
| from?: EnterDir; | |
| dist?: number; // px of travel | |
| scale?: number; // starting scale (1 = no scale) | |
| blur?: number; // px of entry blur (0 disables) | |
| spring?: SpringPreset; | |
| }; | |
| // frame-based (safe inside .map / loops) | |
| export const enter = ( | |
| frame: number, | |
| fps: number, | |
| at: number, | |
| opts: EnterOpts = {}, | |
| ): React.CSSProperties => { | |
| const { from = "up", dist = 42, scale = 0.94, blur = 10, spring = SPRING.SNAPPY } = opts; | |
| const s = springAt(frame, fps, at, spring); // may overshoot >1 → settle | |
| const o = clamp01(s); | |
| const inv = 1 - s; // keep the overshoot in the transform for a live settle | |
| let tx = 0; | |
| let ty = 0; | |
| if (from === "up") ty = inv * dist; | |
| else if (from === "down") ty = -inv * dist; | |
| else if (from === "left") tx = inv * dist; | |
| else if (from === "right") tx = -inv * dist; | |
| const sc = mix(scale, 1, s); | |
| return { | |
| opacity: o, | |
| transform: `translate(${tx.toFixed(2)}px, ${ty.toFixed(2)}px) scale(${sc.toFixed(4)})`, | |
| filter: blur ? `blur(${(clamp01(inv) * blur).toFixed(2)}px)` : undefined, | |
| willChange: "transform, opacity, filter", | |
| }; | |
| }; | |
| // hook form: const style = useEnter(0.4, { from: "left" }) | |
| export const useEnter = (at: number, opts?: EnterOpts): React.CSSProperties => { | |
| const frame = useCurrentFrame(); | |
| const { fps } = useVideoConfig(); | |
| return enter(frame, fps, at, opts); | |
| }; | |
| // staggered entrances for a list of children — returns one style per index. | |
| // const styles = useStagger(items.length, 0.6, 0.12, { from: "up" }) | |
| export const useStagger = ( | |
| count: number, | |
| baseAt: number, | |
| step: number, | |
| opts?: EnterOpts, | |
| ): React.CSSProperties[] => { | |
| const frame = useCurrentFrame(); | |
| const { fps } = useVideoConfig(); | |
| return Array.from({ length: count }, (_, i) => | |
| enter(frame, fps, baseAt + i * step, opts), | |
| ); | |
| }; | |
| // fade + lift OUT, starting at `at`. Multiply opacity with an entrance for a | |
| // full in→hold→out lifecycle on elements that shouldn't live the whole scene. | |
| export const exit = ( | |
| frame: number, | |
| fps: number, | |
| at: number, | |
| opts: { dist?: number; dur?: number; scale?: number } = {}, | |
| ): React.CSSProperties => { | |
| const { dist = 30, dur = 0.5, scale = 1.04 } = opts; | |
| const p = beat(frame, fps, at, at + dur, EASE.inOut); | |
| return { | |
| opacity: 1 - p, | |
| transform: `translateY(${(-p * dist).toFixed(2)}px) scale(${mix(1, scale, p).toFixed(4)})`, | |
| }; | |
| }; | |
| // emphasis pop — a quick scale overshoot at `at` (for stamps, key numbers, beats). | |
| export const pop = ( | |
| frame: number, | |
| fps: number, | |
| at: number, | |
| amount = 0.12, | |
| config: SpringPreset = SPRING.BOUNCY, | |
| ): number => { | |
| const s = springAt(frame, fps, at, config); | |
| const env = Math.sin(clamp01(s) * Math.PI); // 0 → 1 → 0 | |
| return 1 + env * amount; | |
| }; | |
| // idle breathing scale — keeps a hero element alive without drifting (pairs with | |
| // or replaces Float when you want pulse, not wander). Returns a scale multiplier. | |
| export const breathe = (t: number, amp = 0.015, period = 4, phase = 0): number => | |
| 1 + amp * Math.sin((t / period) * Math.PI * 2 + phase); | |
| /* ============================================================================ | |
| PATHS — draw-on strokes and motion along a path. Wraps @remotion/paths so | |
| scenes stop re-deriving pathLength / stroke-dash math by hand (the connectors, | |
| node graphs, traced underlines and orbiting packets all want this). | |
| ========================================================================== */ | |
| // draw a stroke on as p goes 0→1. Spread onto an SVG <path>/<line>/<polyline>: | |
| // <path d={d} {...drawOn(p, d)} /> (the element needs pathLength-able geometry) | |
| export const drawOn = (p: number, d: string): ReturnType<typeof evolvePath> => | |
| evolvePath(clamp01(p), d); | |
| export const useDrawOn = ( | |
| d: string, | |
| fromSec: number, | |
| toSec: number, | |
| easing: (t: number) => number = EASE.out, | |
| ): ReturnType<typeof evolvePath> => { | |
| const frame = useCurrentFrame(); | |
| const { fps } = useVideoConfig(); | |
| return drawOn(beat(frame, fps, fromSec, toSec, easing), d); | |
| }; | |
| // position + heading at fraction p along a path — for packets/dots/icons that | |
| // ride a wire or orbit. angle is degrees (rotate the element to face travel). | |
| export const alongPath = ( | |
| p: number, | |
| d: string, | |
| ): { x: number; y: number; angle: number } => { | |
| const len = getLength(d); | |
| const at = clamp01(p) * len; | |
| const pt = getPointAtLength(d, at); | |
| const tan = getTangentAtLength(d, at); | |
| return { x: pt.x, y: pt.y, angle: (Math.atan2(tan.y, tan.x) * 180) / Math.PI }; | |
| }; | |
| /* ============================================================================ | |
| EXITS & LIFECYCLE — enter() has direction/scale/blur; exits should match. | |
| exit() (above) is the quick fade-up; exitTo() gives full directional parity, | |
| and lifecycle() composes enter→hold→exit so an element that shouldn't live the | |
| whole scene has an intentional out-state (essential once scenes chain via | |
| transitions instead of hard cuts). | |
| ========================================================================== */ | |
| export const exitTo = ( | |
| frame: number, | |
| fps: number, | |
| at: number, | |
| opts: { to?: EnterDir; dist?: number; dur?: number; scale?: number; blur?: number } = {}, | |
| ): React.CSSProperties => { | |
| const { to = "down", dist = 42, dur = 0.5, scale = 1.04, blur = 0 } = opts; | |
| const p = beat(frame, fps, at, at + dur, EASE.inOut); | |
| let tx = 0; | |
| let ty = 0; | |
| if (to === "up") ty = -p * dist; | |
| else if (to === "down") ty = p * dist; | |
| else if (to === "left") tx = -p * dist; | |
| else if (to === "right") tx = p * dist; | |
| const sc = to === "scale" ? mix(1, scale, p) : 1; | |
| return { | |
| opacity: 1 - p, | |
| transform: `translate(${tx.toFixed(2)}px, ${ty.toFixed(2)}px) scale(${sc.toFixed(4)})`, | |
| filter: blur ? `blur(${(p * blur).toFixed(2)}px)` : undefined, | |
| }; | |
| }; | |
| export const useExit = (at: number, opts?: Parameters<typeof exitTo>[3]): React.CSSProperties => { | |
| const frame = useCurrentFrame(); | |
| const { fps } = useVideoConfig(); | |
| return exitTo(frame, fps, at, opts); | |
| }; | |
| // enter → hold → exit in one style. `at` = entrance, `hold` = seconds held after it | |
| // settles before exiting. Composes enter()/exitTo() (transforms concatenate). | |
| export const lifecycle = ( | |
| frame: number, | |
| fps: number, | |
| opts: { | |
| at: number; | |
| hold: number; | |
| from?: EnterDir; | |
| to?: EnterDir; | |
| dist?: number; | |
| spring?: SpringPreset; | |
| outDur?: number; | |
| }, | |
| ): React.CSSProperties => { | |
| const { at, hold, from = "up", to = "down", dist = 42, spring: sp = SPRING.SNAPPY, outDur = 0.5 } = opts; | |
| const ein = enter(frame, fps, at, { from, dist, spring: sp }); | |
| const outAt = at + 0.55 + hold; | |
| const eout = exitTo(frame, fps, outAt, { to, dist, dur: outDur }); | |
| const oIn = typeof ein.opacity === "number" ? ein.opacity : 1; | |
| const oOut = typeof eout.opacity === "number" ? eout.opacity : 1; | |
| return { | |
| opacity: oIn * oOut, | |
| transform: `${ein.transform ?? ""} ${eout.transform ?? ""}`.trim(), | |
| filter: ein.filter, | |
| willChange: "transform, opacity, filter", | |
| }; | |
| }; |