import type React from "react"; import { AbsoluteFill, Img, interpolate, interpolateColors, spring, staticFile, useCurrentFrame, useVideoConfig, } from "remotion"; import { ClipReveal } from "@/components/ClipReveal"; import { GrainOverlay } from "@/components/GrainOverlay"; import { TerminalWindow } from "@/components/TerminalWindow"; import { TypeWriter } from "@/components/TypeWriter"; import { FONT, TEXT } from "@/lib/fonts"; import { EASE, SPRING } from "@/lib/motion"; /* ------------------------------------------------------------------ */ /* Docker command segments with syntax highlighting */ /* ------------------------------------------------------------------ */ const DOCKER_SEGMENTS = [ { text: "docker", color: "#ff7b72" }, { text: " run ", color: "#7ee787" }, { text: "-d ", color: "#79c0ff" }, { text: "--name ", color: "#79c0ff" }, { text: "snapotter ", color: "#a5d6ff" }, { text: "-p ", color: "#79c0ff" }, { text: "1349:1349 ", color: "#a5d6ff" }, { text: "snapotter/snapotter", color: "#7ee787" }, ]; /* ------------------------------------------------------------------ */ /* Spinner characters */ /* ------------------------------------------------------------------ */ const SPINNER_CHARS = ["|", "/", "-", "\\"]; /* ------------------------------------------------------------------ */ /* Address bar component for the morph */ /* ------------------------------------------------------------------ */ const AddressBar: React.FC<{ opacity: number }> = ({ opacity }) => (
{/* Green lock icon */} localhost:1349
); /* ------------------------------------------------------------------ */ /* Main composition */ /* ------------------------------------------------------------------ */ export const OneCommand: React.FC = () => { const frame = useCurrentFrame(); const { fps, width, height } = useVideoConfig(); const cx = width / 2; const cy = height / 2; /* ================================================================ */ /* Act 1: Terminal slides up (frame 0-30) */ /* ================================================================ */ const terminalEntrySpring = spring({ frame, fps, config: SPRING.snappy, }); const terminalSlideY = interpolate(terminalEntrySpring, [0, 1], [600, 0]); /* ================================================================ */ /* Act 2: Typing (frame 30-180) */ /* ================================================================ */ /* Prompt "$ " visible once terminal is in */ const promptOpacity = interpolate(frame, [28, 30], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp", }); /* Cursor blink at the prompt before typing starts (frame 30-35) */ const preTypingCursorVisible = frame >= 30 && frame < 35 && Math.floor(frame / 4) % 2 === 0; /* ================================================================ */ /* Act 3: Processing lines (frame 180-215) */ /* ================================================================ */ /* "Pulling..." text appears */ const pullingOpacity = interpolate(frame, [180, 185], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp", }); /* Spinner cycles every 3 frames */ const spinnerChar = frame >= 185 && frame < 210 ? SPINNER_CHARS[Math.floor((frame - 185) / 3) % SPINNER_CHARS.length] : null; /* Checkmark line */ const checkmarkSpring = spring({ frame: frame - 210, fps, config: SPRING.popIn, }); const showCheckmark = frame >= 210; /* ================================================================ */ /* Act 4: Terminal-to-App Morph (frame 215-280) */ /* ================================================================ */ /* Body background color morph */ const bodyBg = frame >= 215 ? interpolateColors(frame, [215, 250], ["#0d1117", "#ffffff"]) : "#0d1117"; /* Terminal text fade out */ const textFadeOut = interpolate(frame, [215, 235], [1, 0], { extrapolateLeft: "clamp", extrapolateRight: "clamp", }); /* Window size morph */ const windowWidth = interpolate(frame, [215, 260], [900, 1100], { extrapolateLeft: "clamp", extrapolateRight: "clamp", easing: EASE.smooth, }); const windowHeight = interpolate(frame, [215, 260], [500, 650], { extrapolateLeft: "clamp", extrapolateRight: "clamp", easing: EASE.smooth, }); /* Dots fade out */ const dotsOpacity = interpolate(frame, [220, 240], [1, 0], { extrapolateLeft: "clamp", extrapolateRight: "clamp", }); /* Address bar fade in */ const addressBarOpacity = interpolate(frame, [240, 255], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp", }); /* Top bar color morph */ const topBarColor = frame >= 215 ? interpolateColors(frame, [215, 250], ["#1e1e2e", "#2d2d3f"]) : "#1e1e2e"; /* App mockup appears at frame 260 */ const showAppMockup = frame >= 258; const appMockupOpacity = interpolate(frame, [258, 270], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp", }); /* ================================================================ */ /* Act 5: Tagline + float (frame 280-390) */ /* ================================================================ */ /* Amber glow behind browser */ const glowOpacity = interpolate(frame, [320, 340], [0, 0.12], { extrapolateLeft: "clamp", extrapolateRight: "clamp", }); /* Float animation (subtle oscillation) */ const floatY = frame >= 350 ? Math.sin((frame - 350) * 0.15) * 3 : 0; return ( {/* Amber glow behind browser (Act 5) */} {frame >= 320 && (
)} {/* Terminal / Browser window */}
= 240 ? : undefined} > {/* Terminal text content (fades out during morph) */}
{/* Prompt line */}
$ {frame >= 35 ? ( ) : ( preTypingCursorVisible && ( ) )}
{/* Processing output (Act 3) */} {frame >= 180 && (
Pulling snapotter/snapotter:latest...
{/* Spinner line */} {spinnerChar && !showCheckmark && (
{spinnerChar}
)} {/* Checkmark line */} {showCheckmark && (
✓ Container started on :1349
)}
)}
{/* Real app screenshot (appears during morph) */} {showAppMockup && (
)}
{/* Tagline (Act 5) */} {frame >= 310 && (
One command. That's it.
)} ); };