import { useCurrentFrame, useVideoConfig, interpolate } from 'remotion'; import { C } from '../lib/colors'; import { rand, clampLerp, easeInOut, softSpring } from '../lib/easings'; import { BEAT } from '../HeroVideo'; // Beat 2 — the wow moment. // // The prompt words detonate into ~30 particle dots. Each particle has // (a) a random scatter velocity that decays, (b) a magnetic pull toward // a designated target slot on the SERVER SCHEMATIC that materialises in // the centre. As particles arrive, the schematic strokes itself on // line-by-line. A scan-line sweeps over the formed server right before // it settles. const PARTICLE_COUNT = 36; // Target slots on the server schematic — points on the rectangle's // perimeter plus internal "row" markers. Particles slot into these. const SERVER_W = 460; const SERVER_H = 300; const CX = 960; const CY = 540; function targetSlot(i: number) { const N = PARTICLE_COUNT; // Distribute slots: half on the perimeter, half on internal "tool rows". if (i < N / 2) { // perimeter — walk around the rectangle const t = i / (N / 2); const perim = 2 * (SERVER_W + SERVER_H); const d = t * perim; const left = CX - SERVER_W / 2; const top = CY - SERVER_H / 2; let px = left; let py = top; if (d < SERVER_W) { px = left + d; py = top; } else if (d < SERVER_W + SERVER_H) { px = left + SERVER_W; py = top + (d - SERVER_W); } else if (d < 2 * SERVER_W + SERVER_H) { px = left + SERVER_W - (d - SERVER_W - SERVER_H); py = top + SERVER_H; } else { px = left; py = top + SERVER_H - (d - 2 * SERVER_W - SERVER_H); } return { x: px, y: py }; } // Internal tool rows — three horizontal lines inside the server const j = i - N / 2; const row = j % 3; const col = Math.floor(j / 3) / (N / 2 / 3 - 1); const rowY = CY - 60 + row * 60; const rowX = CX - SERVER_W / 2 + 30 + col * (SERVER_W - 60); return { x: rowX, y: rowY }; } export function TransformScene() { const frame = useCurrentFrame(); const { fps } = useVideoConfig(); // Local frame within the beat — frame 0 is the start of Transform. const local = frame - BEAT.transform.in; // 0..110 const total = BEAT.transform.out - BEAT.transform.in; // Entrance + exit envelopes for the scene as a whole. const sceneIn = clampLerp(frame, BEAT.transform.in, BEAT.transform.in + 6); const sceneOut = 1 - clampLerp(frame, BEAT.transform.out - 8, BEAT.transform.out); const sceneAlpha = Math.min(sceneIn, sceneOut); // Schematic stroke-on: 0→1 between frames 35..65 of the beat. const strokeT = easeInOut(clampLerp(local, 30, 70)); // Port pulse readiness — only after schematic strokes complete. const portPulse = clampLerp(local, 70, 95); // Scan-line sweep — diagonal gradient passes once across the formed server const scanT = clampLerp(local, 70, 100); return (
{/* Particles */} {Array.from({ length: PARTICLE_COUNT }).map((_, i) => { // Source: prompt word approximate position spread across the line const wordIndex = i % 4; const wordX = 760 + wordIndex * 130 + rand(i * 7.13) * 60 - 30; const wordY = 540 + rand(i * 3.71) * 20 - 10; // Target slot on server const slot = targetSlot(i); // Scatter velocity — frames 0..25 the particle drifts outward; // then frames 25..60 it pulls toward the target with spring. const vx = (rand(i * 1.31) - 0.5) * 600; const vy = (rand(i * 2.71) - 0.5) * 400; // Phase 1: explosion 0..25 const explode = clampLerp(local, 0, 25); // Phase 2: magnetic pull 25..60 const pull = softSpring(frame, fps, BEAT.transform.in + 25, 36); // Position is: (wordPos) + (vx,vy * explode) lerped toward target by pull const driftX = wordX + vx * explode * (1 - pull); const driftY = wordY + vy * explode * (1 - pull); const x = driftX + (slot.x - driftX) * pull; const y = driftY + (slot.y - driftY) * pull; // Size grows as particles "lock in" const r = interpolate(pull, [0, 1], [2.5, 1.8]); // Color shifts from white → indigo as they lock to schematic const color = pull > 0.6 ? C.accent : C.fg; // Fade in fast at the start so the explosion is visible const alpha = clampLerp(local, 0, 4); // Slight fade-out near end as the schematic takes visual primacy const fadeOut = 1 - clampLerp(local, 85, 105) * 0.3; return ( 0.5 ? `drop-shadow(0 0 4px ${C.accentGlow})` : undefined, }} /> ); })} {/* Server schematic — strokes on as particles arrive */} {/* Outer rectangle — stroke draws perimeter from top-left clockwise */} {/* Three internal "tool" rows — appear after perimeter completes */} {[0, 1, 2].map((r) => { const rowAlpha = clampLerp(local, 60 + r * 4, 72 + r * 4); const y = CY - 60 + r * 60; return ( ); })} {/* Port dots — 4 each side, pulse once schematic locks in */} {[-1, 1].map((side) => [-1, 0, 1].map((off) => ( )), )} {/* Scan-line sweep — diagonal pass after stroke completes */} {scanT > 0 && scanT < 1 && ( )} {/* Corner labels — typographic detail that sells "this is a real server" */}
); } function CornerLabel({ x, y, text, appearAt, delay, align = 'left', }: { x: number; y: number; text: string; appearAt: number; delay: number; align?: 'left' | 'right'; }) { const t = clampLerp(appearAt, delay, delay + 8); return (
{text}
); }