import React from "react"; import { useCurrentFrame, useVideoConfig, spring, interpolate } from "remotion"; import { FONT_DISPLAY, FONT_BODY } from "./parts"; /* ═══════════════════════════════════════════════════════════════════════ REUSABLE MONO ASSETS — animated visual building blocks. All monochrome flat, transform/opacity only (no blur/SVG filters → fast software render). Each is a self-contained component sized to fit the "asset zone" of a scene (typically placed above the caption card). PITFALL (verified): a flex-row container whose children use `flex:1` collapses to width 0 when it is itself a horizontal flex item (intrinsic width = sum of flex-basis:0 items = 0). Always give root wrappers an explicit width so assets render regardless of parent layout. ═══════════════════════════════════════════════════════════════════════ */ const MONO = "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"; /** Growing bar chart — editorial "growth" visual. */ export const ChartBars: React.FC<{ bars?: number; delay?: number }> = ({ bars = 5, delay = 0 }) => { const frame = useCurrentFrame(); const { fps } = useVideoConfig(); const heights = [0.42, 0.66, 0.5, 0.82, 0.62, 0.92, 0.72, 0.55, 0.88, 0.6]; const MAX_H = 180; return (
{Array.from({ length: bars }).map((_, i) => { const h = heights[(i + Math.floor(delay)) % heights.length]; const grow = interpolate(frame, [delay + i * 3, delay + i * 3 + 12], [0.05, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp", }); const px = Math.max(2, Math.round(h * MAX_H * grow)); return (
); })}
); }; /** Line chart that draws itself left→right (clip-width animation). */ export const ChartLine: React.FC<{ delay?: number }> = ({ delay = 0 }) => { const frame = useCurrentFrame(); const { fps } = useVideoConfig(); const p = interpolate(frame, [delay, delay + fps * 0.9], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" }); return ( {[60, 140, 220].map((y) => ( ))} {/* reveal mask */} ); }; /** Ring/donut progress (percent). */ export const RingProgress: React.FC<{ pct?: number; size?: number; delay?: number }> = ({ pct = 0.72, size = 220, delay = 0 }) => { const frame = useCurrentFrame(); const { fps } = useVideoConfig(); const p = spring({ frame: Math.max(0, frame - delay), fps, config: { damping: 20, stiffness: 60 } }); const r = 80; const c = 2 * Math.PI * r; const filled = Math.max(0, Math.min(1, pct * p)); return ( {Math.round(filled * 100)} % ); }; /** Gauge with sweeping needle. */ export const Gauge: React.FC<{ value?: number; label?: string; delay?: number }> = ({ value = 0.62, label = "ACCURACY", delay = 0 }) => { const frame = useCurrentFrame(); const { fps } = useVideoConfig(); const v = spring({ frame: Math.max(0, frame - delay), fps, config: { damping: 16, stiffness: 70 } }); const angle = -90 + 180 * value * v; // -90°..+90° return ( {Math.round(value * 100)}% {label} ); }; /** Scrolling ticker tape (endless marquee text). */ export const TickerTape: React.FC<{ text: string; speed?: number }> = ({ text, speed = 90 }) => { const frame = useCurrentFrame(); const { fps } = useVideoConfig(); const t = frame / fps; const offset = (t * speed) % 50; // percent const doubled = `${text} • ${text} • ${text}`; return (
{doubled} {doubled}
); }; /** Big countdown / counter (rolling digits). */ export const Countdown: React.FC<{ end?: number; from?: number; delay?: number }> = ({ end = 3, from = 5, delay = 0 }) => { const frame = useCurrentFrame(); const { fps } = useVideoConfig(); const t = (frame - delay) / fps; const val = Math.max(end, Math.round(from - t)); return (
{val}
SECONDS
); }; /** Node graph + data pulse traveling an edge (AI/workflow vibe). */ export const NodeFlow: React.FC<{ delay?: number }> = ({ delay = 0 }) => { const frame = useCurrentFrame(); const { fps } = useVideoConfig(); const t = (frame - delay) / fps; const pulse = (t % 1.4) / 1.4; const nodes = [ { x: 60, y: 160 }, { x: 200, y: 60 }, { x: 200, y: 260 }, { x: 350, y: 160 }, ]; const edges = [ [0, 1], [0, 2], [1, 3], [2, 3], ]; return ( {edges.map(([a, b], i) => { const x1 = nodes[a].x, y1 = nodes[a].y, x2 = nodes[b].x, y2 = nodes[b].y; // pulse position const px = x1 + (x2 - x1) * pulse; const py = y1 + (y2 - y1) * pulse; return ( ); })} {nodes.map((n, i) => ( ))} ); }; /** Pulse bars — audio-equalizer energy. */ export const PulseBars: React.FC<{ bars?: number; delay?: number }> = ({ bars = 12, delay = 0 }) => { const frame = useCurrentFrame(); const t = (frame - delay) / 60; return (
{Array.from({ length: bars }).map((_, i) => { const base = 0.25 + 0.3 * Math.abs(Math.sin(i * 1.7 + t * 4)); return (
); })}
); }; /** Animated checklist — boxes tick one by one. */ export const Checklist: React.FC<{ items?: string[]; delay?: number }> = ({ items = ["PLAN", "BUILD", "SHIP"], delay = 0 }) => { const frame = useCurrentFrame(); const { fps } = useVideoConfig(); return (
{items.map((it, i) => { const p = spring({ frame: Math.max(0, frame - delay - i * 14), fps, config: { damping: 16, stiffness: 100 } }); const done = p > 0.35; return (
{done && }
{it}
); })}
); }; /** HUD corner frame + crosshair center (tech overlay asset). */ export const HudFrame: React.FC<{ label?: string }> = ({ label = "SYSTEM" }) => { const frame = useCurrentFrame(); const t = frame / 60; const c = 36; const gap = 26; return ( {[ `M ${gap} ${gap} L ${gap + c} ${gap} L ${gap + c} ${gap + c / 2} M ${gap} ${gap} L ${gap} ${gap + c} L ${gap + c / 2} ${gap + c}`, `M ${700 - gap} ${gap} L ${700 - gap - c} ${gap} L ${700 - gap - c} ${gap + c / 2} M ${700 - gap} ${gap} L ${700 - gap} ${gap + c} L ${700 - gap - c / 2} ${gap + c}`, `M ${gap} ${440 - gap} L ${gap + c} ${440 - gap} L ${gap + c} ${440 - gap - c / 2} M ${gap} ${440 - gap} L ${gap} ${440 - gap - c} L ${gap + c / 2} ${440 - gap - c}`, `M ${700 - gap} ${440 - gap} L ${700 - gap - c} ${440 - gap} L ${700 - gap - c} ${440 - gap - c / 2} M ${700 - gap} ${440 - gap} L ${700 - gap} ${440 - gap - c} L ${700 - gap - c / 2} ${440 - gap - c}`, ].map((d, i) => ( ))} {/* scanning line */} {/* center crosshair */} {label} ); }; /** Percent stat block — big value + thin bar (editorial). */ export const StatBlock: React.FC<{ value: string; sub: string; delay?: number }> = ({ value, sub, delay = 0 }) => { const frame = useCurrentFrame(); const { fps } = useVideoConfig(); const pop = spring({ frame: Math.max(0, frame - delay), fps, config: { damping: 14, stiffness: 80 } }); return (
{value}
{sub}
); };