From 72b0077a57e2f3bd8ffc930b60035ac012f1087a Mon Sep 17 00:00:00 2001 From: SnapOtter Date: Fri, 8 May 2026 16:38:28 +0800 Subject: [PATCH] feat(videos): add OneCommand hero composition with terminal-to-app morph --- apps/videos/src/Root.tsx | 9 + .../src/compositions/hero/OneCommand.tsx | 344 ++++++++++++++++++ 2 files changed, 353 insertions(+) create mode 100644 apps/videos/src/compositions/hero/OneCommand.tsx diff --git a/apps/videos/src/Root.tsx b/apps/videos/src/Root.tsx index 707be692..0a6f4a51 100644 --- a/apps/videos/src/Root.tsx +++ b/apps/videos/src/Root.tsx @@ -12,6 +12,7 @@ import { FloatingTools } from "./compositions/ambient/FloatingTools"; import { AiMagicReel } from "./compositions/features/AiMagicReel"; import { FormatUniverse } from "./compositions/features/FormatUniverse"; import { PipelineFlow } from "./compositions/features/PipelineFlow"; +import { OneCommand } from "./compositions/hero/OneCommand"; import { PrivacyPromise } from "./compositions/hero/PrivacyPromise"; import { TEXT } from "./lib/fonts"; @@ -110,5 +111,13 @@ export const RemotionRoot: React.FC = () => ( width={1920} height={1080} /> + ); diff --git a/apps/videos/src/compositions/hero/OneCommand.tsx b/apps/videos/src/compositions/hero/OneCommand.tsx new file mode 100644 index 00000000..ef818200 --- /dev/null +++ b/apps/videos/src/compositions/hero/OneCommand.tsx @@ -0,0 +1,344 @@ +import type React from "react"; +import { + AbsoluteFill, + interpolate, + interpolateColors, + spring, + useCurrentFrame, + useVideoConfig, +} from "remotion"; +import { AppMockup } from "@/components/AppMockup"; +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.05], { + 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 +
+ )} +
+ )} +
+ + {/* App mockup (appears during morph) */} + {showAppMockup && ( +
+ +
+ )} +
+
+ + {/* Tagline (Act 5) */} + {frame >= 310 && ( +
+ + + One command. That's it. + + +
+ )} + + + + ); +};