diff --git a/apps/videos/src/compositions/product-demo/ProductDemo.tsx b/apps/videos/src/compositions/product-demo/ProductDemo.tsx new file mode 100644 index 00000000..74d8e67d --- /dev/null +++ b/apps/videos/src/compositions/product-demo/ProductDemo.tsx @@ -0,0 +1,46 @@ +import type React from "react"; +import { AbsoluteFill, Series } from "remotion"; +import { GrainOverlay } from "@/components/GrainOverlay"; +import { AiToolsScene } from "./scenes/AiToolsScene"; +import { ApiDocsScene } from "./scenes/ApiDocsScene"; +import { BatchProcessingScene } from "./scenes/BatchProcessingScene"; +import { DashboardScene } from "./scenes/DashboardScene"; +import { EndCardScene } from "./scenes/EndCardScene"; +import { ImageEditorScene } from "./scenes/ImageEditorScene"; +import { PipelineBuilderScene } from "./scenes/PipelineBuilderScene"; +import { SingleToolScene } from "./scenes/SingleToolScene"; + +export const ProductDemo: React.FC = () => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/apps/videos/src/compositions/product-demo/scenes/AiToolsScene.tsx b/apps/videos/src/compositions/product-demo/scenes/AiToolsScene.tsx new file mode 100644 index 00000000..ea48a6b0 --- /dev/null +++ b/apps/videos/src/compositions/product-demo/scenes/AiToolsScene.tsx @@ -0,0 +1,206 @@ +import type React from "react"; +import { AbsoluteFill, Sequence, useCurrentFrame } from "remotion"; +import { AppWindow } from "@/components/AppWindow"; +import { BeforeAfter } from "@/components/BeforeAfter"; +import { ClipReveal } from "@/components/ClipReveal"; +import { PhotoPlaceholder } from "@/components/PhotoPlaceholder"; +import { COLOR } from "@/lib/colors"; +import { FONT, TEXT } from "@/lib/fonts"; + +const Checkerboard: React.FC<{ w: number; h: number }> = ({ w, h }) => ( +
+); + +export const AiToolsScene: React.FC = () => { + const frame = useCurrentFrame(); + + return ( + + {/* Background removal demo (0-180) */} + + + +
+ + +
+
+ + Running rembg model locally... + +
+ + + + + + } + after={ +
+ +
+
+ } + scanStartFrame={10} + scanDuration={50} + width={450} + height={340} + /> + + +
+ +
+
+ + {/* Quick AI tool montage (180-360) */} + + + +
+ + + 4x + + +
+
+
+
+ + + + +
+
+ +
+
+
+ Extracted Text +
+ {["Lorem ipsum dolor sit", "amet consectetur", "adipiscing elit sed do"].map( + (line, i) => ( +
+ {line} +
+ ), + )} +
+
+
+
+
+ + {/* Bottom text */} +
+ + + All on your hardware. + + +
+ + ); +}; diff --git a/apps/videos/src/compositions/product-demo/scenes/ApiDocsScene.tsx b/apps/videos/src/compositions/product-demo/scenes/ApiDocsScene.tsx new file mode 100644 index 00000000..d2841fcf --- /dev/null +++ b/apps/videos/src/compositions/product-demo/scenes/ApiDocsScene.tsx @@ -0,0 +1,84 @@ +import type React from "react"; +import { AbsoluteFill } from "remotion"; +import { AppWindow } from "@/components/AppWindow"; +import { ClipReveal } from "@/components/ClipReveal"; +import { TypeWriter } from "@/components/TypeWriter"; +import { FONT, TEXT } from "@/lib/fonts"; + +const CURL_SEGMENTS = [ + { text: "curl ", color: "#e6edf3" }, + { text: "-X POST ", color: "#79c0ff" }, + { text: "localhost:1349/api/v1/tools/resize ", color: "#a5d6ff" }, + { text: '-F "file=@photo.jpg" ', color: "#7ee787" }, + { text: "-F ", color: "#79c0ff" }, + { text: '"settings={\\"width\\":800}"', color: "#ffa657" }, +]; + +export const ApiDocsScene: React.FC = () => { + return ( + + +
+
+ Example Request +
+
+ +
+ + +
+
+                {`{
+  "downloadUrl": "/api/v1/download/abc123",
+  "originalSize": 2400000,
+  "processedSize": 340000
+}`}
+              
+
+
+
+
+ +
+ + + Every tool via REST API. + + +
+
+ ); +}; diff --git a/apps/videos/src/compositions/product-demo/scenes/BatchProcessingScene.tsx b/apps/videos/src/compositions/product-demo/scenes/BatchProcessingScene.tsx new file mode 100644 index 00000000..e00402c8 --- /dev/null +++ b/apps/videos/src/compositions/product-demo/scenes/BatchProcessingScene.tsx @@ -0,0 +1,127 @@ +import type React from "react"; +import { AbsoluteFill, spring, useCurrentFrame, useVideoConfig } from "remotion"; +import { AppWindow } from "@/components/AppWindow"; +import { ClipReveal } from "@/components/ClipReveal"; +import { ProgressBar } from "@/components/ProgressBar"; +import { FONT, TEXT } from "@/lib/fonts"; +import { SPRING } from "@/lib/motion"; + +const FILES = [ + { name: "photo_001.jpg", from: "1.8 MB", to: "95 KB" }, + { name: "photo_002.jpg", from: "3.2 MB", to: "210 KB" }, + { name: "landscape.png", from: "4.1 MB", to: "280 KB" }, + { name: "portrait.jpg", from: "2.7 MB", to: "160 KB" }, + { name: "macro_01.jpg", from: "5.0 MB", to: "320 KB" }, + { name: "event_23.jpg", from: "1.5 MB", to: "85 KB" }, + { name: "scan_hires.png", from: "8.2 MB", to: "450 KB" }, + { name: "product_a.jpg", from: "2.0 MB", to: "120 KB" }, +]; + +export const BatchProcessingScene: React.FC = () => { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + + const processStartFrame = 100; + const filesPerSecond = 3; + const framesPerFile = Math.floor(30 / filesPerSecond); + + return ( + + +
+
+ {FILES.map((file, i) => { + const enterDelay = 20 + i * 4; + const enterSpring = spring({ frame: frame - enterDelay, fps, config: SPRING.popIn }); + const fileProcessFrame = processStartFrame + i * framesPerFile; + const isDone = frame > fileProcessFrame + framesPerFile; + const isProcessing = frame >= fileProcessFrame && !isDone; + + return ( +
+
+ {file.name} +
+ + {isProcessing && ( + + )} + + {isDone && ( +
+ + + {file.from} + + + {"→"} + + + {file.to} + +
+ )} +
+ ); + })} +
+ + {frame > 280 && ( +
+ + + Unlimited batch. No caps. + + +
+ )} +
+
+
+ ); +}; diff --git a/apps/videos/src/compositions/product-demo/scenes/DashboardScene.tsx b/apps/videos/src/compositions/product-demo/scenes/DashboardScene.tsx new file mode 100644 index 00000000..eb9eef47 --- /dev/null +++ b/apps/videos/src/compositions/product-demo/scenes/DashboardScene.tsx @@ -0,0 +1,149 @@ +import type React from "react"; +import { + AbsoluteFill, + Img, + interpolate, + spring, + staticFile, + useCurrentFrame, + useVideoConfig, +} from "remotion"; +import { AppWindow } from "@/components/AppWindow"; +import { CATEGORY_LABELS, CATEGORY_ORDER, COLOR } from "@/lib/colors"; +import { FONT } from "@/lib/fonts"; +import { SPRING } from "@/lib/motion"; +import { TOOLS } from "@/lib/tools"; + +const VISIBLE_TOOLS = TOOLS.slice(0, 12); + +export const DashboardScene: React.FC = () => { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + + const windowScale = spring({ frame, fps, config: SPRING.snappy }); + const searchText = "resize"; + const typingStart = 60; + const charsVisible = Math.floor(Math.max(0, frame - typingStart) / 3); + const typed = searchText.slice(0, charsVisible); + + return ( + +
+ +
+ {/* Top bar */} +
+ + + SnapOtter + +
+ {typed || "Search tools..."} + {frame >= typingStart && + charsVisible < searchText.length && + Math.floor(frame / 8) % 2 === 0 && ( + + )} +
+
+ + {/* Category pills */} +
+ {CATEGORY_ORDER.map((cat) => ( +
+ {CATEGORY_LABELS[cat]} +
+ ))} +
+ + {/* Tool grid */} +
+ {VISIBLE_TOOLS.map((tool) => { + const matches = !typed || tool.name.toLowerCase().includes(typed.toLowerCase()); + const cardOpacity = typed ? (matches ? 1 : 0.2) : 1; + const cardScale = typed ? (matches ? 1 : 0.95) : 1; + return ( +
+
+
+ {tool.name} +
+
+ ); + })} +
+
+ +
+ + ); +}; diff --git a/apps/videos/src/compositions/product-demo/scenes/EndCardScene.tsx b/apps/videos/src/compositions/product-demo/scenes/EndCardScene.tsx new file mode 100644 index 00000000..f6ab9e53 --- /dev/null +++ b/apps/videos/src/compositions/product-demo/scenes/EndCardScene.tsx @@ -0,0 +1,62 @@ +import type React from "react"; +import { + AbsoluteFill, + Img, + interpolate, + spring, + staticFile, + useCurrentFrame, + useVideoConfig, +} from "remotion"; +import { ClipReveal } from "@/components/ClipReveal"; +import { COLOR } from "@/lib/colors"; +import { FONT, TEXT } from "@/lib/fonts"; +import { SPRING } from "@/lib/motion"; + +export const EndCardScene: React.FC = () => { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + + const logoSpring = spring({ frame, fps, config: SPRING.natural }); + const fadeOut = interpolate(frame, [70, 90], [1, 0], { + extrapolateLeft: "clamp", + extrapolateRight: "clamp", + }); + + return ( + + + +
+ + + + Self-hosted image processing. + + + +
+ + + + snapotter.com + + + + ); +}; diff --git a/apps/videos/src/compositions/product-demo/scenes/ImageEditorScene.tsx b/apps/videos/src/compositions/product-demo/scenes/ImageEditorScene.tsx new file mode 100644 index 00000000..a3815765 --- /dev/null +++ b/apps/videos/src/compositions/product-demo/scenes/ImageEditorScene.tsx @@ -0,0 +1,188 @@ +import type React from "react"; +import { AbsoluteFill, interpolate, spring, useCurrentFrame, useVideoConfig } from "remotion"; +import { AppWindow } from "@/components/AppWindow"; +import { PhotoPlaceholder } from "@/components/PhotoPlaceholder"; +import { COLOR } from "@/lib/colors"; +import { FONT } from "@/lib/fonts"; +import { SPRING } from "@/lib/motion"; + +const TOOLBAR_ICONS = [ + "Move", + "Select", + "Brush", + "Eraser", + "Clone", + "Dodge", + "Text", + "Shape", + "Line", + "Gradient", + "Fill", + "Crop", + "Eyedropper", + "Zoom", + "Hand", +]; + +export const ImageEditorScene: React.FC = () => { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + + const enterSpring = spring({ frame, fps, config: SPRING.snappy }); + const brushFrame = 60; + const brushProgress = interpolate(frame, [brushFrame, brushFrame + 60], [0, 1], { + extrapolateLeft: "clamp", + extrapolateRight: "clamp", + }); + + const activeToolIdx = frame < 60 ? 2 : frame < 120 ? 6 : frame < 200 ? 11 : 2; + + return ( + +
+ +
+ {/* Toolbar */} +
+ {TOOLBAR_ICONS.map((icon, i) => ( +
+ {icon.slice(0, 2)} +
+ ))} +
+ + {/* Canvas */} +
+ + + {/* Brush stroke */} + {brushProgress > 0 && ( + + + + )} + + {/* Text overlay */} + {frame > 130 && ( +
+ SnapOtter +
+ )} +
+ + {/* Layers panel */} +
+
+ Layers +
+ {["Text Layer", "Brush Stroke", "Background"].map((layer, i) => ( +
+ {layer} +
+ ))} +
+
+
+
+
+ ); +}; diff --git a/apps/videos/src/compositions/product-demo/scenes/PipelineBuilderScene.tsx b/apps/videos/src/compositions/product-demo/scenes/PipelineBuilderScene.tsx new file mode 100644 index 00000000..fd000d94 --- /dev/null +++ b/apps/videos/src/compositions/product-demo/scenes/PipelineBuilderScene.tsx @@ -0,0 +1,121 @@ +import type React from "react"; +import { AbsoluteFill, interpolate, spring, useCurrentFrame, useVideoConfig } from "remotion"; +import { AppWindow } from "@/components/AppWindow"; +import { COLOR } from "@/lib/colors"; +import { FONT } from "@/lib/fonts"; +import { EASE, SPRING } from "@/lib/motion"; + +const BLOCKS = [ + { name: "Resize", color: COLOR.category.essentials, enterFrame: 40, x: 250 }, + { name: "Compress", color: COLOR.category.optimization, enterFrame: 80, x: 580 }, + { name: "Watermark", color: COLOR.category.watermark, enterFrame: 120, x: 910 }, +]; + +export const PipelineBuilderScene: React.FC = () => { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + + return ( + + +
+ {/* Blocks */} + {BLOCKS.map((block) => { + const s = spring({ frame: frame - block.enterFrame, fps, config: SPRING.popIn }); + return ( +
+ {block.name} +
+ ); + })} + + {/* Connection lines */} + {frame > 140 && + [0, 1].map((ci) => { + const lineProgress = interpolate(frame, [140 + ci * 20, 170 + ci * 20], [0, 1], { + extrapolateLeft: "clamp", + extrapolateRight: "clamp", + easing: EASE.enter, + }); + const x1 = BLOCKS[ci].x + 160; + const x2 = BLOCKS[ci + 1].x; + return ( + + + + ); + })} + + {/* Running indicator */} + {frame > 220 && ( +
+ ✓ Pipeline completed -- 5 images processed +
+ )} +
+
+
+ ); +}; diff --git a/apps/videos/src/compositions/product-demo/scenes/SingleToolScene.tsx b/apps/videos/src/compositions/product-demo/scenes/SingleToolScene.tsx new file mode 100644 index 00000000..e44cfe32 --- /dev/null +++ b/apps/videos/src/compositions/product-demo/scenes/SingleToolScene.tsx @@ -0,0 +1,195 @@ +import type React from "react"; +import { AbsoluteFill, interpolate, spring, useCurrentFrame, useVideoConfig } from "remotion"; +import { AppWindow } from "@/components/AppWindow"; +import { PhotoPlaceholder } from "@/components/PhotoPlaceholder"; +import { ProgressBar } from "@/components/ProgressBar"; +import { COLOR } from "@/lib/colors"; +import { FONT } from "@/lib/fonts"; +import { SPRING } from "@/lib/motion"; + +export const SingleToolScene: React.FC = () => { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + + const slideIn = spring({ frame, fps, config: SPRING.snappy }); + const dropFrame = 40; + const dropProgress = spring({ frame: frame - dropFrame, fps, config: SPRING.natural }); + const processFrame = 180; + const showResult = frame > processFrame + 60; + + return ( + +
+ +
+ {/* Dropzone */} +
+ {frame < dropFrame ? ( +
+ Drop an image here +
+ ) : ( +
+ +
+ )} +
+ + {/* Settings */} +
+
+ Settings +
+ + {/* Width field */} + +
+ 1920 +
+ + {/* Height field */} + +
+ auto +
+ + {/* Process button / results */} + {frame >= processFrame && !showResult && ( + + )} + + {showResult && ( +
+
+ 2.4 MB +
+
+ 340 KB +
+
+ -86% +
+
+ )} +
+
+
+
+
+ ); +};