import { useCurrentFrame, useVideoConfig, interpolate, spring } from 'remotion'; import { C } from '../lib/colors'; import { rand, clampLerp, easeInOut } from '../lib/easings'; import { BEAT } from '../HeroVideo'; // Beat 2 — the wow moment. // // The collapsed prompt point at (960, 540) detonates RADIALLY into ~60 // glowing particles that scatter spherically, then magnetically snap into // target slots along a SERVER SCHEMATIC. Particles are supporting players: // small enough that the schematic — the thing being built — reads as the // primary subject. Schematic strokes on IN PARALLEL with the convergence // so the eye always has something to anchor to. const PARTICLE_COUNT = 60; const SERVER_W = 720; const SERVER_H = 420; const CX = 960; const CY = 540; function targetSlot(i: number) { const N = PARTICLE_COUNT; if (i < N / 2) { // perimeter walk 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 }; } // Inside the box: three tool rows const j = i - N / 2; const perRow = Math.ceil(N / 2 / 3); const row = Math.floor(j / perRow); const col = (j % perRow) / Math.max(1, perRow - 1); const rowY = CY - 90 + row * 90; const rowX = CX - SERVER_W / 2 + 50 + col * (SERVER_W - 100); return { x: rowX, y: rowY }; } export function TransformScene() { const frame = useCurrentFrame(); const { fps } = useVideoConfig(); const local = frame - BEAT.transform.in; 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 strokes on IN PARALLEL with the convergence — starts at // local 8 instead of 30 so the box is visible before particles arrive. const strokeT = easeInOut(clampLerp(local, 8, 55)); // Ports + rows show up as schematic completes const innerT = clampLerp(local, 35, 65); const portPulse = clampLerp(local, 55, 90); // Scan-line — diagonal pass once the schematic is fully drawn const scanT = clampLerp(local, 55, 90); // Central core glow — visible throughout Beat 2 so the eye has an // anchor even when particles are mid-flight. Pulses softly. const coreAlpha = clampLerp(local, 0, 12) * (1 - clampLerp(local, 95, 110) * 0.4); const corePulse = 1 + 0.25 * Math.sin(local * 0.18); return (
{/* Central core — a hint of radial glow at the explosion origin. Toned down from earlier versions so the schematic, not the core, carries the visual weight. */}
{/* Particles — 60 small glowing dots radiating from a single origin. They support the schematic; they do not dominate it. */} {Array.from({ length: PARTICLE_COUNT }).map((_, i) => { // SINGLE-POINT ORIGIN. All 60 particles start at canvas center — // the exact point Beat 1's prompt just collapsed into. This is // what makes the explosion read as radial/spherical instead of // horizontal. const originX = CX; const originY = CY; const slot = targetSlot(i); // Velocity vectors — even spherical distribution. Golden-angle // stratification of `i` plus a small jitter prevents the visible // banding you'd get from a pure uniform-random angle on 60 dots. const goldenAngle = (i * 2.39996323) % (Math.PI * 2); const jitter = (rand(i * 1.71) - 0.5) * 0.35; const angle = goldenAngle + jitter; const speed = 220 + rand(i * 4.13) * 320; const vx = Math.cos(angle) * speed; const vy = Math.sin(angle) * speed; const explode = clampLerp(local, 0, 22); // Pull — slower, more deliberate. Inlined spring so we can set // the exact damping/mass/stiffness/duration the scene needs // without bloating the easings module. const pull = spring({ frame: frame - (BEAT.transform.in + 22), fps, config: { damping: 25, mass: 1.3, stiffness: 55 }, durationInFrames: 60, }); const driftX = originX + vx * explode * (1 - pull); const driftY = originY + vy * explode * (1 - pull); const x = driftX + (slot.x - driftX) * pull; const y = driftY + (slot.y - driftY) * pull; // Radius: 4→2 as particles lock in. Smaller than v2 so the // schematic carries primary visual weight. const r = interpolate(pull, [0, 1], [4, 2]); const color = C.accent; const alpha = clampLerp(local, 0, 4); const fadeOut = 1 - clampLerp(local, 88, 108) * 0.4; return ( ); })} {/* Server schematic */} {/* Inner panel — fills earlier and darker so the schematic reads as a solid object the particles are building, not a wireframe sketch. Reaches 0.9 opacity by the time the strokes complete. */} {/* Outer rectangle stroke — wider and with a heavier drop-shadow so the chassis outline is the dominant element on screen. */} {/* Three internal tool rows */} {[0, 1, 2].map((r) => { const y = CY - 90 + r * 90; return ( ); })} {/* Port dots */} {[-1, 1].map((side) => [-1, 0, 1].map((off) => ( )), )} {/* Scan-line sweep */} {scanT > 0 && scanT < 1 && ( )} {/* Corner labels — bigger and earlier */}
); } 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}
); }