Moeeldouma's picture
Deploy Eldouma Graphics — Docker Space, bring-your-own-key demo
39d98a0 verified
Raw
History Blame Contribute Delete
13.1 kB
// @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",
};
};