diff --git a/apps/videos/src/Root.tsx b/apps/videos/src/Root.tsx new file mode 100644 index 00000000..7f6fa4a9 --- /dev/null +++ b/apps/videos/src/Root.tsx @@ -0,0 +1,96 @@ +import "./style.css"; +import type React from "react"; +import { AbsoluteFill, Composition } from "remotion"; +import { ClipReveal } from "./components/ClipReveal"; +import { Counter } from "./components/Counter"; +import { GradientBlob } from "./components/GradientBlob"; +import { GrainOverlay } from "./components/GrainOverlay"; +import { PhotoPlaceholder } from "./components/PhotoPlaceholder"; +import { ToolPill } from "./components/ToolPill"; +import { BrandGradient } from "./compositions/ambient/BrandGradient"; +import { FloatingTools } from "./compositions/ambient/FloatingTools"; +import { FormatUniverse } from "./compositions/features/FormatUniverse"; +import { PipelineFlow } from "./compositions/features/PipelineFlow"; +import { TEXT } from "./lib/fonts"; + +const ComponentTest: React.FC = () => ( + +
+ + Hello SnapOtter + +
+
+ +
+
+ +
+ + + +
+); + +export const RemotionRoot: React.FC = () => ( + <> + + + + + + +); diff --git a/apps/videos/src/compositions/features/PipelineFlow.tsx b/apps/videos/src/compositions/features/PipelineFlow.tsx new file mode 100644 index 00000000..2e509ce7 --- /dev/null +++ b/apps/videos/src/compositions/features/PipelineFlow.tsx @@ -0,0 +1,701 @@ +import { Trail } from "@remotion/motion-blur"; +import { evolvePath } from "@remotion/paths"; +import type React from "react"; +import { + AbsoluteFill, + interpolate, + random, + Sequence, + spring, + useCurrentFrame, + useVideoConfig, +} from "remotion"; +import { ClipReveal } from "@/components/ClipReveal"; +import { Counter } from "@/components/Counter"; +import { GrainOverlay } from "@/components/GrainOverlay"; +import { PhotoPlaceholder } from "@/components/PhotoPlaceholder"; +import { COLOR } from "@/lib/colors"; +import { FONT, TEXT } from "@/lib/fonts"; +import { EASE, SPRING } from "@/lib/motion"; + +/* ------------------------------------------------------------------ */ +/* Constants */ +/* ------------------------------------------------------------------ */ + +const STATIONS = [ + { id: "resize", label: "Resize", color: "#3B82F6", x: 120 }, + { id: "compress", label: "Compress", color: "#10B981", x: 340 }, + { id: "watermark", label: "Watermark", color: "#EF4444", x: 560 }, +] as const; + +const FOLDER_X = 720; +const STATION_Y = 280; +const STATION_W = 100; +const STATION_H = 80; +const THUMB_START_X = -60; +const BATCH_COUNT = 50; + +/* ------------------------------------------------------------------ */ +/* SVG Station Icons (minimal 20x20) */ +/* ------------------------------------------------------------------ */ + +const ResizeIcon: React.FC = () => ( + + + +); + +const CompressIcon: React.FC = () => ( + + + + +); + +const WatermarkIcon: React.FC = () => ( + + + + +); + +const STATION_ICONS: Record = { + resize: ResizeIcon, + compress: CompressIcon, + watermark: WatermarkIcon, +}; + +/* ------------------------------------------------------------------ */ +/* Station component */ +/* ------------------------------------------------------------------ */ + +const Station: React.FC<{ + station: (typeof STATIONS)[number]; + index: number; + glowing: boolean; + pulseGlow: boolean; +}> = ({ station, index, glowing, pulseGlow }) => { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + + const scale = spring({ + frame, + fps, + delay: index * 8, + config: SPRING.snappy, + }); + + const Icon = STATION_ICONS[station.id]; + const glowOpacity = pulseGlow ? 0.3 + Math.sin(frame * 0.3) * 0.2 : glowing ? 0.5 : 0; + + return ( +
0 + ? `0 0 20px ${station.color}${Math.round(glowOpacity * 255) + .toString(16) + .padStart(2, "0")}` + : "none", + transform: `scale(${scale})`, + display: "flex", + flexDirection: "column", + alignItems: "center", + justifyContent: "center", + gap: 6, + }} + > + {Icon && } + + {station.label} + +
+ ); +}; + +/* ------------------------------------------------------------------ */ +/* Connector lines between stations */ +/* ------------------------------------------------------------------ */ + +const ConnectorLine: React.FC<{ + fromX: number; + toX: number; + delay: number; +}> = ({ fromX, toX, delay }) => { + const frame = useCurrentFrame(); + const progress = interpolate(frame, [delay, delay + 20], [0, 1], { + extrapolateLeft: "clamp", + extrapolateRight: "clamp", + easing: EASE.smooth, + }); + + const x1 = fromX + STATION_W / 2 + 4; + const x2 = toX - STATION_W / 2 - 4; + const pathD = `M ${x1} ${STATION_Y} L ${x2} ${STATION_Y}`; + const evolved = evolvePath(progress, pathD); + + return ( + + + + ); +}; + +/* ------------------------------------------------------------------ */ +/* Folder icon */ +/* ------------------------------------------------------------------ */ + +const FolderIcon: React.FC<{ pulse: boolean }> = ({ pulse }) => { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + const scale = spring({ frame, fps, delay: 24, config: SPRING.snappy }); + const pulseScale = pulse ? 1 + Math.sin(frame * 0.2) * 0.05 : 1; + + return ( +
+ + + + +
+ ); +}; + +/* ------------------------------------------------------------------ */ +/* Progress ring (SVG circle with stroke animation) */ +/* ------------------------------------------------------------------ */ + +const ProgressRing: React.FC<{ + progress: number; + color: string; + x: number; + y: number; +}> = ({ progress, color, x, y }) => { + const r = 14; + const circumference = 2 * Math.PI * r; + const offset = circumference * (1 - Math.min(progress, 1)); + + return ( + 0 ? 1 : 0, + }} + > + + + + ); +}; + +/* ------------------------------------------------------------------ */ +/* Checkmark */ +/* ------------------------------------------------------------------ */ + +const Checkmark: React.FC<{ visible: boolean }> = ({ visible }) => { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + + if (!visible) return null; + + const scale = spring({ + frame, + fps, + config: SPRING.popIn, + }); + + return ( +
+ + + + +
+ ); +}; + +/* ------------------------------------------------------------------ */ +/* Single thumbnail traversal (Act 2) */ +/* ------------------------------------------------------------------ */ + +const SingleFlow: React.FC = () => { + const frame = useCurrentFrame(); + + // Spec timing (global -> local, Sequence from=40): + // Frame 40-70 (local 0-30): Travel to Station 1 + resize effect + // Frame 70-100 (local 30-60): Travel to Station 2 + compress effect + // Frame 100-130(local 60-90): Travel to Station 3 + watermark effect + // Frame 130-155(local 90-115): Travel to folder + checkmark + + // Thumbnail X position: moves through each station to folder + // Input/output arrays must be same length (12 entries each) + const thumbX = interpolate( + frame, + [0, 18, 26, 30, 48, 56, 60, 78, 86, 90, 110, 115], + [ + THUMB_START_X, // 0: start off-screen + STATIONS[0].x, // 18: arrive at Resize + STATIONS[0].x, // 26: pause for resize effect + STATIONS[0].x, // 30: leave Resize + STATIONS[1].x, // 48: arrive at Compress + STATIONS[1].x, // 56: pause for compress effect + STATIONS[1].x, // 60: leave Compress + STATIONS[2].x, // 78: arrive at Watermark + STATIONS[2].x, // 86: pause for watermark effect + STATIONS[2].x, // 90: leave Watermark + FOLDER_X, // 110: arrive at folder + FOLDER_X, // 115: hold at folder + ], + { extrapolateLeft: "clamp", extrapolateRight: "clamp", easing: EASE.smooth }, + ); + + // Thumbnail size: shrinks at resize station (local 18-26) + const thumbSize = interpolate(frame, [18, 26], [48, 36], { + extrapolateLeft: "clamp", + extrapolateRight: "clamp", + easing: EASE.smooth, + }); + + // Resize label morph + const showResizeLabel = frame >= 16 && frame <= 34; + const resizeLabelProgress = interpolate(frame, [18, 26], [0, 1], { + extrapolateLeft: "clamp", + extrapolateRight: "clamp", + }); + + // Compress ring progress (local 48-60) + const compressRingProgress = interpolate(frame, [48, 60], [0, 1], { + extrapolateLeft: "clamp", + extrapolateRight: "clamp", + easing: EASE.smooth, + }); + + // Watermark stamp (local 78-86) + const stampOpacity = interpolate(frame, [78, 84], [0, 0.4], { + extrapolateLeft: "clamp", + extrapolateRight: "clamp", + }); + + // Slide behind folder (local 105-115) + const folderScale = interpolate(frame, [105, 115], [1, 0.8], { + extrapolateLeft: "clamp", + extrapolateRight: "clamp", + }); + const thumbOpacity = interpolate(frame, [108, 115], [1, 0], { + extrapolateLeft: "clamp", + extrapolateRight: "clamp", + }); + + // Station glow states (6 frames each centered on arrival) + const resizeGlow = frame >= 16 && frame < 30; + const compressGlow = frame >= 46 && frame < 62; + const watermarkGlow = frame >= 76 && frame < 90; + + // Checkmark appears at local frame 115 + const showCheck = frame >= 115; + + return ( + <> + {/* Thumbnail */} +
= 108 ? 0 : 5, + }} + > + + {/* Watermark stamp */} + {stampOpacity > 0 && ( + + SnapOtter + + )} +
+ + {/* Resize label */} + {showResizeLabel && ( + + {resizeLabelProgress < 0.5 ? "1200px" : "800px"} + + )} + + {/* Compress ring + file size */} + + {frame >= 46 && frame <= 68 && ( + + (n >= 1000 ? `${(n / 1000).toFixed(1)} MB` : `${n} KB`)} + /> + + )} + + {/* Station glows passed as props */} + {STATIONS.map((s, i) => ( + + ))} + + + + ); +}; + +/* ------------------------------------------------------------------ */ +/* Batch thumbnail (Act 3) */ +/* ------------------------------------------------------------------ */ + +const BatchThumb: React.FC<{ + index: number; +}> = ({ index }) => { + const frame = useCurrentFrame(); + const staggerDelay = index * 3; + const localFrame = frame - staggerDelay; + + if (localFrame < 0) return null; + + const hue = random(`thumb-hue-${index}`) * 360; + const size = 24 + random(`thumb-size-${index}`) * 8; + const yOffset = (random(`thumb-y-${index}`) - 0.5) * 30; + + // Fast traversal: cover pipeline in ~28 frames per thumb + const thumbX = interpolate(localFrame, [0, 28], [THUMB_START_X, FOLDER_X + 10], { + extrapolateLeft: "clamp", + extrapolateRight: "clamp", + easing: EASE.smooth, + }); + + const opacity = interpolate(localFrame, [0, 3, 25, 28], [0, 1, 1, 0], { + extrapolateLeft: "clamp", + extrapolateRight: "clamp", + }); + + if (localFrame > 32) return null; + + return ( +
+ +
+ ); +}; + +/* ------------------------------------------------------------------ */ +/* Batch stream wrapper with Trail */ +/* ------------------------------------------------------------------ */ + +const BatchStream: React.FC = () => { + const frame = useCurrentFrame(); + const thumbs = Array.from({ length: BATCH_COUNT }, (_, i) => i); + + // How many have arrived at folder + const arrivedCount = thumbs.filter((i) => { + const localFrame = frame - i * 3; + return localFrame >= 28; + }).length; + + return ( + <> + {/* Stations with continuous pulse glow during batch */} + {STATIONS.map((s, i) => ( + = 10} /> + ))} + + + + {thumbs.map((i) => ( + + ))} + + + + {/* Batch counter above folder */} + {arrivedCount > 0 && ( +
+ +
+ )} + + ); +}; + +/* ------------------------------------------------------------------ */ +/* Main composition */ +/* ------------------------------------------------------------------ */ + +export const PipelineFlow: React.FC = () => { + const frame = useCurrentFrame(); + + return ( + + {/* Act 1: Stations Appear (frame 0-40) */} + + {/* Connector lines (drawn during Act 1) */} + + + + + {/* Folder (appears last in Act 1) */} + = 340 && frame < 380} /> + + + {/* Act 1 stations (visible throughout until Act 2 takes over) */} + {frame < 40 && ( + <> + {STATIONS.map((s, i) => ( + + ))} + + )} + + {/* Act 2: Single Image Flow (frame 40-170) */} + + + + + {/* "Chain any tools." text (frame 155-350) */} + {frame >= 155 && frame < 350 && ( +
+ + Chain any tools. + +
+ )} + + {/* Act 3: Batch Mode (frame 170-340) */} + + + + + {/* Act 3 stations when not covered by SingleFlow or BatchStream */} + {frame >= 170 && frame < 180 && ( + <> + {STATIONS.map((s, i) => ( + + ))} + + )} + + {/* Act 4: Result (frame 340-390) */} + {frame >= 340 && ( + <> + {STATIONS.map((s, i) => ( + + ))} + + {/* Final counter at 50 */} +
+ + 50 + +
+ + )} + + {/* "Batch everything." text (frame 360-390) */} + +
+ + Batch everything. + +
+
+ + +
+ ); +};