diff --git a/apps/landing/public/videos/ai-magic-reel-poster.webp b/apps/landing/public/videos/ai-magic-reel-poster.webp deleted file mode 100644 index 4f661dea..00000000 Binary files a/apps/landing/public/videos/ai-magic-reel-poster.webp and /dev/null differ diff --git a/apps/landing/public/videos/ai-magic-reel.mp4 b/apps/landing/public/videos/ai-magic-reel.mp4 deleted file mode 100644 index 840c1e66..00000000 Binary files a/apps/landing/public/videos/ai-magic-reel.mp4 and /dev/null differ diff --git a/apps/landing/public/videos/ai-magic-reel.webm b/apps/landing/public/videos/ai-magic-reel.webm deleted file mode 100644 index 157212ba..00000000 Binary files a/apps/landing/public/videos/ai-magic-reel.webm and /dev/null differ diff --git a/apps/landing/public/videos/brand-gradient-poster.webp b/apps/landing/public/videos/brand-gradient-poster.webp deleted file mode 100644 index 181974b1..00000000 Binary files a/apps/landing/public/videos/brand-gradient-poster.webp and /dev/null differ diff --git a/apps/landing/public/videos/brand-gradient.mp4 b/apps/landing/public/videos/brand-gradient.mp4 deleted file mode 100644 index d5c13749..00000000 Binary files a/apps/landing/public/videos/brand-gradient.mp4 and /dev/null differ diff --git a/apps/landing/public/videos/brand-gradient.webm b/apps/landing/public/videos/brand-gradient.webm deleted file mode 100644 index 18b617aa..00000000 Binary files a/apps/landing/public/videos/brand-gradient.webm and /dev/null differ diff --git a/apps/landing/public/videos/cloud-vs-local-poster.webp b/apps/landing/public/videos/cloud-vs-local-poster.webp deleted file mode 100644 index 0299b1c6..00000000 Binary files a/apps/landing/public/videos/cloud-vs-local-poster.webp and /dev/null differ diff --git a/apps/landing/public/videos/cloud-vs-local.mp4 b/apps/landing/public/videos/cloud-vs-local.mp4 deleted file mode 100644 index d29e39fe..00000000 Binary files a/apps/landing/public/videos/cloud-vs-local.mp4 and /dev/null differ diff --git a/apps/landing/public/videos/cloud-vs-local.webm b/apps/landing/public/videos/cloud-vs-local.webm deleted file mode 100644 index a9db4664..00000000 Binary files a/apps/landing/public/videos/cloud-vs-local.webm and /dev/null differ diff --git a/apps/landing/public/videos/floating-tools-poster.webp b/apps/landing/public/videos/floating-tools-poster.webp deleted file mode 100644 index f224ca8f..00000000 Binary files a/apps/landing/public/videos/floating-tools-poster.webp and /dev/null differ diff --git a/apps/landing/public/videos/floating-tools.mp4 b/apps/landing/public/videos/floating-tools.mp4 deleted file mode 100644 index cb497d3e..00000000 Binary files a/apps/landing/public/videos/floating-tools.mp4 and /dev/null differ diff --git a/apps/landing/public/videos/floating-tools.webm b/apps/landing/public/videos/floating-tools.webm deleted file mode 100644 index 70da3ea5..00000000 Binary files a/apps/landing/public/videos/floating-tools.webm and /dev/null differ diff --git a/apps/landing/public/videos/format-universe-poster.webp b/apps/landing/public/videos/format-universe-poster.webp deleted file mode 100644 index 1cc22f5a..00000000 Binary files a/apps/landing/public/videos/format-universe-poster.webp and /dev/null differ diff --git a/apps/landing/public/videos/format-universe.mp4 b/apps/landing/public/videos/format-universe.mp4 deleted file mode 100644 index 82a83c67..00000000 Binary files a/apps/landing/public/videos/format-universe.mp4 and /dev/null differ diff --git a/apps/landing/public/videos/format-universe.webm b/apps/landing/public/videos/format-universe.webm deleted file mode 100644 index 0a0a65e8..00000000 Binary files a/apps/landing/public/videos/format-universe.webm and /dev/null differ diff --git a/apps/landing/public/videos/one-command-poster.webp b/apps/landing/public/videos/one-command-poster.webp deleted file mode 100644 index 9971683f..00000000 Binary files a/apps/landing/public/videos/one-command-poster.webp and /dev/null differ diff --git a/apps/landing/public/videos/one-command.mp4 b/apps/landing/public/videos/one-command.mp4 deleted file mode 100644 index 14aa363f..00000000 Binary files a/apps/landing/public/videos/one-command.mp4 and /dev/null differ diff --git a/apps/landing/public/videos/one-command.webm b/apps/landing/public/videos/one-command.webm deleted file mode 100644 index 0cbbc9f6..00000000 Binary files a/apps/landing/public/videos/one-command.webm and /dev/null differ diff --git a/apps/landing/public/videos/pipeline-flow-poster.webp b/apps/landing/public/videos/pipeline-flow-poster.webp deleted file mode 100644 index f686df07..00000000 Binary files a/apps/landing/public/videos/pipeline-flow-poster.webp and /dev/null differ diff --git a/apps/landing/public/videos/pipeline-flow.mp4 b/apps/landing/public/videos/pipeline-flow.mp4 deleted file mode 100644 index 7c712662..00000000 Binary files a/apps/landing/public/videos/pipeline-flow.mp4 and /dev/null differ diff --git a/apps/landing/public/videos/pipeline-flow.webm b/apps/landing/public/videos/pipeline-flow.webm deleted file mode 100644 index e271fb61..00000000 Binary files a/apps/landing/public/videos/pipeline-flow.webm and /dev/null differ diff --git a/apps/landing/public/videos/privacy-promise-poster.webp b/apps/landing/public/videos/privacy-promise-poster.webp deleted file mode 100644 index b93f930b..00000000 Binary files a/apps/landing/public/videos/privacy-promise-poster.webp and /dev/null differ diff --git a/apps/landing/public/videos/privacy-promise.mp4 b/apps/landing/public/videos/privacy-promise.mp4 deleted file mode 100644 index 58cb4d3c..00000000 Binary files a/apps/landing/public/videos/privacy-promise.mp4 and /dev/null differ diff --git a/apps/landing/public/videos/privacy-promise.webm b/apps/landing/public/videos/privacy-promise.webm deleted file mode 100644 index 678a14f1..00000000 Binary files a/apps/landing/public/videos/privacy-promise.webm and /dev/null differ diff --git a/apps/landing/public/videos/the-48-poster.webp b/apps/landing/public/videos/the-48-poster.webp deleted file mode 100644 index 10c124b1..00000000 Binary files a/apps/landing/public/videos/the-48-poster.webp and /dev/null differ diff --git a/apps/landing/public/videos/the-48.mp4 b/apps/landing/public/videos/the-48.mp4 deleted file mode 100644 index a8cab4b0..00000000 Binary files a/apps/landing/public/videos/the-48.mp4 and /dev/null differ diff --git a/apps/landing/public/videos/the-48.webm b/apps/landing/public/videos/the-48.webm deleted file mode 100644 index a9756684..00000000 Binary files a/apps/landing/public/videos/the-48.webm and /dev/null differ diff --git a/apps/landing/public/videos/tool-galaxy-poster.webp b/apps/landing/public/videos/tool-galaxy-poster.webp deleted file mode 100644 index 21712204..00000000 Binary files a/apps/landing/public/videos/tool-galaxy-poster.webp and /dev/null differ diff --git a/apps/landing/public/videos/tool-galaxy.mp4 b/apps/landing/public/videos/tool-galaxy.mp4 deleted file mode 100644 index 840de5c9..00000000 Binary files a/apps/landing/public/videos/tool-galaxy.mp4 and /dev/null differ diff --git a/apps/landing/public/videos/tool-galaxy.webm b/apps/landing/public/videos/tool-galaxy.webm deleted file mode 100644 index cbb66221..00000000 Binary files a/apps/landing/public/videos/tool-galaxy.webm and /dev/null differ diff --git a/apps/videos/package.json b/apps/videos/package.json deleted file mode 100644 index a575af36..00000000 --- a/apps/videos/package.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "name": "@snapotter/videos", - "version": "1.0.0", - "private": true, - "type": "module", - "scripts": { - "dev": "npx remotion studio", - "render": "node scripts/render-all.mjs", - "render:single": "npx remotion render" - }, - "dependencies": { - "@remotion/bundler": "^4.0.0", - "@remotion/cli": "^4.0.0", - "@remotion/google-fonts": "^4.0.0", - "@remotion/media": "^4.0.459", - "@remotion/motion-blur": "^4.0.0", - "@remotion/noise": "^4.0.0", - "@remotion/paths": "^4.0.0", - "@remotion/renderer": "^4.0.0", - "@remotion/shapes": "^4.0.0", - "@remotion/tailwind": "^4.0.0", - "react": "^18.3.1", - "react-dom": "^18.3.1", - "remotion": "^4.0.0" - }, - "devDependencies": { - "@types/react": "^18.3.0", - "@types/react-dom": "^18.3.0", - "tailwindcss": "^3.4.0", - "typescript": "^5.7.0" - } -} diff --git a/apps/videos/remotion.config.ts b/apps/videos/remotion.config.ts deleted file mode 100644 index f14d9396..00000000 --- a/apps/videos/remotion.config.ts +++ /dev/null @@ -1,19 +0,0 @@ -import path from "node:path"; -import { Config } from "@remotion/cli/config"; -import { enableTailwind } from "@remotion/tailwind"; - -Config.setVideoImageFormat("jpeg"); -Config.setOverwriteOutput(true); -Config.overrideWebpackConfig((config) => { - const withTailwind = enableTailwind(config); - return { - ...withTailwind, - resolve: { - ...withTailwind.resolve, - alias: { - ...withTailwind.resolve?.alias, - "@": path.resolve("./src"), - }, - }, - }; -}); diff --git a/apps/videos/scripts/render-all.mjs b/apps/videos/scripts/render-all.mjs deleted file mode 100644 index 3f69025f..00000000 --- a/apps/videos/scripts/render-all.mjs +++ /dev/null @@ -1,105 +0,0 @@ -import { bundle } from "@remotion/bundler"; -import { renderMedia, renderStill, selectComposition } from "@remotion/renderer"; -import { enableTailwind } from "@remotion/tailwind"; -import fs from "node:fs"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; - -const __dirname = path.dirname(fileURLToPath(import.meta.url)); - -const COMPOSITIONS = [ - { id: "PrivacyPromise", slug: "privacy-promise", posterFrame: 140 }, - { id: "ToolGalaxy", slug: "tool-galaxy", posterFrame: 450 }, - { id: "OneCommand", slug: "one-command", posterFrame: 300 }, - { id: "AiMagicReel", slug: "ai-magic-reel", posterFrame: 190 }, - { id: "FormatUniverse", slug: "format-universe", posterFrame: 90 }, - { id: "PipelineFlow", slug: "pipeline-flow", posterFrame: 260 }, - { id: "FloatingTools", slug: "floating-tools", posterFrame: 60 }, - { id: "BrandGradient", slug: "brand-gradient", posterFrame: 0 }, - { id: "The48", slug: "the-48", posterFrame: 420 }, - { id: "CloudVsLocal", slug: "cloud-vs-local", posterFrame: 380 }, -]; - -const OUTPUT_DIR = path.resolve(__dirname, "../../landing/public/videos"); -fs.mkdirSync(OUTPUT_DIR, { recursive: true }); - -console.log("Bundling Remotion project..."); -const bundleLocation = await bundle({ - entryPoint: path.resolve(__dirname, "../src/index.ts"), - webpackOverride: (config) => { - const tailwindConfig = enableTailwind(config); - return { - ...tailwindConfig, - resolve: { - ...tailwindConfig.resolve, - alias: { - ...tailwindConfig.resolve?.alias, - "@": path.resolve(__dirname, "../src"), - }, - }, - }; - }, -}); -console.log("Bundle complete.\n"); - -const total = COMPOSITIONS.length; - -for (let i = 0; i < total; i++) { - const comp = COMPOSITIONS[i]; - console.log(`[${i + 1}/${total}] ${comp.id}`); - - const composition = await selectComposition({ - serveUrl: bundleLocation, - id: comp.id, - }); - - // MP4 (H.264, CRF 22) - console.log(" Rendering MP4 (H.264)..."); - await renderMedia({ - composition, - serveUrl: bundleLocation, - codec: "h264", - crf: 22, - pixelFormat: "yuv420p", - imageFormat: "jpeg", - concurrency: 4, - outputLocation: path.join(OUTPUT_DIR, `${comp.slug}.mp4`), - onProgress: ({ progress }) => { - const pct = Math.round(progress * 100); - if (pct % 10 === 0) process.stdout.write(`\r MP4: ${pct}%`); - }, - }); - console.log(`\r MP4: done`); - - // WebM (VP9, CRF 31) - console.log(" Rendering WebM (VP9)..."); - await renderMedia({ - composition, - serveUrl: bundleLocation, - codec: "vp9", - crf: 31, - imageFormat: "jpeg", - concurrency: 4, - outputLocation: path.join(OUTPUT_DIR, `${comp.slug}.webm`), - onProgress: ({ progress }) => { - const pct = Math.round(progress * 100); - if (pct % 10 === 0) process.stdout.write(`\r WebM: ${pct}%`); - }, - }); - console.log(`\r WebM: done`); - - // Poster frame (WebP still) - console.log(" Rendering poster frame..."); - await renderStill({ - composition, - serveUrl: bundleLocation, - frame: comp.posterFrame, - imageFormat: "webp", - output: path.join(OUTPUT_DIR, `${comp.slug}-poster.webp`), - }); - console.log(" Poster: done\n"); -} - -console.log( - `All ${total} compositions rendered (MP4 + WebM + poster) to ${OUTPUT_DIR}`, -); diff --git a/apps/videos/src/Root.tsx b/apps/videos/src/Root.tsx deleted file mode 100644 index 01ac7afa..00000000 --- a/apps/videos/src/Root.tsx +++ /dev/null @@ -1,143 +0,0 @@ -import "./style.css"; -import type React from "react"; -import { Composition } from "remotion"; -import { BrandGradient } from "./compositions/ambient/BrandGradient"; -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 { ToolGalaxy } from "./compositions/hero/ToolGalaxy"; -import { ProductDemo } from "./compositions/product-demo/ProductDemo"; -import { PromoTeaser } from "./compositions/promo-teaser/PromoTeaser"; -import { PromoTeaserVertical } from "./compositions/promo-teaser/PromoTeaserVertical"; -import { CloudVsLocal } from "./compositions/showcase/CloudVsLocal"; -import { The48 } from "./compositions/showcase/The48"; -import { XLaunchVideo } from "./compositions/x-launch/XLaunchVideo"; - -export const RemotionRoot: React.FC = () => ( - <> - {/* Ambient */} - - - - {/* Features */} - - - - - {/* Hero */} - - - - - {/* Showcase */} - - - - {/* Promo / Marketing */} - - - - - -); diff --git a/apps/videos/src/components/AppMockup.tsx b/apps/videos/src/components/AppMockup.tsx deleted file mode 100644 index 8c250b4e..00000000 --- a/apps/videos/src/components/AppMockup.tsx +++ /dev/null @@ -1,201 +0,0 @@ -import type React from "react"; -import { Img, interpolate, spring, staticFile, useCurrentFrame, useVideoConfig } from "remotion"; -import { PhotoPlaceholder } from "@/components/PhotoPlaceholder"; -import { COLOR } from "@/lib/colors"; -import { FONT } from "@/lib/fonts"; -import { SPRING } from "@/lib/motion"; - -const CATEGORY_COLORS = [ - COLOR.category.essentials, - COLOR.category.optimization, - COLOR.category.adjustments, - COLOR.category.ai, - COLOR.category.watermark, - COLOR.category.utilities, - COLOR.category.layout, - COLOR.category.format, -]; - -const TOOLBAR_ICONS = [ - { id: "crop", width: 24, bg: "#d1d5db" }, - { id: "resize", width: 24, bg: "#d1d5db" }, - { id: "adjust", width: 24, bg: "#d1d5db" }, - { id: "filter", width: 32, bg: "#d1d5db" }, - { id: "export", width: 24, bg: "#d1d5db" }, -]; - -export const AppMockup: React.FC<{ - startFrame: number; -}> = ({ startFrame }) => { - const frame = useCurrentFrame(); - const { fps } = useVideoConfig(); - - /* Sidebar slides in from left */ - const sidebarSpring = spring({ - frame: frame - startFrame, - fps, - config: SPRING.snappy, - }); - const sidebarX = interpolate(sidebarSpring, [0, 1], [-240, 0]); - - /* Header slides down from top */ - const headerSpring = spring({ - frame: frame - (startFrame + 4), - fps, - config: SPRING.snappy, - }); - const headerY = interpolate(headerSpring, [0, 1], [-56, 0]); - - /* Center canvas fade in */ - const canvasOpacity = interpolate(frame, [startFrame + 8, startFrame + 20], [0, 1], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - }); - - /* Toolbar fade in */ - const toolbarOpacity = interpolate(frame, [startFrame + 12, startFrame + 24], [0, 1], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - }); - - return ( -
- {/* Header bar */} -
- {/* Otter logo + name */} - - - SnapOtter - - - {/* Search bar placeholder */} -
-
- -
- {/* Left sidebar */} -
- {CATEGORY_COLORS.map((color, i) => ( -
-
-
-
- ))} -
- - {/* Center canvas area */} -
- {/* Photo placeholder */} -
- -
- - {/* Toolbar row */} -
- {TOOLBAR_ICONS.map((icon) => ( -
- ))} -
-
-
-
- ); -}; diff --git a/apps/videos/src/components/AppWindow.tsx b/apps/videos/src/components/AppWindow.tsx deleted file mode 100644 index 5a9bb02b..00000000 --- a/apps/videos/src/components/AppWindow.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import type React from "react"; - -const TRAFFIC_LIGHTS = [ - { color: "#ff5f57", border: "#e0443e" }, - { color: "#febc2e", border: "#dea123" }, - { color: "#28c840", border: "#1aab29" }, -]; - -export const AppWindow: React.FC<{ - children: React.ReactNode; - title?: string; - width: number; - height: number; - topBarColor?: string; - bodyColor?: string; - style?: React.CSSProperties; -}> = ({ - children, - title, - width, - height, - topBarColor = "#1e1e2e", - bodyColor = "#0d1117", - style, -}) => { - return ( -
-
- {TRAFFIC_LIGHTS.map((dot) => ( -
- ))} - {title && ( -
- {title} -
- )} -
-
- {children} -
-
- ); -}; diff --git a/apps/videos/src/components/BeforeAfter.tsx b/apps/videos/src/components/BeforeAfter.tsx deleted file mode 100644 index 88b0de62..00000000 --- a/apps/videos/src/components/BeforeAfter.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import type React from "react"; -import { interpolate, useCurrentFrame } from "remotion"; -import { COLOR } from "@/lib/colors"; -import { EASE } from "@/lib/motion"; - -export const BeforeAfter: React.FC<{ - before: React.ReactNode; - after: React.ReactNode; - scanStartFrame: number; - scanDuration: number; - width: number; - height: number; - style?: React.CSSProperties; -}> = ({ before, after, scanStartFrame, scanDuration, width, height, style }) => { - const frame = useCurrentFrame(); - - const scanProgress = interpolate( - frame, - [scanStartFrame, scanStartFrame + scanDuration], - [0, 100], - { extrapolateLeft: "clamp", extrapolateRight: "clamp", easing: EASE.smooth }, - ); - - const scanX = (scanProgress / 100) * width; - - return ( -
-
{before}
- -
- {after} -
- - {scanProgress > 0 && scanProgress < 100 && ( -
- )} -
- ); -}; diff --git a/apps/videos/src/components/ClipReveal.tsx b/apps/videos/src/components/ClipReveal.tsx deleted file mode 100644 index 0e9400e1..00000000 --- a/apps/videos/src/components/ClipReveal.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import type React from "react"; -import { interpolate, useCurrentFrame } from "remotion"; -import { EASE } from "@/lib/motion"; - -export const ClipReveal: React.FC<{ - children: React.ReactNode; - startFrame: number; - duration?: number; - direction?: "up" | "down"; -}> = ({ children, startFrame, duration = 15, direction = "up" }) => { - const frame = useCurrentFrame(); - const progress = interpolate(frame, [startFrame, startFrame + duration], [0, 1], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - easing: EASE.enter, - }); - const translateY = - direction === "up" - ? interpolate(progress, [0, 1], [40, 0]) - : interpolate(progress, [0, 1], [-40, 0]); - - return ( -
-
- {children} -
-
- ); -}; diff --git a/apps/videos/src/components/Counter.tsx b/apps/videos/src/components/Counter.tsx deleted file mode 100644 index 432817a4..00000000 --- a/apps/videos/src/components/Counter.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import type React from "react"; -import { interpolate, useCurrentFrame } from "remotion"; -import { EASE } from "@/lib/motion"; - -export const Counter: React.FC<{ - from: number; - to: number; - startFrame: number; - duration: number; - style?: React.CSSProperties; - format?: (n: number) => string; -}> = ({ from, to, startFrame, duration, style, format }) => { - const frame = useCurrentFrame(); - const raw = interpolate(frame, [startFrame, startFrame + duration], [from, to], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - easing: EASE.enter, - }); - const value = Math.floor(raw); - return {format ? format(value) : String(value)}; -}; diff --git a/apps/videos/src/components/FeaturePill.tsx b/apps/videos/src/components/FeaturePill.tsx deleted file mode 100644 index d2b751f9..00000000 --- a/apps/videos/src/components/FeaturePill.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import type React from "react"; -import { interpolate, spring, useCurrentFrame, useVideoConfig } from "remotion"; -import { COLOR } from "@/lib/colors"; -import { FONT } from "@/lib/fonts"; -import { SPRING } from "@/lib/motion"; - -export const FeaturePill: React.FC<{ - label: string; - enterFrame: number; - targetX: number; - targetY: number; - fromDirection?: "left" | "right" | "top" | "bottom"; -}> = ({ label, enterFrame, targetX, targetY, fromDirection = "left" }) => { - const frame = useCurrentFrame(); - const { fps } = useVideoConfig(); - - const progress = spring({ - frame: frame - enterFrame, - fps, - config: SPRING.popIn, - }); - - const offscreen = { - left: { x: -300, y: targetY }, - right: { x: 1400, y: targetY }, - top: { x: targetX, y: -100 }, - bottom: { x: targetX, y: 1200 }, - }[fromDirection]; - - const x = interpolate(progress, [0, 1], [offscreen.x, targetX]); - const y = interpolate(progress, [0, 1], [offscreen.y, targetY]); - const scale = interpolate(progress, [0, 1], [0.8, 1]); - - return ( -
- {label} -
- ); -}; diff --git a/apps/videos/src/components/GitHubCTA.tsx b/apps/videos/src/components/GitHubCTA.tsx deleted file mode 100644 index 9bd83ea6..00000000 --- a/apps/videos/src/components/GitHubCTA.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import type React from "react"; -import { AbsoluteFill, Img, interpolate, staticFile, useCurrentFrame } from "remotion"; -import { ClipReveal } from "@/components/ClipReveal"; -import { COLOR } from "@/lib/colors"; -import { FONT, TEXT } from "@/lib/fonts"; - -export const GitHubCTA: React.FC<{ - labelFrame?: number; - logoFrame?: number; - taglineFrame?: number; - ctaFrame?: number; - urlFrame?: number; -}> = ({ labelFrame = 0, logoFrame = 15, taglineFrame = 30, ctaFrame = 50, urlFrame = 65 }) => { - const frame = useCurrentFrame(); - - const logoScale = interpolate(frame, [logoFrame, logoFrame + 15], [0, 1], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - }); - - const glowRadius = interpolate(frame, [logoFrame, logoFrame + 15], [0, 200], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - }); - - const borderOpacity = interpolate(Math.sin(frame * 0.06), [-1, 1], [0.3, 0.6]); - - return ( - - - - 100% OPEN SOURCE - - - -
- -
-
- -
- -
- - - Free forever. - - -
- - -
- - - - - Star us on GitHub - -
-
- -
- - - - github.com/snapotter-hq/SnapOtter - - - - ); -}; diff --git a/apps/videos/src/components/GradientBlob.tsx b/apps/videos/src/components/GradientBlob.tsx deleted file mode 100644 index d7ae3da5..00000000 --- a/apps/videos/src/components/GradientBlob.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import type React from "react"; -import { useCurrentFrame } from "remotion"; - -interface BlobConfig { - color: string; - radius: number; - cx: number; - cy: number; - a: number; - b: number; - phaseX: number; - phaseY: number; - amplitudeX: number; - amplitudeY: number; -} - -export const GradientBlob: React.FC<{ - config: BlobConfig; - duration: number; -}> = ({ config, duration }) => { - const frame = useCurrentFrame(); - const t = (frame / duration) * Math.PI * 2; - const x = config.cx + config.amplitudeX * Math.sin(config.a * t + config.phaseX); - const y = config.cy + config.amplitudeY * Math.sin(config.b * t + config.phaseY); - - return ( -
- ); -}; diff --git a/apps/videos/src/components/GradientMesh.tsx b/apps/videos/src/components/GradientMesh.tsx deleted file mode 100644 index 33c80021..00000000 --- a/apps/videos/src/components/GradientMesh.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import type React from "react"; -import type { CSSProperties } from "react"; -import { AbsoluteFill, interpolate, useCurrentFrame } from "remotion"; -import { GradientBlob } from "@/components/GradientBlob"; - -interface BlobDef { - color: string; - radius: number; - cx: number; - cy: number; - a: number; - b: number; - phaseX: number; - phaseY: number; - amplitudeX: number; - amplitudeY: number; -} - -export const GradientMesh: React.FC<{ - blobs: BlobDef[]; - duration: number; - baseOpacity?: number; - opacityRange?: [number, number]; - opacityFrameRange?: [number, number]; - style?: CSSProperties; -}> = ({ blobs, duration, baseOpacity, opacityRange, opacityFrameRange, style }) => { - const frame = useCurrentFrame(); - let containerOpacity = baseOpacity ?? 1; - - if (opacityRange && opacityFrameRange) { - containerOpacity = interpolate(frame, opacityFrameRange, opacityRange, { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - }); - } - - return ( - - {blobs.map((blob, i) => ( - - ))} - - ); -}; diff --git a/apps/videos/src/components/GrainOverlay.tsx b/apps/videos/src/components/GrainOverlay.tsx deleted file mode 100644 index 0b4f5289..00000000 --- a/apps/videos/src/components/GrainOverlay.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import type React from "react"; -import { AbsoluteFill, useCurrentFrame } from "remotion"; - -export const GrainOverlay: React.FC<{ opacity?: number }> = ({ opacity = 0.03 }) => { - const frame = useCurrentFrame(); - return ( - - - - - - - - - - ); -}; diff --git a/apps/videos/src/components/LogoReveal.tsx b/apps/videos/src/components/LogoReveal.tsx deleted file mode 100644 index c8188c00..00000000 --- a/apps/videos/src/components/LogoReveal.tsx +++ /dev/null @@ -1,140 +0,0 @@ -import type React from "react"; -import { - AbsoluteFill, - Img, - interpolate, - random, - spring, - staticFile, - useCurrentFrame, - useVideoConfig, -} from "remotion"; -import { ClipReveal } from "@/components/ClipReveal"; -import { COLOR } from "@/lib/colors"; -import { TEXT } from "@/lib/fonts"; -import { SPRING } from "@/lib/motion"; - -const PARTICLE_COUNT = 25; - -export const LogoReveal: React.FC<{ - convergeFrame?: number; - burstFrame?: number; - logoFrame?: number; - textFrame?: number; - taglineFrame?: number; - logoSize?: number; -}> = ({ - convergeFrame = 0, - burstFrame = 30, - logoFrame = 30, - textFrame = 50, - taglineFrame = 70, - logoSize = 80, -}) => { - const frame = useCurrentFrame(); - const { fps, width, height } = useVideoConfig(); - - const logoScale = spring({ - frame: frame - logoFrame, - fps, - config: SPRING.popIn, - }); - - const glowRadius = interpolate(frame, [burstFrame, burstFrame + 15], [0, 200], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - }); - const glowOpacity = interpolate(frame, [burstFrame, burstFrame + 20], [0.2, 0], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - }); - - return ( - - {Array.from({ length: PARTICLE_COUNT }).map((_, i) => { - const baseAngle = random(`particle-angle-${i}`) * Math.PI * 2; - const maxRadius = 300 + random(`particle-radius-${i}`) * 200; - const size = 2 + random(`particle-size-${i}`) * 3; - const rotSpeed = 0.03 + random(`particle-speed-${i}`) * 0.02; - const particleOpacity = 0.3 + random(`particle-opacity-${i}`) * 0.7; - - const convergeProgress = interpolate(frame, [convergeFrame, burstFrame], [1, 0], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - }); - - const angle = baseAngle + frame * rotSpeed; - const radius = maxRadius * convergeProgress; - const px = width / 2 + Math.cos(angle) * radius; - const py = height / 2 + Math.sin(angle) * radius; - const pOpacity = convergeProgress > 0.05 ? particleOpacity : 0; - - return ( -
- ); - })} - -
- - - -
- - - SnapOtter - - -
- - - - Your images. Stay yours. - - - - ); -}; diff --git a/apps/videos/src/components/NumberPunch.tsx b/apps/videos/src/components/NumberPunch.tsx deleted file mode 100644 index 57039487..00000000 --- a/apps/videos/src/components/NumberPunch.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import type React from "react"; -import { interpolate, spring, useCurrentFrame, useVideoConfig } from "remotion"; -import { COLOR } from "@/lib/colors"; -import { TEXT } from "@/lib/fonts"; -import { SPRING } from "@/lib/motion"; - -export const NumberPunch: React.FC<{ - number: string; - descriptor: string; - enterFrame: number; - numberSize?: number; - shakeIntensity?: number; -}> = ({ number, descriptor, enterFrame, numberSize = 120, shakeIntensity = 2 }) => { - const frame = useCurrentFrame(); - const { fps } = useVideoConfig(); - - const elapsed = frame - enterFrame; - const scaleSpring = spring({ frame: elapsed, fps, config: SPRING.settle }); - const scale = interpolate(scaleSpring, [0, 1], [1.5, 1]); - - const shakeDecay = interpolate(elapsed, [0, 12], [1, 0], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - }); - const shakeX = Math.sin(elapsed * 3) * shakeIntensity * shakeDecay; - const shakeY = Math.cos(elapsed * 4) * shakeIntensity * shakeDecay; - - const descOpacity = interpolate(elapsed, [8, 18], [0, 1], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - }); - - const numberOpacity = interpolate(elapsed, [0, 4], [0, 1], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - }); - - return ( -
- - {number} - - - {descriptor} - -
- ); -}; diff --git a/apps/videos/src/components/PhotoPlaceholder.tsx b/apps/videos/src/components/PhotoPlaceholder.tsx deleted file mode 100644 index 8d5f2592..00000000 --- a/apps/videos/src/components/PhotoPlaceholder.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import type React from "react"; - -export const PhotoPlaceholder: React.FC<{ - width: number; - height: number; - hue?: number; - style?: React.CSSProperties; -}> = ({ width, height, hue = 30, style }) => ( -
-); diff --git a/apps/videos/src/components/ProgressBar.tsx b/apps/videos/src/components/ProgressBar.tsx deleted file mode 100644 index adcccfa8..00000000 --- a/apps/videos/src/components/ProgressBar.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import type React from "react"; -import { interpolate, useCurrentFrame } from "remotion"; -import { EASE } from "@/lib/motion"; - -export const ProgressBar: React.FC<{ - startFrame: number; - duration: number; - width?: number; - height?: number; - color?: string; - bgColor?: string; - style?: React.CSSProperties; -}> = ({ - startFrame, - duration, - width = 200, - height = 6, - color = "#3b82f6", - bgColor = "rgba(255,255,255,0.1)", - style, -}) => { - const frame = useCurrentFrame(); - const progress = interpolate(frame, [startFrame, startFrame + duration], [0, 100], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - easing: EASE.smooth, - }); - - return ( -
-
-
- ); -}; diff --git a/apps/videos/src/components/RotatingTaglines.tsx b/apps/videos/src/components/RotatingTaglines.tsx deleted file mode 100644 index f7801894..00000000 --- a/apps/videos/src/components/RotatingTaglines.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import type React from "react"; -import { interpolate, useCurrentFrame } from "remotion"; -import { COLOR } from "@/lib/colors"; -import { FONT } from "@/lib/fonts"; -import { EASE } from "@/lib/motion"; - -export const RotatingTaglines: React.FC<{ - lines: string[]; - startFrame: number; - framesPerLine: number; - fontSize?: number; - color?: string; -}> = ({ lines, startFrame, framesPerLine, fontSize = 36, color = COLOR.accent }) => { - const frame = useCurrentFrame(); - const elapsed = frame - startFrame; - if (elapsed < 0) return null; - - const lineIndex = Math.floor(elapsed / framesPerLine); - const lineProgress = (elapsed % framesPerLine) / framesPerLine; - if (lineIndex >= lines.length) return null; - - const enterDuration = 0.2; - const holdEnd = 0.7; - const exitEnd = 1.0; - - const opacity = - lineProgress < enterDuration - ? interpolate(lineProgress, [0, enterDuration], [0, 1], { easing: EASE.enter }) - : lineProgress < holdEnd - ? 1 - : interpolate(lineProgress, [holdEnd, exitEnd], [1, 0], { easing: EASE.exit }); - - const translateY = - lineProgress < enterDuration - ? interpolate(lineProgress, [0, enterDuration], [30, 0], { easing: EASE.enter }) - : lineProgress > holdEnd - ? interpolate(lineProgress, [holdEnd, exitEnd], [0, -20], { easing: EASE.exit }) - : 0; - - const glowOpacity = interpolate(Math.sin(frame * 0.08), [-1, 1], [0.03, 0.08]); - - return ( -
-
-
- {lines[lineIndex]} -
-
- ); -}; diff --git a/apps/videos/src/components/ShieldIcon.tsx b/apps/videos/src/components/ShieldIcon.tsx deleted file mode 100644 index f4a80be0..00000000 --- a/apps/videos/src/components/ShieldIcon.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { evolvePath } from "@remotion/paths"; -import type React from "react"; -import { interpolate, useCurrentFrame } from "remotion"; -import { EASE } from "@/lib/motion"; - -const SHIELD_PATH = "M50 5 L90 25 L90 55 C90 80 70 95 50 100 C30 95 10 80 10 55 L10 25 Z"; - -export const ShieldIcon: React.FC<{ - size: number; - startFrame: number; - duration?: number; - fillOpacity?: number; - style?: React.CSSProperties; -}> = ({ size, startFrame, duration = 35, fillOpacity = 0, style }) => { - const frame = useCurrentFrame(); - const progress = interpolate(frame, [startFrame, startFrame + duration], [0, 1], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - easing: EASE.enter, - }); - - const { strokeDasharray, strokeDashoffset } = evolvePath(progress, SHIELD_PATH); - - return ( - - - - - - - - - - 0 ? "url(#shield-fill-grad)" : "none"} - fillOpacity={fillOpacity > 0 ? fillOpacity * 12 : 0} - strokeDasharray={strokeDasharray} - strokeDashoffset={strokeDashoffset} - strokeLinecap="round" - strokeLinejoin="round" - /> - - ); -}; diff --git a/apps/videos/src/components/Terminal.tsx b/apps/videos/src/components/Terminal.tsx deleted file mode 100644 index 95348002..00000000 --- a/apps/videos/src/components/Terminal.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import type React from "react"; -import { interpolate, spring, useCurrentFrame, useVideoConfig } from "remotion"; -import { AppWindow } from "@/components/AppWindow"; -import { TypeWriter } from "@/components/TypeWriter"; -import { SPRING } from "@/lib/motion"; - -interface OutputLine { - text: string; - color: string; - delay: number; -} - -export const Terminal: React.FC<{ - command: { text: string; color: string }[]; - commandStartFrame: number; - commandSpeed?: number; - outputLines?: OutputLine[]; - outputStartFrame: number; - outputStagger?: number; - width?: number; - height?: number; - style?: React.CSSProperties; - enterFrame?: number; -}> = ({ - command, - commandStartFrame, - commandSpeed = 2, - outputLines = [], - outputStartFrame, - outputStagger = 3, - width = 800, - height = 450, - style, - enterFrame = 0, -}) => { - const frame = useCurrentFrame(); - const { fps } = useVideoConfig(); - - const enterProgress = spring({ - frame: frame - enterFrame, - fps, - config: SPRING.snappy, - }); - - const translateY = interpolate(enterProgress, [0, 1], [height + 50, 0]); - const opacity = interpolate(enterProgress, [0, 1], [0, 1]); - - return ( -
- -
-
- $ - -
- - {outputLines.map((line, i) => { - const lineFrame = outputStartFrame + i * outputStagger; - const lineOpacity = interpolate(frame, [lineFrame, lineFrame + 6], [0, 1], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - }); - - return ( -
- {line.text} -
- ); - })} -
-
-
- ); -}; diff --git a/apps/videos/src/components/TerminalWindow.tsx b/apps/videos/src/components/TerminalWindow.tsx deleted file mode 100644 index 1323dc46..00000000 --- a/apps/videos/src/components/TerminalWindow.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import type React from "react"; -import { FONT } from "@/lib/fonts"; - -const TRAFFIC_LIGHTS = [ - { color: "#ff5f57", key: "red" }, - { color: "#febc2e", key: "yellow" }, - { color: "#28c840", key: "green" }, -]; - -export const TerminalWindow: React.FC<{ - width: number; - height: number; - title?: string; - style?: React.CSSProperties; - children?: React.ReactNode; - /** Opacity for traffic light dots (0-1), used for morph transitions */ - dotsOpacity?: number; - /** Override top bar background color */ - topBarColor?: string; - /** Override body background color */ - bodyColor?: string; - /** Optional element to render in the title area instead of plain text */ - titleElement?: React.ReactNode; -}> = ({ - width, - height, - title = "Terminal", - style, - children, - dotsOpacity = 1, - topBarColor = "#1e1e2e", - bodyColor = "#0d1117", - titleElement, -}) => { - return ( -
- {/* Top bar */} -
- {/* Traffic light dots */} -
- {TRAFFIC_LIGHTS.map((dot) => ( -
- ))} -
- - {/* Title area (centered) */} -
- {titleElement || ( - - {title} - - )} -
-
- - {/* Body */} -
- {children} -
-
- ); -}; diff --git a/apps/videos/src/components/ToolGrid.tsx b/apps/videos/src/components/ToolGrid.tsx deleted file mode 100644 index e26a08f6..00000000 --- a/apps/videos/src/components/ToolGrid.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import type React from "react"; -import { spring, useCurrentFrame, useVideoConfig } from "remotion"; -import { ToolPill } from "@/components/ToolPill"; -import { CATEGORY_ORDER } from "@/lib/colors"; -import { SPRING, TIMING } from "@/lib/motion"; -import { getToolsByCategory, TOOLS } from "@/lib/tools"; - -export const ToolGrid: React.FC<{ - startFrame: number; - cellWidth?: number; - cellHeight?: number; - gap?: number; - style?: React.CSSProperties; -}> = ({ startFrame, cellWidth = 140, cellHeight = 32, gap = 6, style }) => { - const frame = useCurrentFrame(); - const { fps } = useVideoConfig(); - - let globalIndex = 0; - - return ( -
- {CATEGORY_ORDER.map((cat) => { - const tools = getToolsByCategory(cat); - return ( -
- {tools.map((tool) => { - const i = globalIndex++; - const enterDelay = startFrame + i * TIMING.staggerFrames; - const s = spring({ - frame: frame - enterDelay, - fps, - config: SPRING.settle, - }); - - return ( -
- -
- ); - })} -
- ); - })} -
- ); -}; diff --git a/apps/videos/src/components/ToolPill.tsx b/apps/videos/src/components/ToolPill.tsx deleted file mode 100644 index 7b9ccbf1..00000000 --- a/apps/videos/src/components/ToolPill.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import type React from "react"; -import { COLOR } from "@/lib/colors"; -import { TEXT } from "@/lib/fonts"; - -export const ToolPill: React.FC<{ - name: string; - category: string; - style?: React.CSSProperties; -}> = ({ name, category, style }) => { - const color = COLOR.category[category] ?? COLOR.accent; - return ( -
- {name} -
- ); -}; diff --git a/apps/videos/src/components/TypeWriter.tsx b/apps/videos/src/components/TypeWriter.tsx deleted file mode 100644 index 6f48582c..00000000 --- a/apps/videos/src/components/TypeWriter.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import type React from "react"; -import { useCurrentFrame } from "remotion"; -import { FONT } from "@/lib/fonts"; - -interface Segment { - text: string; - color: string; -} - -export const TypeWriter: React.FC<{ - segments: Segment[]; - startFrame: number; - speed?: number; - style?: React.CSSProperties; -}> = ({ segments, startFrame, speed = 2, style }) => { - const frame = useCurrentFrame(); - const elapsed = Math.max(0, frame - startFrame); - const charIndex = Math.floor(elapsed / speed); - const fullText = segments.map((s) => s.text).join(""); - const visibleChars = Math.min(charIndex, fullText.length); - - let rendered = 0; - const nodes: React.ReactNode[] = []; - for (const seg of segments) { - const segStart = rendered; - const visible = Math.max(0, Math.min(visibleChars - segStart, seg.text.length)); - if (visible > 0) { - nodes.push( - - {seg.text.slice(0, visible)} - , - ); - } - rendered += seg.text.length; - if (rendered >= visibleChars) break; - } - - const showCursor = visibleChars < fullText.length && Math.floor(frame / 8) % 2 === 0; - - return ( - - {nodes} - {showCursor && ( - - )} - - ); -}; diff --git a/apps/videos/src/components/WipeTransition.tsx b/apps/videos/src/components/WipeTransition.tsx deleted file mode 100644 index 0fd0ff7b..00000000 --- a/apps/videos/src/components/WipeTransition.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import type React from "react"; -import { AbsoluteFill, interpolate, useCurrentFrame } from "remotion"; -import { EASE } from "@/lib/motion"; - -export const WipeTransition: React.FC<{ - children: React.ReactNode; - startFrame: number; - duration?: number; - direction?: "left" | "right"; -}> = ({ children, startFrame, duration = 18, direction = "left" }) => { - const frame = useCurrentFrame(); - const progress = interpolate(frame, [startFrame, startFrame + duration], [0, 100], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - easing: EASE.enter, - }); - const clipPath = - direction === "left" ? `inset(0 ${100 - progress}% 0 0)` : `inset(0 0 0 ${100 - progress}%)`; - - return {children}; -}; diff --git a/apps/videos/src/compositions/ambient/BrandGradient.tsx b/apps/videos/src/compositions/ambient/BrandGradient.tsx deleted file mode 100644 index 2502b34e..00000000 --- a/apps/videos/src/compositions/ambient/BrandGradient.tsx +++ /dev/null @@ -1,140 +0,0 @@ -import type React from "react"; -import { AbsoluteFill, Img, staticFile, useCurrentFrame } from "remotion"; -import { GradientBlob } from "@/components/GradientBlob"; -import { GrainOverlay } from "@/components/GrainOverlay"; -import { COLOR } from "@/lib/colors"; -import { FONT } from "@/lib/fonts"; -import { TOOLS } from "@/lib/tools"; - -const DURATION = 150; - -const BLOBS = [ - { - color: "#f59e0b", - radius: 180, - cx: 200, - cy: 150, - a: 2, - b: 3, - phaseX: 0, - phaseY: 0, - amplitudeX: 80, - amplitudeY: 60, - }, - { - color: "#f97316", - radius: 150, - cx: 500, - cy: 350, - a: 3, - b: 2, - phaseX: 1.2, - phaseY: 0.8, - amplitudeX: 70, - amplitudeY: 90, - }, - { - color: "#3b82f6", - radius: 200, - cx: 600, - cy: 150, - a: 2, - b: 1, - phaseX: 2.4, - phaseY: 1.6, - amplitudeX: 90, - amplitudeY: 50, - }, - { - color: "#14b8a6", - radius: 140, - cx: 350, - cy: 400, - a: 1, - b: 2, - phaseX: 3.6, - phaseY: 2.4, - amplitudeX: 60, - amplitudeY: 80, - }, - { - color: "#8b5cf6", - radius: 160, - cx: 150, - cy: 350, - a: 3, - b: 1, - phaseX: 4.8, - phaseY: 3.2, - amplitudeX: 50, - amplitudeY: 70, - }, -]; - -/** Pre-computed scattered tool name positions (frozen, for texture) */ -const SCATTERED_TOOLS = TOOLS.slice(0, 15).map((tool, i) => ({ - name: tool.name, - x: ((i * 137 + 47) % 750) + 25, - y: ((i * 89 + 23) % 550) + 25, - fontSize: 11 + (i % 3) * 2, -})); - -export const BrandGradient: React.FC = () => { - const frame = useCurrentFrame(); - const logoScale = 1 + Math.sin(frame * 0.08) * 0.03; - - return ( - - {/* Scattered tool names for texture (frozen, 5% opacity) */} - {SCATTERED_TOOLS.map((tool) => ( - - {tool.name} - - ))} - - {BLOBS.map((blob) => ( - - ))} - - {/* Center logo with subtle scale pulse */} - - - {/* Radial vignette */} -
- - - - ); -}; diff --git a/apps/videos/src/compositions/ambient/FloatingTools.tsx b/apps/videos/src/compositions/ambient/FloatingTools.tsx deleted file mode 100644 index f0b3f335..00000000 --- a/apps/videos/src/compositions/ambient/FloatingTools.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import type React from "react"; -import { useState } from "react"; -import { AbsoluteFill, random, useCurrentFrame, useVideoConfig } from "remotion"; -import { GrainOverlay } from "@/components/GrainOverlay"; -import { COLOR } from "@/lib/colors"; -import { FONT } from "@/lib/fonts"; -import { TOOLS } from "@/lib/tools"; - -const LAYERS = [ - { speed: 0.8, fontSize: 14, opacity: 0.12, count: 10, blur: 1 }, - { speed: 1.5, fontSize: 18, opacity: 0.22, count: 9, blur: 0.5 }, - { speed: 2.5, fontSize: 24, opacity: 0.35, count: 6, blur: 0 }, -]; - -const X_FREQ = (2 * Math.PI) / 120; - -export const FloatingTools: React.FC = () => { - const frame = useCurrentFrame(); - const { width, height } = useVideoConfig(); - const wrapH = height + 100; - - const [items] = useState(() => { - let toolIdx = 0; - return LAYERS.flatMap((layer, li) => - Array.from({ length: layer.count }, (_, i) => { - const tool = TOOLS[toolIdx % TOOLS.length]; - toolIdx++; - return { - tool, - layer, - x: random(`float-x-${li}-${i}`) * width, - startY: random(`float-y-${li}-${i}`) * wrapH, - phase: random(`float-phase-${li}-${i}`) * Math.PI * 2, - }; - }), - ); - }); - - return ( - - {items.map((item, idx) => { - const rawY = item.startY - frame * item.layer.speed; - const y = (((rawY % wrapH) + wrapH) % wrapH) - 50; - const xOffset = Math.sin(frame * X_FREQ + item.phase) * 8; - - return ( - 0 ? `blur(${item.layer.blur}px)` : undefined, - }} - > - {item.tool.name} - - ); - })} - {/* Radial vignette */} -
- - - ); -}; diff --git a/apps/videos/src/compositions/features/AiMagicReel.tsx b/apps/videos/src/compositions/features/AiMagicReel.tsx deleted file mode 100644 index 5fe440da..00000000 --- a/apps/videos/src/compositions/features/AiMagicReel.tsx +++ /dev/null @@ -1,799 +0,0 @@ -import type React from "react"; -import { - AbsoluteFill, - Img, - interpolate, - interpolateColors, - random, - Sequence, - spring, - staticFile, - useCurrentFrame, - useVideoConfig, -} from "remotion"; -import { ClipReveal } from "@/components/ClipReveal"; -import { GrainOverlay } from "@/components/GrainOverlay"; -import { WipeTransition } from "@/components/WipeTransition"; -import { COLOR } from "@/lib/colors"; -import { TEXT } from "@/lib/fonts"; -import { EASE, SPRING, TIMING } from "@/lib/motion"; - -/* ------------------------------------------------------------------ */ -/* Shared helpers */ -/* ------------------------------------------------------------------ */ - -const CHECKERBOARD_BG = "repeating-conic-gradient(#888 0% 25%, #ccc 0% 50%) 0 0 / 16px 16px"; - -/** A tiny sparkle that scales 0 -> 1.2 -> 0 and fades over 4 frames */ -const Sparkle: React.FC<{ - x: number; - y: number; - startFrame: number; - color?: string; - size?: number; -}> = ({ x, y, startFrame, color = COLOR.accent, size = 6 }) => { - const frame = useCurrentFrame(); - const local = frame - startFrame; - if (local < 0 || local > 4) return null; - const scale = interpolate(local, [0, 2, 4], [0, 1.2, 0], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - }); - const opacity = interpolate(local, [0, 1, 4], [0, 1, 0], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - }); - return ( -
- ); -}; - -/** Checkmark icon that pops in with a spring */ -const Checkmark: React.FC<{ startFrame: number }> = ({ startFrame }) => { - const frame = useCurrentFrame(); - const { fps } = useVideoConfig(); - const scale = spring({ - frame: frame - startFrame, - fps, - config: SPRING.popIn, - }); - if (frame < startFrame) return null; - return ( -
- -
- ); -}; - -/* ------------------------------------------------------------------ */ -/* Segment A -- Background Removal (frame 0-120) */ -/* ------------------------------------------------------------------ */ - -const SegmentBackgroundRemoval: React.FC = () => { - const frame = useCurrentFrame(); - const photoW = 280; - const photoH = 360; - const photoX = (800 - photoW) / 2; - const photoY = (600 - photoH) / 2 + 20; - - // Scan line position (x within the photo) - const scanX = interpolate(frame, [15, 80], [0, photoW], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - easing: EASE.smooth, - }); - - // Right clip for the "after" layer - const rightClipPct = interpolate(frame, [15, 80], [100, 0], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - easing: EASE.smooth, - }); - - // Subject float-forward after scan completes - const subjectScale = interpolate(frame, [80, 110], [1.0, 1.02], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - easing: EASE.smooth, - }); - const subjectY = interpolate(frame, [80, 110], [0, -5], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - easing: EASE.smooth, - }); - - // Checkerboard background for transparency effect - const checkerBg = CHECKERBOARD_BG; - - // Scan line particle trail - const particles = Array.from({ length: 5 }, (_, i) => { - const py = random(`scan-particle-y-${i}`) * photoH; - const drift = interpolate(frame, [15, 80], [0, -20], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - }); - const particleOpacity = interpolate(frame, [15, 80], [0.8, 0], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - }); - return { - x: scanX - 4 + random(`scan-particle-x-${i}`) * 8, - y: py + drift * random(`scan-drift-${i}`), - opacity: particleOpacity * random(`scan-particle-o-${i}`), - }; - }); - - return ( - - {/* Label */} -
- - Background Removal - -
- - {/* Photo container */} -
- {/* "Before" layer: real portrait photo */} - - - {/* "After" layer: checkerboard + portrait (revealed via clip) */} -
- {/* Checkerboard background */} -
- {/* Subject portrait on top of checkerboard */} - -
- - {/* Scan line */} - {frame >= 15 && frame <= 82 && ( -
- )} - - {/* Scan line particle trail */} - {frame >= 15 && - frame <= 80 && - particles.map((p, i) => ( -
- ))} -
- - {/* Checkmark after completion */} -
- -
- - ); -}; - -/* ------------------------------------------------------------------ */ -/* Segment B -- AI Colorization (frame 120-240) */ -/* ------------------------------------------------------------------ */ - -const SegmentColorization: React.FC = () => { - const frame = useCurrentFrame(); - const photoW = 360; - const photoH = 240; - const photoX = (800 - photoW) / 2; - const photoY = (600 - photoH) / 2 + 20; - - // Color wave sweep - const waveProgress = interpolate(frame, [25, 90], [0, 100], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - easing: EASE.smooth, - }); - - // Hold pulse - const holdScale = interpolate(frame, [90, 100, 110, 120], [1.0, 1.01, 1.0, 1.0], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - }); - - // Color particles along the wavefront - const colorParticles = Array.from({ length: 8 }, (_, i) => { - const py = random(`color-particle-y-${i}`) * photoH; - const px = (waveProgress / 100) * photoW; - const hue = random(`color-particle-hue-${i}`) * 360; - const lifeStart = 25 + Math.floor(random(`color-particle-start-${i}`) * 60); - const localFrame = frame - lifeStart; - const visible = localFrame >= 0 && localFrame <= 5; - const particleOpacity = visible - ? interpolate(localFrame, [0, 2, 5], [0, 1, 0], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - }) - : 0; - return { - x: px + (random(`color-particle-ox-${i}`) - 0.5) * 20, - y: py, - hue, - opacity: particleOpacity, - visible, - }; - }); - - return ( - - {/* Label */} -
- - AI Colorization - -
- - {/* Photo container */} -
- {/* B&W layer (on top, real B&W photo) */} - - - {/* Color layer (revealed via clip, using portrait as color version) */} -
- -
- - {/* Color particles */} - {colorParticles.map( - (p, i) => - p.visible && ( -
- ), - )} -
- - ); -}; - -/* ------------------------------------------------------------------ */ -/* Segment C -- 4x Super Resolution (frame 240-360) */ -/* ------------------------------------------------------------------ */ - -const PIXEL_GRID_SIZE = 8; -const PIXEL_GAP = 0.5; - -/** Generate deterministic colors for the pixel grid (blues and greens from landscape) */ -const pixelColors: string[] = Array.from({ length: PIXEL_GRID_SIZE * PIXEL_GRID_SIZE }, (_, i) => { - const hue = random(`pixel-hue-${i}`) * 80 + 180; // blues and greens (180-260) - const sat = 35 + random(`pixel-sat-${i}`) * 35; - const lit = 35 + random(`pixel-lit-${i}`) * 30; - return `hsl(${hue}, ${sat}%, ${lit}%)`; -}); - -const SegmentSuperResolution: React.FC = () => { - const frame = useCurrentFrame(); - - const tinySize = 60; - const largeSize = 240; - - // Zoom in: frame 25-60 (local) - const zoomIn = interpolate(frame, [25, 60], [1, 6], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - easing: EASE.smooth, - }); - - // Pixel dissolve: frame 60-90 (local) - const borderRadiusPct = interpolate(frame, [60, 80], [0, 50], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - easing: EASE.smooth, - }); - const pixelBlur = interpolate(frame, [60, 75, 90], [0, 3, 0], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - }); - const gapSize = interpolate(frame, [60, 90], [PIXEL_GAP, 0], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - easing: EASE.smooth, - }); - - // Zoom out: frame 90-110 (local) - const zoomOut = interpolate(frame, [90, 110], [6, 1], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - easing: EASE.smooth, - }); - - // Combined zoom - const zoom = frame < 90 ? zoomIn : zoomOut; - - // Image size transition: tiny -> large after zoom out - const imgSize = interpolate(frame, [90, 110], [tinySize, largeSize], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - easing: EASE.smooth, - }); - - // Determine if we should show the smooth version - const showSmooth = frame > 85; - const smoothOpacity = interpolate(frame, [85, 95], [0, 1], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - }); - - return ( - - {/* Label */} -
- - 4x Super Resolution - -
- - {/* Pixel grid container */} -
= 90 ? imgSize : tinySize, - height: frame >= 90 ? imgSize : tinySize, - }} - > - {/* Pixelated grid */} -
- {pixelColors.map((color, i) => ( -
- ))} -
- - {/* Smooth "resolved" version - actual landscape photo */} - {showSmooth && ( - - )} -
- - {/* Size label */} -
- {frame >= 90 && ( - <> - - - 60x60 - - - - 240x240 - - - )} -
- - {/* Checkmark */} -
- -
- - ); -}; - -/* ------------------------------------------------------------------ */ -/* Segment D -- Photo Restoration (frame 360-480) */ -/* ------------------------------------------------------------------ */ - -/** Generate deterministic scratch lines */ -const SCRATCH_COUNT = 7; -const scratches = Array.from({ length: SCRATCH_COUNT }, (_, i) => ({ - x1: random(`scratch-x1-${i}`) * 300, - y1: random(`scratch-y1-${i}`) * 300, - x2: random(`scratch-x2-${i}`) * 300, - y2: random(`scratch-y2-${i}`) * 300, - width: 1 + random(`scratch-w-${i}`) * 1.5, -})); - -const DUST_COUNT = 4; -const dustSpots = Array.from({ length: DUST_COUNT }, (_, i) => ({ - cx: random(`dust-cx-${i}`) * 260 + 20, - cy: random(`dust-cy-${i}`) * 260 + 20, - r: 3 + random(`dust-r-${i}`) * 6, -})); - -/** Deterministic order for scratch/dust removal */ -const damageOrder = Array.from({ length: SCRATCH_COUNT + DUST_COUNT }, (_, i) => i).sort( - (a, b) => random(`damage-order-${a}`) - random(`damage-order-${b}`), -); - -const SegmentRestoration: React.FC = () => { - const frame = useCurrentFrame(); - const photoSize = 300; - const photoX = (800 - photoSize) / 2; - const photoY = (600 - photoSize) / 2 - 20; - - // Stagger: each damage element heals over the 390-440 range (local 30-80) - const stagger = TIMING.staggerFrames * 4; // 8 frames between each - - // Sepia lift: frame 80-95 (local) - const sepiaAmount = interpolate(frame, [80, 95], [1, 0], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - easing: EASE.smooth, - }); - const contrastAmount = interpolate(frame, [80, 95], [0.9, 1.05], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - easing: EASE.smooth, - }); - - // Color shift from sepia to warm - const _bgFrom = interpolateColors(frame, [80, 95], ["hsl(35, 40%, 45%)", "hsl(30, 55%, 50%)"]); - const _bgTo = interpolateColors(frame, [80, 95], ["hsl(40, 35%, 55%)", "hsl(45, 50%, 58%)"]); - - return ( - - {/* Label */} -
- - Photo Restoration - -
- - {/* Photo with damage */} -
- {/* Base photo - real landscape */} - - - {/* SVG damage overlay */} - - - {/* Sparkles at healed damage locations */} - {damageOrder.map((dmgIdx, orderIdx) => { - const healStart = 30 + orderIdx * stagger; - const isDust = dmgIdx >= SCRATCH_COUNT; - const sparkleX = isDust - ? dustSpots[dmgIdx - SCRATCH_COUNT].cx - : (scratches[dmgIdx].x1 + scratches[dmgIdx].x2) / 2; - const sparkleY = isDust - ? dustSpots[dmgIdx - SCRATCH_COUNT].cy - : (scratches[dmgIdx].y1 + scratches[dmgIdx].y2) / 2; - return ( - - ); - })} -
- - {/* Final text */} -
- - 15 AI models. Your hardware. - -
-
- ); -}; - -/* ------------------------------------------------------------------ */ -/* Main Composition */ -/* ------------------------------------------------------------------ */ - -export const AiMagicReel: React.FC = () => { - return ( - - {/* Segment A: Background Removal (frame 0-120) */} - - - - - {/* Segment B: AI Colorization (frame 120-240) */} - - - - - - - {/* Segment C: 4x Super Resolution (frame 240-360) */} - - - - - - - {/* Segment D: Photo Restoration (frame 360-480) */} - - - - - - - {/* Film grain */} - - - ); -}; diff --git a/apps/videos/src/compositions/features/FormatUniverse.tsx b/apps/videos/src/compositions/features/FormatUniverse.tsx deleted file mode 100644 index be9398f3..00000000 --- a/apps/videos/src/compositions/features/FormatUniverse.tsx +++ /dev/null @@ -1,215 +0,0 @@ -import type React from "react"; -import { AbsoluteFill, Img, interpolate, staticFile, useCurrentFrame } from "remotion"; -import { GrainOverlay } from "@/components/GrainOverlay"; -import { COLOR } from "@/lib/colors"; -import { FONT } from "@/lib/fonts"; - -const RINGS = [ - { - formats: ["CR2", "NEF", "ARW", "RAF", "DNG"], - radius: 120, - color: "#f59e0b", - direction: 1, - }, - { - formats: ["AVIF", "JXL", "HEIC", "WebP", "QOI"], - radius: 195, - color: "#14b8a6", - direction: -1, - }, - { - formats: ["JPEG", "PNG", "TIFF", "BMP", "GIF", "PSD"], - radius: 270, - color: "#3b82f6", - direction: 1, - }, -]; - -const DURATION = 180; -const SPEED = (2 * Math.PI) / DURATION; - -const Badge: React.FC<{ - label: string; - color: string; - angle: number; - radius: number; -}> = ({ label, color, angle, radius }) => { - const x = Math.cos(angle) * radius; - const y = Math.sin(angle) * radius * 0.55; - const z = Math.sin(angle); - const scale = interpolate(z, [-1, 1], [0.75, 1.0]); - const opacity = interpolate(z, [-1, 1], [0.4, 1.0]); - const zIndex = Math.round(interpolate(z, [-1, 1], [0, 10])); - - return ( -
- {label} -
- ); -}; - -/** Trail badge - renders a badge at a past frame's position with reduced opacity */ -const TrailBadge: React.FC<{ - label: string; - color: string; - angle: number; - radius: number; - trailOpacity: number; -}> = ({ label, color, angle, radius, trailOpacity }) => { - const x = Math.cos(angle) * radius; - const y = Math.sin(angle) * radius * 0.55; - const z = Math.sin(angle); - const scale = interpolate(z, [-1, 1], [0.75, 1.0]); - const zIndex = Math.round(interpolate(z, [-1, 1], [0, 10])); - - return ( -
- {label} -
- ); -}; - -export const FormatUniverse: React.FC = () => { - const frame = useCurrentFrame(); - const logoScale = Math.sin(frame * 0.06) * 0.05 + 1; - - return ( - - {/* Orbit ring lines (faint elliptical paths behind each ring) */} - - {RINGS.map((ring) => ( - - ))} - - - {/* Opacity trail: render each badge at frame-1 and frame-2 positions */} - {RINGS.map((ring) => - ring.formats.map((fmt, fi) => { - const angleOffset = (fi / ring.formats.length) * Math.PI * 2; - const trail1Angle = angleOffset + (frame - 1) * SPEED * ring.direction; - const trail2Angle = angleOffset + (frame - 2) * SPEED * ring.direction; - return [ - , - , - ]; - }), - )} - - {/* Main badges at current frame */} - {RINGS.map((ring) => - ring.formats.map((fmt, fi) => { - const angleOffset = (fi / ring.formats.length) * Math.PI * 2; - const angle = angleOffset + frame * SPEED * ring.direction; - return ( - - ); - }), - )} - - - -
- 55+ formats. Every camera. Every device. -
- - -
- ); -}; diff --git a/apps/videos/src/compositions/features/PipelineFlow.tsx b/apps/videos/src/compositions/features/PipelineFlow.tsx deleted file mode 100644 index 1c3ca8bd..00000000 --- a/apps/videos/src/compositions/features/PipelineFlow.tsx +++ /dev/null @@ -1,718 +0,0 @@ -import { Trail } from "@remotion/motion-blur"; -import { evolvePath } from "@remotion/paths"; -import type React from "react"; -import { - AbsoluteFill, - Img, - interpolate, - random, - Sequence, - spring, - staticFile, - useCurrentFrame, - useVideoConfig, -} from "remotion"; -import { ClipReveal } from "@/components/ClipReveal"; -import { Counter } from "@/components/Counter"; -import { GrainOverlay } from "@/components/GrainOverlay"; -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 4px 20px rgba(0,0,0,0.4), 0 0 20px ${station.color}${Math.round(glowOpacity * 255) - .toString(16) - .padStart(2, "0")}` - : "0 4px 20px rgba(0,0,0,0.4)", - 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 size = 24 + random(`thumb-size-${index}`) * 8; - const yOffset = (random(`thumb-y-${index}`) - 0.5) * 30; - const objPosX = Math.round(random(`thumb-opx-${index}`) * 100); - const objPosY = Math.round(random(`thumb-opy-${index}`) * 100); - - // 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. - -
-
- - -
- ); -}; diff --git a/apps/videos/src/compositions/hero/OneCommand.tsx b/apps/videos/src/compositions/hero/OneCommand.tsx deleted file mode 100644 index 6d0018d1..00000000 --- a/apps/videos/src/compositions/hero/OneCommand.tsx +++ /dev/null @@ -1,355 +0,0 @@ -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. - - -
- )} - - - - ); -}; diff --git a/apps/videos/src/compositions/hero/PrivacyPromise.tsx b/apps/videos/src/compositions/hero/PrivacyPromise.tsx deleted file mode 100644 index 1d253e6b..00000000 --- a/apps/videos/src/compositions/hero/PrivacyPromise.tsx +++ /dev/null @@ -1,298 +0,0 @@ -import type React from "react"; -import { - AbsoluteFill, - Img, - interpolate, - spring, - staticFile, - useCurrentFrame, - useVideoConfig, -} from "remotion"; -import { ClipReveal } from "@/components/ClipReveal"; -import { GrainOverlay } from "@/components/GrainOverlay"; -import { ShieldIcon } from "@/components/ShieldIcon"; -import { TEXT } from "@/lib/fonts"; -import { EASE, SPRING } from "@/lib/motion"; - -/* ------------------------------------------------------------------ */ -/* Photo config */ -/* ------------------------------------------------------------------ */ - -const PHOTOS = [ - { - src: "screenshots/sample-photo-portrait.jpg", - w: 180, - h: 135, - fromX: -300, - fromY: -250, - delay: 0, - }, - { - src: "screenshots/sample-photo-landscape.jpg", - w: 160, - h: 120, - fromX: 320, - fromY: 280, - delay: 4, - }, - { src: "screenshots/sample-photo-bw.jpg", w: 150, h: 115, fromX: 280, fromY: -260, delay: 8 }, -] as const; - -/* ------------------------------------------------------------------ */ -/* Padlock SVG (simple circle + rect) */ -/* ------------------------------------------------------------------ */ - -const PadlockIcon: React.FC<{ opacity: number }> = ({ opacity }) => ( - - - - -); - -/* ------------------------------------------------------------------ */ -/* Main composition */ -/* ------------------------------------------------------------------ */ - -export const PrivacyPromise: React.FC = () => { - const frame = useCurrentFrame(); - const { fps, width, height } = useVideoConfig(); - const cx = width / 2; - const cy = height / 2; - - /* ---- Act 3: shield + photos scale & move up ---- */ - const act3Scale = interpolate(frame, [160, 180], [1, 0.6], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - easing: EASE.snap, - }); - const act3OffsetY = interpolate(frame, [160, 180], [0, -120], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - easing: EASE.snap, - }); - - /* ---- Act 4: fade out ---- */ - const act4ShieldOpacity = interpolate(frame, [240, 270], [1, 0], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - easing: EASE.exit, - }); - - /* ---- Shield fill (Act 2) ---- */ - const shieldFillOpacity = interpolate(frame, [110, 130], [0, 0.08], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - }); - - /* ---- Shield pulse glow (Act 2, frame 130-160) ---- */ - const pulseScale = frame >= 130 && frame <= 240 ? 1 + Math.sin((frame - 130) * 0.314) * 0.02 : 1; - - /* ---- Padlock fade in (Act 2) ---- */ - const padlockOpacity = interpolate(frame, [130, 145], [0, 1], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - }); - - /* ---- Act 4 scale-down for shield ---- */ - const act4ShieldScale = interpolate(frame, [240, 270], [1, 0.8], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - easing: EASE.exit, - }); - - /* ---- Text fade out (Act 4) ---- */ - const act4TextOpacity = interpolate(frame, [240, 265], [1, 0], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - easing: EASE.exit, - }); - - return ( - - {/* ---- Shield + Photos group (scales together in Act 3) ---- */} -
- {/* Shield icon */} - - - {/* Padlock at shield base */} -
- -
- - {/* Photos floating around the shield */} - {PHOTOS.map((photo, i) => { - const entrySpring = spring({ - frame: frame - 15 - photo.delay, - fps, - config: SPRING.natural, - }); - - /* Act 1: drift from edges toward center */ - const driftProgress = interpolate(frame, [25, 75], [0, 1], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - easing: EASE.smooth, - }); - - /* Act 2: settle inside shield */ - const settleSpring = spring({ - frame: frame - 110, - fps, - config: SPRING.settle, - }); - - /* Settled positions: clustered near center */ - const settledX = (i - 1) * 40; - const settledY = (i - 1) * 25; - - /* Current position blends drift -> settled */ - const preSettleX = photo.fromX * (1 - driftProgress); - const preSettleY = photo.fromY * (1 - driftProgress); - const photoX = - frame < 110 ? preSettleX : preSettleX + (settledX - preSettleX) * settleSpring; - const photoY = - frame < 110 ? preSettleY : preSettleY + (settledY - preSettleY) * settleSpring; - - /* Rotation: organic drift then settle */ - const driftRotation = (i % 2 === 0 ? 3 : -3) * (1 - driftProgress); - const settledRotation = (i - 1) * 2; - const rotation = - frame < 110 - ? driftRotation - : driftRotation + (settledRotation - driftRotation) * settleSpring; - - /* Act 4: photos drift back out (reverse of arrival) */ - const exitProgress = interpolate(frame, [270, 295], [0, 1], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - easing: EASE.exit, - }); - const exitX = photoX + photo.fromX * exitProgress; - const exitY = photoY + photo.fromY * exitProgress; - const exitOpacity = interpolate(frame, [270, 295], [1, 0], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - easing: EASE.exit, - }); - - /* Entry opacity (photos appear from frame 15) */ - const entryOpacity = interpolate(frame, [15 + photo.delay, 20 + photo.delay], [0, 1], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - }); - - return ( - - ); - })} -
- - {/* ---- "Your images." text (Act 2) ---- */} -
- - Your images. - -
- - {/* ---- "Stay yours." text (Act 2) ---- */} -
- - Stay yours. - -
- - {/* ---- Subtext lines (Act 3) ---- */} - {frame >= 160 && ( -
- - No uploads to the cloud. Ever. - - - 100% local processing. - - - Free. Forever. - -
- )} - - -
- ); -}; diff --git a/apps/videos/src/compositions/hero/ToolGalaxy.tsx b/apps/videos/src/compositions/hero/ToolGalaxy.tsx deleted file mode 100644 index 4724fc45..00000000 --- a/apps/videos/src/compositions/hero/ToolGalaxy.tsx +++ /dev/null @@ -1,495 +0,0 @@ -import { Trail } from "@remotion/motion-blur"; -import type React from "react"; -import { - AbsoluteFill, - Img, - interpolate, - random, - spring, - staticFile, - useCurrentFrame, - useVideoConfig, -} from "remotion"; -import { ClipReveal } from "@/components/ClipReveal"; -import { Counter } from "@/components/Counter"; -import { GrainOverlay } from "@/components/GrainOverlay"; -import { ToolPill } from "@/components/ToolPill"; -import { CATEGORY_LABELS, CATEGORY_ORDER, COLOR } from "@/lib/colors"; -import { TEXT } from "@/lib/fonts"; -import { EASE, SPRING } from "@/lib/motion"; -import { TOOLS } from "@/lib/tools"; - -/* ------------------------------------------------------------------ */ -/* Pre-computed pill data */ -/* ------------------------------------------------------------------ */ - -interface PillData { - index: number; - name: string; - category: string; - categoryIndex: number; - rowInCategory: number; - angle: number; - burstRadius: number; - rotationOffset: number; -} - -const RING_RADII = [160, 210, 260, 310, 360, 410, 460, 510]; - -/** Speed in rad/frame for each ring (inner faster, outer slower) */ -const RING_SPEEDS = RING_RADII.map( - (_, i) => 0.008 - i * ((0.008 - 0.003) / (RING_RADII.length - 1)), -); - -/** Alternating rotation direction: even rings CW, odd rings CCW */ -const RING_DIRECTIONS = RING_RADII.map((_, i) => (i % 2 === 0 ? 1 : -1)); - -/** Category counts for computing per-category row indices */ -const categoryCounts: Record = {}; -for (const cat of CATEGORY_ORDER) { - categoryCounts[cat] = 0; -} - -const PILLS: PillData[] = TOOLS.map((tool, i) => { - const catIdx = CATEGORY_ORDER.indexOf(tool.category as (typeof CATEGORY_ORDER)[number]); - const categoryIndex = catIdx >= 0 ? catIdx : 0; - const rowInCategory = categoryCounts[tool.category] ?? 0; - categoryCounts[tool.category] = rowInCategory + 1; - - return { - index: i, - name: tool.name, - category: tool.category, - categoryIndex, - rowInCategory, - angle: random(`angle-${i}`) * Math.PI * 2, - burstRadius: 200 + random(`radius-${i}`) * 400, - rotationOffset: (random(`rot-${i}`) - 0.5) * 30, - }; -}); - -/* ------------------------------------------------------------------ */ -/* Grid layout constants */ -/* ------------------------------------------------------------------ */ - -const GRID_COLS = CATEGORY_ORDER.length; // 8 -const CELL_W = 120; -const CELL_H = 32; -const CELL_GAP = 8; - -/** Max rows across all categories (AI has 15) */ -const MAX_ROWS = Math.max( - ...CATEGORY_ORDER.map((cat) => TOOLS.filter((t) => t.category === cat).length), -); - -const GRID_TOTAL_W = GRID_COLS * CELL_W + (GRID_COLS - 1) * CELL_GAP; -const GRID_TOTAL_H = MAX_ROWS * CELL_H + (MAX_ROWS - 1) * CELL_GAP; -const GRID_LEFT = (1920 - GRID_TOTAL_W) / 2; -const GRID_TOP = (1080 - GRID_TOTAL_H) / 2 + 30; // offset down slightly for label room - -/* ------------------------------------------------------------------ */ -/* Position helpers */ -/* ------------------------------------------------------------------ */ - -const CX = 1920 / 2; -const CY = 1080 / 2; - -function getBurstPosition( - frame: number, - fps: number, - pill: PillData, -): { x: number; y: number; scale: number; opacity: number; rotation: number } { - const entryFrame = 60 + pill.index * 2; - const s = spring({ - frame: frame - entryFrame, - fps, - config: SPRING.popIn, - }); - - const x = CX + Math.cos(pill.angle) * pill.burstRadius * s; - const y = CY + Math.sin(pill.angle) * pill.burstRadius * s; - - return { - x, - y, - scale: s, - opacity: Math.min(s * 2, 1), - rotation: pill.rotationOffset * (1 - s * 0.5), - }; -} - -function getOrbitPosition( - frame: number, - pill: PillData, -): { x: number; y: number; scale: number; opacity: number; rotation: number } { - const catIdx = pill.categoryIndex; - const ringRadius = RING_RADII[catIdx]; - const ringSpeed = RING_SPEEDS[catIdx]; - const direction = RING_DIRECTIONS[catIdx]; - - // Distribute pills evenly within their ring - const toolsInCat = TOOLS.filter((t) => t.category === pill.category).length; - const baseAngle = (pill.rowInCategory / toolsInCat) * Math.PI * 2; - - // Accumulated angle: integrate speed from frame 210 to current frame - // For simplicity, use frame-based angle with deceleration applied - let orbitAngle: number; - if (frame < 360) { - orbitAngle = baseAngle + (frame - 210) * ringSpeed * direction; - } else { - // Angle accumulated up to frame 360 + decelerating portion - const preDecAngle = (360 - 210) * ringSpeed * direction; - const decProgress = interpolate(frame, [360, 420], [0, 1], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - easing: EASE.exit, - }); - // Deceleration: linearly decreasing speed from full to 0 - // Integral of (1-t) from 0 to decProgress = decProgress - decProgress^2/2 - const decAngle = (frame - 360) * ringSpeed * direction * (1 - decProgress * 0.5); - orbitAngle = baseAngle + preDecAngle + decAngle; - } - - const rawX = Math.cos(orbitAngle) * ringRadius; - const rawY = Math.sin(orbitAngle) * ringRadius * 0.6; // elliptical perspective - - // Depth: pills at "back" of orbit (positive sin component) are dimmer/smaller - const depthFactor = Math.sin(orbitAngle); - const depthOpacity = interpolate(depthFactor, [-1, 1], [1, 0.7]); - const depthScale = interpolate(depthFactor, [-1, 1], [1, 0.85]); - - // Transition from burst to orbit (frame 180-210) - const transitionProgress = interpolate(frame, [180, 210], [0, 1], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - easing: EASE.enter, - }); - - // If still transitioning, we need the burst end position - if (transitionProgress < 1) { - const burstX = CX + Math.cos(pill.angle) * pill.burstRadius; - const burstY = CY + Math.sin(pill.angle) * pill.burstRadius; - const orbitX = CX + rawX; - const orbitY = CY + rawY; - - return { - x: burstX + (orbitX - burstX) * transitionProgress, - y: burstY + (orbitY - burstY) * transitionProgress, - scale: 1 + (depthScale - 1) * transitionProgress, - opacity: 1 + (depthOpacity - 1) * transitionProgress, - rotation: pill.rotationOffset * (1 - transitionProgress), - }; - } - - return { - x: CX + rawX, - y: CY + rawY, - scale: depthScale, - opacity: depthOpacity, - rotation: 0, - }; -} - -function getGridPosition( - frame: number, - fps: number, - pill: PillData, -): { x: number; y: number; scale: number; opacity: number; rotation: number } { - const col = pill.categoryIndex; - const row = pill.rowInCategory; - - const targetX = GRID_LEFT + col * (CELL_W + CELL_GAP) + CELL_W / 2; - const targetY = GRID_TOP + row * (CELL_H + CELL_GAP) + CELL_H / 2; - - // Spring animation from orbit position to grid position - const staggerDelay = pill.index * 1.5; - const settleFrame = 420 + staggerDelay; - const s = spring({ - frame: frame - settleFrame, - fps, - config: SPRING.settle, - }); - - // Get last orbit position (frozen at deceleration end) - const orbitPos = getOrbitPosition(420, pill); - - const x = orbitPos.x + (targetX - orbitPos.x) * s; - const y = orbitPos.y + (targetY - orbitPos.y) * s; - - // Act 5: grid fades to 40% opacity - const gridOpacity = interpolate(frame, [540, 570], [1, 0.4], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - easing: EASE.smooth, - }); - - return { - x, - y, - scale: orbitPos.scale + (1 - orbitPos.scale) * s, - opacity: gridOpacity, - rotation: 0, - }; -} - -function getPillPosition( - frame: number, - fps: number, - pill: PillData, -): { x: number; y: number; scale: number; opacity: number; rotation: number } { - if (frame < 180) return getBurstPosition(frame, fps, pill); - if (frame < 420) return getOrbitPosition(frame, pill); - return getGridPosition(frame, fps, pill); -} - -/* ------------------------------------------------------------------ */ -/* AnimatedPill - renders a single pill at its computed position */ -/* ------------------------------------------------------------------ */ - -const AnimatedPill: React.FC<{ pill: PillData }> = ({ pill }) => { - const frame = useCurrentFrame(); - const { fps } = useVideoConfig(); - const pos = getPillPosition(frame, fps, pill); - - // Don't render before the pill's entry frame - const entryFrame = 60 + pill.index * 2; - if (frame < entryFrame) return null; - - return ( -
- -
- ); -}; - -/* ------------------------------------------------------------------ */ -/* BurstPhase - pills wrapped in Trail for motion blur */ -/* ------------------------------------------------------------------ */ - -const BurstPills: React.FC = () => ( - - {PILLS.map((pill) => ( - - ))} - -); - -/* ------------------------------------------------------------------ */ -/* Category labels for the grid */ -/* ------------------------------------------------------------------ */ - -const CategoryLabels: React.FC = () => { - const frame = useCurrentFrame(); - if (frame < 500) return null; - - return ( - <> - {CATEGORY_ORDER.map((cat, i) => { - const labelX = GRID_LEFT + i * (CELL_W + CELL_GAP) + CELL_W / 2; - const labelY = GRID_TOP - 20; - const catColor = COLOR.category[cat] ?? COLOR.accent; - - // Act 5 grid opacity - const gridOpacity = interpolate(frame, [540, 570], [1, 0.4], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - easing: EASE.smooth, - }); - - return ( -
- - - {CATEGORY_LABELS[cat]} - - -
- ); - })} - - ); -}; - -/* ------------------------------------------------------------------ */ -/* Main composition */ -/* ------------------------------------------------------------------ */ - -export const ToolGalaxy: React.FC = () => { - const frame = useCurrentFrame(); - - /* ================================================================ */ - /* Act 1: Logo Pulse (frame 0-60) */ - /* ================================================================ */ - - const logoOpacity = interpolate(frame, [0, 20], [0, 1], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - }); - - const logoPulseScale = frame >= 30 ? Math.sin(frame * 0.15) * 0.05 + 1 : 1; - - // Logo fades out as burst starts - const logoFadeOut = interpolate(frame, [60, 90], [1, 0], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - }); - - // Radial glow breathes with the pulse - const glowOpacity = frame >= 30 ? 0.15 + Math.sin(frame * 0.15) * 0.05 : 0.05; - - // Ambient radial glow (always present, fades during burst) - const ambientGlow = interpolate(frame, [0, 20], [0.05, 0.15], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - }); - - /* ================================================================ */ - /* Act 5: Counter + Tagline (frame 540-660) */ - /* ================================================================ */ - - const counterOpacity = interpolate(frame, [540, 555], [0, 1], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - }); - - // Counter scale bump: subtle pulse as numbers tick - const counterProgress = interpolate(frame, [540, 590], [0, 48], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - easing: EASE.enter, - }); - const counterBump = frame >= 540 && frame <= 595 ? 1 + Math.sin(counterProgress * 0.8) * 0.03 : 1; - - /* ================================================================ */ - /* Determine rendering phase */ - /* ================================================================ */ - - // During burst (frame 60-180), wrap pills in Trail for motion blur - const inBurstPhase = frame >= 60 && frame < 180; - - return ( - - {/* ---- Radial amber glow behind logo (Act 1) ---- */} - {frame < 90 && ( -
- )} - - {/* ---- Logo (Act 1) ---- */} - {frame < 120 && ( -
- -
- )} - - {/* ---- Pills ---- */} - {frame >= 60 && - (inBurstPhase ? ( - - - - ) : ( - - ))} - - {/* ---- Category labels (Act 4) ---- */} - - - {/* ---- Counter + Tagline (Act 5) ---- */} - {frame >= 540 && ( -
- -
- )} - - {/* ---- Tagline text (Act 5) ---- */} - {frame >= 580 && ( -
- - - {" "} - tools. Zero cloud dependency. - - -
- )} - - - - ); -}; diff --git a/apps/videos/src/compositions/product-demo/ProductDemo.tsx b/apps/videos/src/compositions/product-demo/ProductDemo.tsx deleted file mode 100644 index f29b887b..00000000 --- a/apps/videos/src/compositions/product-demo/ProductDemo.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import type React from "react"; -import { AbsoluteFill, Series, staticFile } from "remotion"; -import { GrainOverlay } from "@/components/GrainOverlay"; -import { BackgroundMusic } from "@/lib/audio"; -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 deleted file mode 100644 index 93d746b2..00000000 --- a/apps/videos/src/compositions/product-demo/scenes/AiToolsScene.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import type React from "react"; -import { ScreenshotScene } from "./ScreenshotScene"; - -export const AiToolsScene: React.FC = () => { - return ( - - ); -}; diff --git a/apps/videos/src/compositions/product-demo/scenes/ApiDocsScene.tsx b/apps/videos/src/compositions/product-demo/scenes/ApiDocsScene.tsx deleted file mode 100644 index bc952aa2..00000000 --- a/apps/videos/src/compositions/product-demo/scenes/ApiDocsScene.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import type React from "react"; -import { ScreenshotScene } from "./ScreenshotScene"; - -export const ApiDocsScene: React.FC = () => { - return ( - - ); -}; diff --git a/apps/videos/src/compositions/product-demo/scenes/BatchProcessingScene.tsx b/apps/videos/src/compositions/product-demo/scenes/BatchProcessingScene.tsx deleted file mode 100644 index 0c188962..00000000 --- a/apps/videos/src/compositions/product-demo/scenes/BatchProcessingScene.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import type React from "react"; -import { ScreenshotScene } from "./ScreenshotScene"; - -export const BatchProcessingScene: React.FC = () => { - return ( - - ); -}; diff --git a/apps/videos/src/compositions/product-demo/scenes/DashboardScene.tsx b/apps/videos/src/compositions/product-demo/scenes/DashboardScene.tsx deleted file mode 100644 index f8389562..00000000 --- a/apps/videos/src/compositions/product-demo/scenes/DashboardScene.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import type React from "react"; -import { ScreenshotScene } from "./ScreenshotScene"; - -export const DashboardScene: React.FC = () => { - return ( - - ); -}; diff --git a/apps/videos/src/compositions/product-demo/scenes/EndCardScene.tsx b/apps/videos/src/compositions/product-demo/scenes/EndCardScene.tsx deleted file mode 100644 index f6ab9e53..00000000 --- a/apps/videos/src/compositions/product-demo/scenes/EndCardScene.tsx +++ /dev/null @@ -1,62 +0,0 @@ -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 deleted file mode 100644 index f8274d35..00000000 --- a/apps/videos/src/compositions/product-demo/scenes/ImageEditorScene.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import type React from "react"; -import { ScreenshotScene } from "./ScreenshotScene"; - -export const ImageEditorScene: React.FC = () => { - return ( - - ); -}; diff --git a/apps/videos/src/compositions/product-demo/scenes/PipelineBuilderScene.tsx b/apps/videos/src/compositions/product-demo/scenes/PipelineBuilderScene.tsx deleted file mode 100644 index cf578879..00000000 --- a/apps/videos/src/compositions/product-demo/scenes/PipelineBuilderScene.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import type React from "react"; -import { ScreenshotScene } from "./ScreenshotScene"; - -export const PipelineBuilderScene: React.FC = () => { - return ( - - ); -}; diff --git a/apps/videos/src/compositions/product-demo/scenes/ScreenshotScene.tsx b/apps/videos/src/compositions/product-demo/scenes/ScreenshotScene.tsx deleted file mode 100644 index 84cf796f..00000000 --- a/apps/videos/src/compositions/product-demo/scenes/ScreenshotScene.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import type React from "react"; -import { AbsoluteFill, Img, interpolate, staticFile, useCurrentFrame } from "remotion"; -import { AppWindow } from "@/components/AppWindow"; -import { ClipReveal } from "@/components/ClipReveal"; -import { TEXT } from "@/lib/fonts"; - -export const ScreenshotScene: React.FC<{ - screenshot: string; - caption?: string; - captionFrame?: number; - zoomFrom?: number; - zoomTo?: number; - panX?: number; - panY?: number; - topBarColor?: string; - bodyColor?: string; -}> = ({ - screenshot, - caption, - captionFrame = 60, - zoomFrom = 1.0, - zoomTo = 1.06, - panX = 0, - panY = 0, - topBarColor = "#1a1a2e", - bodyColor = "#0f172a", -}) => { - const frame = useCurrentFrame(); - const totalFrames = 300; - - const scale = interpolate(frame, [0, totalFrames], [zoomFrom, zoomTo], { - extrapolateRight: "clamp", - }); - const translateX = interpolate(frame, [0, totalFrames], [0, panX], { - extrapolateRight: "clamp", - }); - const translateY = interpolate(frame, [0, totalFrames], [0, panY], { - extrapolateRight: "clamp", - }); - const windowOpacity = interpolate(frame, [0, 15], [0, 1], { - extrapolateRight: "clamp", - }); - - return ( - -
- -
- -
-
-
- - {caption && ( -
- - - {caption} - - -
- )} -
- ); -}; diff --git a/apps/videos/src/compositions/product-demo/scenes/SingleToolScene.tsx b/apps/videos/src/compositions/product-demo/scenes/SingleToolScene.tsx deleted file mode 100644 index e64ad026..00000000 --- a/apps/videos/src/compositions/product-demo/scenes/SingleToolScene.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import type React from "react"; -import { ScreenshotScene } from "./ScreenshotScene"; - -export const SingleToolScene: React.FC = () => { - return ( - - ); -}; diff --git a/apps/videos/src/compositions/promo-teaser/PromoTeaser.tsx b/apps/videos/src/compositions/promo-teaser/PromoTeaser.tsx deleted file mode 100644 index 9e9aec4a..00000000 --- a/apps/videos/src/compositions/promo-teaser/PromoTeaser.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import type React from "react"; -import { AbsoluteFill, Series, staticFile } from "remotion"; -import { GradientBlob } from "@/components/GradientBlob"; -import { GrainOverlay } from "@/components/GrainOverlay"; -import { BackgroundMusic } from "@/lib/audio"; -import { COLOR } from "@/lib/colors"; -import { AmbientOpenScene } from "./scenes/AmbientOpenScene"; -import { CTAScene } from "./scenes/CTAScene"; -import { LogoRevealScene } from "./scenes/LogoRevealScene"; -import { NumberPunchScene } from "./scenes/NumberPunchScene"; -import { TaglineCascadeScene } from "./scenes/TaglineCascadeScene"; - -const BG_BLOBS = [ - { - color: "#f59e0b", - radius: 200, - cx: 300, - cy: 400, - a: 1, - b: 2, - phaseX: 0, - phaseY: 0, - amplitudeX: 60, - amplitudeY: 40, - }, - { - color: "#f97316", - radius: 150, - cx: 750, - cy: 250, - a: 2, - b: 1, - phaseX: 1.2, - phaseY: 0.8, - amplitudeX: 50, - amplitudeY: 60, - }, - { - color: "#d97706", - radius: 180, - cx: 540, - cy: 700, - a: 1, - b: 1, - phaseX: 2.4, - phaseY: 1.6, - amplitudeX: 70, - amplitudeY: 50, - }, -]; - -export const PromoTeaser: React.FC = () => { - return ( - - {BG_BLOBS.map((blob, i) => ( - - ))} - - - - - - - - - - - - - - - - - - - - - - - ); -}; diff --git a/apps/videos/src/compositions/promo-teaser/PromoTeaserVertical.tsx b/apps/videos/src/compositions/promo-teaser/PromoTeaserVertical.tsx deleted file mode 100644 index 53d4f5f0..00000000 --- a/apps/videos/src/compositions/promo-teaser/PromoTeaserVertical.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import type React from "react"; -import { AbsoluteFill, Series } from "remotion"; -import { GradientBlob } from "@/components/GradientBlob"; -import { GrainOverlay } from "@/components/GrainOverlay"; -import { COLOR } from "@/lib/colors"; -import { AmbientOpenScene } from "./scenes/AmbientOpenScene"; -import { CTAScene } from "./scenes/CTAScene"; -import { LogoRevealScene } from "./scenes/LogoRevealScene"; -import { NumberPunchScene } from "./scenes/NumberPunchScene"; -import { TaglineCascadeScene } from "./scenes/TaglineCascadeScene"; - -const BG_BLOBS = [ - { - color: "#f59e0b", - radius: 250, - cx: 540, - cy: 600, - a: 1, - b: 2, - phaseX: 0, - phaseY: 0, - amplitudeX: 80, - amplitudeY: 100, - }, - { - color: "#f97316", - radius: 200, - cx: 300, - cy: 1200, - a: 2, - b: 1, - phaseX: 1.2, - phaseY: 0.8, - amplitudeX: 60, - amplitudeY: 120, - }, - { - color: "#d97706", - radius: 220, - cx: 700, - cy: 1600, - a: 1, - b: 1, - phaseX: 2.4, - phaseY: 1.6, - amplitudeX: 90, - amplitudeY: 80, - }, -]; - -export const PromoTeaserVertical: React.FC = () => { - return ( - - {BG_BLOBS.map((blob, i) => ( - - ))} - - - - - - - - - - - - - - - - - - - - - - ); -}; diff --git a/apps/videos/src/compositions/promo-teaser/scenes/AmbientOpenScene.tsx b/apps/videos/src/compositions/promo-teaser/scenes/AmbientOpenScene.tsx deleted file mode 100644 index 763d1eb2..00000000 --- a/apps/videos/src/compositions/promo-teaser/scenes/AmbientOpenScene.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import type React from "react"; -import { AbsoluteFill, Img, interpolate, staticFile, useCurrentFrame } from "remotion"; -import { GradientBlob } from "@/components/GradientBlob"; - -const BLOBS = [ - { - color: "#f59e0b", - radius: 200, - cx: 300, - cy: 400, - a: 1, - b: 2, - phaseX: 0, - phaseY: 0, - amplitudeX: 60, - amplitudeY: 40, - }, - { - color: "#f97316", - radius: 150, - cx: 750, - cy: 250, - a: 2, - b: 1, - phaseX: 1.2, - phaseY: 0.8, - amplitudeX: 50, - amplitudeY: 60, - }, - { - color: "#d97706", - radius: 180, - cx: 540, - cy: 700, - a: 1, - b: 1, - phaseX: 2.4, - phaseY: 1.6, - amplitudeX: 70, - amplitudeY: 50, - }, -]; - -const SCREENSHOTS = ["dashboard.png", "resize-tool.png", "editor.png"]; -const CYCLE_FRAMES = 30; - -export const AmbientOpenScene: React.FC = () => { - const frame = useCurrentFrame(); - const fadeIn = interpolate(frame, [0, 45], [0, 1], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - }); - - const currentIdx = Math.floor(frame / CYCLE_FRAMES) % SCREENSHOTS.length; - const nextIdx = (currentIdx + 1) % SCREENSHOTS.length; - const cycleProgress = (frame % CYCLE_FRAMES) / CYCLE_FRAMES; - - const currentOpacity = interpolate(cycleProgress, [0, 0.8, 1], [0.6, 0.6, 0], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - }); - const nextOpacity = interpolate(cycleProgress, [0.7, 1], [0, 0.6], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - }); - - const scale = interpolate(frame % CYCLE_FRAMES, [0, CYCLE_FRAMES], [1.05, 1.0], { - extrapolateRight: "clamp", - }); - - const rotation = (currentIdx % 2 === 0 ? 1 : -1) * 3.5; - - return ( - - {BLOBS.map((blob, i) => ( - - ))} - - -
- - -
-
-
- ); -}; diff --git a/apps/videos/src/compositions/promo-teaser/scenes/CTAScene.tsx b/apps/videos/src/compositions/promo-teaser/scenes/CTAScene.tsx deleted file mode 100644 index 63f7124a..00000000 --- a/apps/videos/src/compositions/promo-teaser/scenes/CTAScene.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import type React from "react"; -import { AbsoluteFill, interpolate, spring, useCurrentFrame, useVideoConfig } from "remotion"; -import { COLOR } from "@/lib/colors"; -import { FONT } from "@/lib/fonts"; -import { SPRING } from "@/lib/motion"; - -export const CTAScene: React.FC = () => { - const frame = useCurrentFrame(); - const { fps } = useVideoConfig(); - - const pillScale = spring({ frame, fps, config: SPRING.popIn }); - const urlOpacity = interpolate(frame, [15, 25], [0, 0.7], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - }); - const fadeOut = interpolate(frame, [60, 90], [1, 0], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - }); - const glowOpacity = interpolate(Math.sin(frame * 0.1), [-1, 1], [0.3, 0.6]); - - return ( - -
- Get it free -
- -
- - - snapotter.com - - - ); -}; diff --git a/apps/videos/src/compositions/promo-teaser/scenes/LogoRevealScene.tsx b/apps/videos/src/compositions/promo-teaser/scenes/LogoRevealScene.tsx deleted file mode 100644 index f7d7fc93..00000000 --- a/apps/videos/src/compositions/promo-teaser/scenes/LogoRevealScene.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import type React from "react"; -import { AbsoluteFill } from "remotion"; -import { LogoReveal } from "@/components/LogoReveal"; - -export const LogoRevealScene: React.FC = () => { - return ( - - - - ); -}; diff --git a/apps/videos/src/compositions/promo-teaser/scenes/NumberPunchScene.tsx b/apps/videos/src/compositions/promo-teaser/scenes/NumberPunchScene.tsx deleted file mode 100644 index 1ddaebb9..00000000 --- a/apps/videos/src/compositions/promo-teaser/scenes/NumberPunchScene.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import type React from "react"; -import { AbsoluteFill, interpolate, useCurrentFrame } from "remotion"; -import { NumberPunch } from "@/components/NumberPunch"; - -const PUNCHES = [ - { number: "49", descriptor: "tools", frame: 0 }, - { number: "15", descriptor: "AI models", frame: 37 }, - { number: "55+", descriptor: "formats", frame: 74 }, - { number: "1", descriptor: "container", frame: 111 }, -]; - -export const NumberPunchScene: React.FC = () => { - const frame = useCurrentFrame(); - - return ( - - {PUNCHES.map((p, i) => { - const segmentEnd = i < PUNCHES.length - 1 ? PUNCHES[i + 1].frame : p.frame + 37; - const fadeInOpacity = interpolate(frame, [p.frame, p.frame + 4], [0, 1], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - }); - const fadeOutOpacity = interpolate(frame, [segmentEnd - 6, segmentEnd], [1, 0], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - }); - const isLastPunch = i === PUNCHES.length - 1; - const visible = frame >= p.frame && frame < segmentEnd; - const opacity = isLastPunch ? fadeInOpacity : Math.min(fadeInOpacity, fadeOutOpacity); - - if (!visible) return null; - - return ( - - - - ); - })} - - ); -}; diff --git a/apps/videos/src/compositions/promo-teaser/scenes/TaglineCascadeScene.tsx b/apps/videos/src/compositions/promo-teaser/scenes/TaglineCascadeScene.tsx deleted file mode 100644 index a38b0f88..00000000 --- a/apps/videos/src/compositions/promo-teaser/scenes/TaglineCascadeScene.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import type React from "react"; -import { AbsoluteFill, Img, interpolate, staticFile, useCurrentFrame } from "remotion"; -import { RotatingTaglines } from "@/components/RotatingTaglines"; - -const TAGLINES = [ - "No signups.", - "No uploads.", - "No limits.", - "Free forever.", - "Open source.", - "Fully offline.", -]; - -export const TaglineCascadeScene: React.FC = () => { - const frame = useCurrentFrame(); - const totalFrames = 150; - - const bgScale = interpolate(frame, [0, totalFrames], [1.0, 1.1], { - extrapolateRight: "clamp", - }); - - return ( - - - - - - - - ); -}; diff --git a/apps/videos/src/compositions/showcase/CloudVsLocal.tsx b/apps/videos/src/compositions/showcase/CloudVsLocal.tsx deleted file mode 100644 index 8489838d..00000000 --- a/apps/videos/src/compositions/showcase/CloudVsLocal.tsx +++ /dev/null @@ -1,1417 +0,0 @@ -import { evolvePath } from "@remotion/paths"; -import type React from "react"; -import { - AbsoluteFill, - Img, - interpolate, - random, - spring, - staticFile, - useCurrentFrame, - useVideoConfig, -} from "remotion"; -import { ClipReveal } from "@/components/ClipReveal"; -import { GrainOverlay } from "@/components/GrainOverlay"; -import { ShieldIcon } from "@/components/ShieldIcon"; -import { COLOR } from "@/lib/colors"; -import { FONT, TEXT } from "@/lib/fonts"; -import { EASE, SPRING } from "@/lib/motion"; - -/* ------------------------------------------------------------------ */ -/* Constants */ -/* ------------------------------------------------------------------ */ - -const CX = 1920 / 2; -const CY = 1080 / 2; -const DIVIDER_X = 960; - -/* ------------------------------------------------------------------ */ -/* SVG path helpers */ -/* ------------------------------------------------------------------ */ - -const CLOUD_SVG_PATH = "M6 19a5 5 0 0 1-1-9.9A7 7 0 0 1 19.07 11 4.5 4.5 0 0 1 18 20H6Z"; - -const LOCK_SVG_PATH = - "M5 11V7a5 5 0 0 1 10 0v4M3 11h14a2 2 0 0 1 2 2v6a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2v-6a2 2 0 0 1 2-2Z"; - -const _SERVER_SVG_PATH = "M2 2h16v6H2zM2 10h16v6H2zM6 5h.01M6 13h.01"; - -const _EYE_SVG_PATH = - "M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z M12 9a3 3 0 1 0 0 6 3 3 0 0 0 0-6z"; - -/* ------------------------------------------------------------------ */ -/* Cloud icon */ -/* ------------------------------------------------------------------ */ - -const CloudIcon: React.FC<{ - size: number; - color?: string; - drawProgress?: number; - style?: React.CSSProperties; -}> = ({ size, color = COLOR.danger, drawProgress = 1, style }) => { - const { strokeDasharray, strokeDashoffset } = evolvePath(drawProgress, CLOUD_SVG_PATH); - return ( - - - - ); -}; - -/* ------------------------------------------------------------------ */ -/* Server icon */ -/* ------------------------------------------------------------------ */ - -const ServerIcon: React.FC<{ - size: number; - color?: string; - label?: string; - style?: React.CSSProperties; -}> = ({ size, color = "#6b7280", label, style }) => ( -
- - - - - - - {label && ( - - {label} - - )} -
-); - -/* ------------------------------------------------------------------ */ -/* Question mark badge */ -/* ------------------------------------------------------------------ */ - -const QuestionBadge: React.FC<{ - x: number; - y: number; - scale: number; -}> = ({ x, y, scale }) => ( -
- ? -
-); - -/* ------------------------------------------------------------------ */ -/* Warning badge (triangle with !) */ -/* ------------------------------------------------------------------ */ - -const WarningBadge: React.FC<{ - x: number; - y: number; - scale: number; - color?: string; -}> = ({ x, y, scale, color = COLOR.danger }) => ( -
- - - - ! - - -
-); - -/* ------------------------------------------------------------------ */ -/* Lock icon */ -/* ------------------------------------------------------------------ */ - -const LockIcon: React.FC<{ - size: number; - color?: string; - drawProgress?: number; - style?: React.CSSProperties; -}> = ({ size, color = COLOR.safe, drawProgress = 1, style }) => { - const { strokeDasharray, strokeDashoffset } = evolvePath(drawProgress, LOCK_SVG_PATH); - return ( - - - - ); -}; - -/* ------------------------------------------------------------------ */ -/* Checkmark icon */ -/* ------------------------------------------------------------------ */ - -const Checkmark: React.FC<{ - size: number; - color: string; - scale: number; - style?: React.CSSProperties; -}> = ({ size, color, scale, style }) => ( - - - - -); - -/* ------------------------------------------------------------------ */ -/* Spinner */ -/* ------------------------------------------------------------------ */ - -const Spinner: React.FC<{ - color: string; - size: number; - style?: React.CSSProperties; -}> = ({ color, size, style }) => { - const frame = useCurrentFrame(); - const rotation = frame * 18; // 6 deg/frame => 180 deg/s - const circumference = Math.PI * (size - 4); - return ( - - - - ); -}; - -/* ------------------------------------------------------------------ */ -/* Eye icon (watching) */ -/* ------------------------------------------------------------------ */ - -const EyeIcon: React.FC<{ - x: number; - y: number; - opacity: number; -}> = ({ x, y, opacity }) => { - const frame = useCurrentFrame(); - // Blink: close eye briefly every ~40 frames - const blinkPhase = frame % 40; - const eyeScale = blinkPhase >= 0 && blinkPhase < 3 ? 0.3 : 1; - - return ( - - - - - ); -}; - -/* ------------------------------------------------------------------ */ -/* Cloud server positions (pre-computed for scatter) */ -/* ------------------------------------------------------------------ */ - -const CLOUD_SERVERS = [ - { x: 180, y: 340, label: "" }, - { x: 340, y: 420, label: "" }, - { x: 580, y: 300, label: "" }, -]; - -const EXTRA_SERVERS = [ - { x: 120, y: 500, label: "" }, - { x: 450, y: 540, label: "" }, - { x: 680, y: 460, label: "" }, - { x: 280, y: 580, label: "" }, -]; - -/* Labeled nodes for Act 3 */ -const THIRD_PARTY_NODES = [ - { x: 620, y: 540, label: "3rd Party", color: "#6b7280" }, - { x: 150, y: 600, label: "Analytics", color: "#6b7280" }, - { x: 420, y: 640, label: "Training Data", color: "#d97706" }, -]; - -/* ------------------------------------------------------------------ */ -/* Main composition */ -/* ------------------------------------------------------------------ */ - -export const CloudVsLocal: React.FC = () => { - const frame = useCurrentFrame(); - const { fps } = useVideoConfig(); - - /* ================================================================ */ - /* Act 1: Split Establishment (frame 0-40) */ - /* ================================================================ */ - - /* Divider line draws top to bottom */ - const dividerPath = `M${DIVIDER_X} 0 L${DIVIDER_X} 1080`; - const dividerProgress = interpolate(frame, [0, 15], [0, 1], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - easing: EASE.enter, - }); - const dividerEvolve = evolvePath(dividerProgress, dividerPath); - - /* Tint overlays */ - const leftTintOpacity = interpolate(frame, [15, 25], [0, 0.03], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - }); - const rightTintOpacity = interpolate(frame, [15, 25], [0, 0.03], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - }); - - /* Cloud icon draw */ - const cloudDrawProgress = interpolate(frame, [25, 35], [0, 1], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - easing: EASE.enter, - }); - - /* ================================================================ */ - /* Act 2: Single Photo Comparison (frame 40-160) */ - /* ================================================================ */ - - /* -- LEFT SIDE -- */ - - /* Photo appears */ - const leftPhotoOpacity = interpolate(frame, [40, 48], [0, 1], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - }); - - /* Photo moves to cloud: follows curved path upward */ - const leftUploadProgress = interpolate(frame, [55, 80], [0, 1], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - easing: EASE.smooth, - }); - const leftPhotoX = interpolate(leftUploadProgress, [0, 0.5, 1], [200, 350, 400]); - const leftPhotoY = interpolate(leftUploadProgress, [0, 0.5, 1], [700, 400, 160]); - - /* Cloud flash when photo arrives */ - const cloudFlashOpacity = - frame >= 80 && frame < 88 - ? interpolate(frame, [80, 84, 88], [0, 0.4, 0], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - }) - : 0; - - /* Scatter copies to servers */ - const scatterProgress = interpolate(frame, [85, 110], [0, 1], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - easing: EASE.enter, - }); - - /* Question marks pop in */ - const questionMarks = CLOUD_SERVERS.map((server, i) => { - const qStart = 110 + i * 6; - const qSpring = spring({ - frame: frame - qStart, - fps, - config: SPRING.popIn, - }); - return { ...server, scale: qSpring }; - }); - - /* Eye icon */ - const eyeOpacity = interpolate(frame, [118, 125], [0, 0.7], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - }); - - /* "who has your data?" */ - const leftLabelOpacity = interpolate(frame, [130, 145], [0, 0.4], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - }); - - /* -- RIGHT SIDE -- */ - - /* Photo appears */ - const rightPhotoOpacity = interpolate(frame, [40, 48], [0, 1], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - }); - - /* Photo moves to local server: curves downward */ - const rightDownloadProgress = interpolate(frame, [55, 80], [0, 1], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - easing: EASE.smooth, - }); - const rightPhotoX = interpolate(rightDownloadProgress, [0, 0.5, 1], [1400, 1350, 1400]); - const rightPhotoY = interpolate(rightDownloadProgress, [0, 0.5, 1], [300, 500, 620]); - - /* Processing spinner -> checkmark */ - const showSpinner = frame >= 80 && frame < 100; - const checkmarkSpring = spring({ - frame: frame - 100, - fps, - config: SPRING.popIn, - }); - const showCheckmark = frame >= 100; - - /* Lock icon draws in */ - const lockProgress = interpolate(frame, [100, 125], [0, 1], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - easing: EASE.enter, - }); - - /* "stays on your machine" */ - const rightLabelOpacity = interpolate(frame, [130, 145], [0, 0.7], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - }); - - /* Shield outline */ - const _shieldDrawProgress = interpolate(frame, [135, 160], [0, 1], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - easing: EASE.enter, - }); - - /* ================================================================ */ - /* Act 3: Escalation (frame 160-320) */ - /* ================================================================ */ - - /* Left: extra photos upload (staggered) */ - const extraPhotoCount = 4; - const extraPhotos = Array.from({ length: extraPhotoCount }, (_, i) => { - const start = 160 + i * 6; - const progress = interpolate(frame, [start, start + 20], [0, 1], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - easing: EASE.smooth, - }); - const hue = 20 + random(`extra-hue-${i}`) * 60; - return { progress, hue, index: i }; - }); - - /* Extra server appearances */ - const extraServerAppearances = EXTRA_SERVERS.map((server, i) => { - const start = 175 + i * 8; - const s = spring({ - frame: frame - start, - fps, - config: SPRING.snappy, - }); - return { ...server, scale: s }; - }); - - /* 3rd party node connections */ - const thirdPartyNodes = THIRD_PARTY_NODES.map((node, i) => { - const start = 200 + i * 15; - const drawProg = interpolate(frame, [start, start + 20], [0, 1], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - easing: EASE.enter, - }); - const labelOpacity = interpolate(frame, [start + 15, start + 25], [0, 1], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - }); - return { ...node, drawProgress: drawProg, labelOpacity }; - }); - - /* ToS scroll */ - const tosHeight = interpolate(frame, [220, 260], [0, 100], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - easing: EASE.smooth, - }); - - /* Warning badges */ - const warningBadges = [ - { x: 160, y: 360, start: 265 }, - { x: 360, y: 440, start: 272 }, - { x: 560, y: 320, start: 278 }, - { x: 110, y: 520, start: 284 }, - { x: 440, y: 560, start: 290 }, - ].map((badge) => { - const s = spring({ - frame: frame - badge.start, - fps, - config: SPRING.popIn, - }); - return { ...badge, scale: s }; - }); - - /* Left side red tint escalation (starts at 5%, escalates to 20% by Act 4) */ - const escalatedLeftTint = interpolate(frame, [160, 320], [0.05, 0.2], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - }); - - /* Shake animation (frame 300-320) */ - const shakeX = frame >= 300 && frame <= 320 ? Math.sin(frame * 2.5) * 5 : 0; - - /* Right: extra photos arrive calmly */ - const rightExtraPhotos = Array.from({ length: 4 }, (_, i) => { - const start = 165 + i * 20; - const progress = interpolate(frame, [start, start + 25], [0, 1], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - easing: EASE.smooth, - }); - const checkSpring = spring({ - frame: frame - (start + 28), - fps, - config: SPRING.popIn, - }); - const hue = 140 + random(`right-hue-${i}`) * 60; - return { progress, checkScale: checkSpring, hue, index: i }; - }); - - /* Shield breathing */ - const shieldBreathing = frame >= 290 ? 1 + Math.sin((frame - 290) * 0.08) * 0.01 : 1; - - /* ================================================================ */ - /* Act 4: Contrast Peak (frame 320-420) */ - /* ================================================================ */ - - /* Left: deeper red tint */ - const peakLeftTint = interpolate(frame, [320, 360], [0.2, 0.25], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - }); - - /* Left vibrate */ - const vibrateX = frame >= 320 && frame < 420 ? Math.sin(frame * 3) * 3 : 0; - - /* Right glow */ - const rightGlowOpacity = interpolate(frame, [360, 400], [0, 0.12], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - }); - - /* ================================================================ */ - /* Act 5: Resolution (frame 420-540) */ - /* ================================================================ */ - - /* Left half slides off */ - const leftSlideX = interpolate(frame, [420, 470], [0, -960], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - easing: EASE.exit, - }); - const leftFadeOut = interpolate(frame, [420, 460], [1, 0], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - }); - - /* Divider slides with left */ - const dividerSlideX = interpolate(frame, [420, 470], [0, -960], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - easing: EASE.exit, - }); - - /* Right side expands to center */ - const rightExpandX = interpolate(frame, [450, 500], [960, 0], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - easing: EASE.enter, - }); - - /* Full-width shield */ - const fullShieldOpacity = interpolate(frame, [470, 500], [0, 0.1], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - }); - - /* Logo fade in */ - const logoOpacity = interpolate(frame, [520, 535], [0, 1], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - }); - - /* Compute effective left tint for current frame */ - const effectiveLeftTint = - frame < 160 ? leftTintOpacity : frame < 320 ? escalatedLeftTint : peakLeftTint; - - return ( - - {/* ================================================================ */} - {/* LEFT HALF: Cloud Services */} - {/* ================================================================ */} -
- {/* Red tint */} -
- - {/* Title */} - {frame >= 25 && ( -
- - - - Cloud Services - - -
- )} - - {/* Cloud icon at top center */} -
- - {/* Cloud flash */} -
-
- - {/* Eye icon above cloud */} - {frame >= 115 && } - - {/* Photo thumbnail (Act 2) */} - {frame >= 40 && frame < 85 && ( - - )} - - {/* Upload arrow path */} - {frame >= 55 && frame < 85 && ( - - - - )} - - {/* Scattered servers */} - {frame >= 85 && - CLOUD_SERVERS.map((server, i) => ( -
- -
- ))} - - {/* Scattered photo copies to servers */} - {frame >= 85 && - CLOUD_SERVERS.map((server, i) => { - const copyX = interpolate(scatterProgress, [0, 1], [400, server.x]); - const copyY = interpolate(scatterProgress, [0, 1], [160, server.y - 30]); - return ( - - ); - })} - - {/* Dotted trail lines from cloud to servers */} - {frame >= 90 && ( - - {CLOUD_SERVERS.map((server, i) => ( - - ))} - - )} - - {/* Question marks */} - {frame >= 110 && - questionMarks.map((q, i) => ( - - ))} - - {/* "who has your data?" label */} - {frame >= 130 && ( -
- - who has your data? - -
- )} - - {/* Act 3: Extra photos uploading */} - {frame >= 160 && - extraPhotos.map((photo) => { - const px = interpolate(photo.progress, [0, 1], [100 + photo.index * 60, 400]); - const py = interpolate(photo.progress, [0, 1], [750, 160]); - if (photo.progress <= 0) return null; - return ( - - ); - })} - - {/* Extra servers */} - {frame >= 175 && - extraServerAppearances.map((server, i) => ( -
- -
- ))} - - {/* 3rd party nodes with connections */} - {frame >= 200 && ( - - {thirdPartyNodes.map((node, i) => { - const sourceServer = i < EXTRA_SERVERS.length ? EXTRA_SERVERS[i] : CLOUD_SERVERS[0]; - return ( - - ); - })} - - )} - - {frame >= 200 && - thirdPartyNodes.map((node, i) => ( -
-
- - {node.label} - -
-
- ))} - - {/* ToS scroll */} - {frame >= 220 && tosHeight > 0 && ( -
- {Array.from({ length: 12 }, (_, i) => ( -
- ))} -
- )} - - {/* Warning badges */} - {frame >= 260 && - warningBadges.map((badge, i) => ( - = 3 ? COLOR.danger : "#d97706"} - /> - ))} - - {/* Act 4: "where did your data go?" */} - {frame >= 320 && frame < 420 && ( -
- - - where did your data go? - - -
- )} -
- - {/* ================================================================ */} - {/* RIGHT HALF: SnapOtter Local */} - {/* ================================================================ */} -
= 420 ? rightExpandX : DIVIDER_X, - top: 0, - width: frame >= 450 ? 1920 : DIVIDER_X, - height: 1080, - overflow: "hidden", - }} - > - {/* Green tint */} -
- - {/* Title */} - {frame >= 30 && frame < 470 && ( -
= 450 ? CX - rightExpandX : DIVIDER_X / 2, - top: 50, - transform: "translateX(-50%)", - display: "flex", - alignItems: "center", - gap: 10, - }} - > - = 35 ? 1 : 0 }} - /> - - - SnapOtter - - -
- )} - - {/* Local server icon */} -
= 450 ? CX - rightExpandX : DIVIDER_X / 2, - top: 620, - transform: `translate(-50%, -50%) scale(${shieldBreathing})`, - }} - > -
- - - Local Server - -
- - {/* Right glow behind server (Act 4) */} - {frame >= 360 && ( -
- )} -
- - {/* First photo (Act 2) */} - {frame >= 40 && frame < 80 && ( - - )} - - {/* Download arrow path */} - {frame >= 55 && frame < 80 && ( - - - - )} - - {/* Processing spinner */} - {showSpinner && ( -
= 450 ? CX - rightExpandX : DIVIDER_X / 2) - 12, - top: 560, - }} - > - -
- )} - - {/* Checkmark */} - {showCheckmark && ( -
= 450 ? CX - rightExpandX : DIVIDER_X / 2) - 12, - top: 560, - }} - > - -
- )} - - {/* Lock icon */} - {frame >= 100 && ( -
= 450 ? CX - rightExpandX : DIVIDER_X / 2) - 10, - top: 520, - }} - > - -
- )} - - {/* "stays on your machine" label */} - {frame >= 130 && frame < 470 && ( -
= 450 ? CX - rightExpandX : DIVIDER_X / 2, - top: 780, - transform: "translateX(-50%)", - opacity: rightLabelOpacity, - textAlign: "center", - }} - > - - stays on your machine - -
- )} - - {/* Shield outline */} - {frame >= 135 && frame < 470 && ( -
= 450 ? CX - rightExpandX : DIVIDER_X / 2) - 100, - top: 400, - opacity: 0.2, - transform: `scale(${shieldBreathing})`, - }} - > - -
- )} - - {/* Right extra photos (Act 3) */} - {frame >= 165 && - rightExtraPhotos.map((photo) => { - if (photo.progress <= 0) return null; - const px = DIVIDER_X / 2 - 80 + photo.index * 40; - const py = interpolate(photo.progress, [0, 1], [400, 680]); - return ( -
- - {photo.checkScale > 0.1 && ( -
- -
- )} -
- ); - })} - - {/* "Air-gapped ready" label (Act 3) */} - {frame >= 260 && frame < 470 && ( -
= 450 ? CX - rightExpandX : DIVIDER_X / 2, - top: 820, - transform: "translateX(-50%)", - textAlign: "center", - }} - > - - - Air-gapped ready - - -
- )} - - {/* GDPR/HIPAA badge */} - {frame >= 270 && frame < 470 && ( -
= 450 ? CX - rightExpandX : DIVIDER_X / 2, - top: 850, - transform: "translateX(-50%)", - }} - > - -
- - GDPR / HIPAA - -
-
-
- )} -
- - {/* ================================================================ */} - {/* DIVIDER LINE */} - {/* ================================================================ */} - {frame < 470 && ( - - - - )} - - {/* ================================================================ */} - {/* Act 5: Full-width shield overlay */} - {/* ================================================================ */} - {frame >= 470 && ( -
- -
- )} - - {/* ================================================================ */} - {/* Act 5: Hero tagline */} - {/* ================================================================ */} - {frame >= 500 && ( -
- - Your images never leave your network. - -
- )} - - {/* ================================================================ */} - {/* Act 5: Logo */} - {/* ================================================================ */} - {frame >= 520 && ( -
- -
- )} - - - - ); -}; diff --git a/apps/videos/src/compositions/showcase/The48.tsx b/apps/videos/src/compositions/showcase/The48.tsx deleted file mode 100644 index bbcd7a80..00000000 --- a/apps/videos/src/compositions/showcase/The48.tsx +++ /dev/null @@ -1,667 +0,0 @@ -import { Trail } from "@remotion/motion-blur"; -import type React from "react"; -import { - AbsoluteFill, - interpolate, - Sequence, - spring, - useCurrentFrame, - useVideoConfig, -} from "remotion"; -import { ClipReveal } from "@/components/ClipReveal"; -import { GrainOverlay } from "@/components/GrainOverlay"; -import { ToolPill } from "@/components/ToolPill"; -import { CATEGORY_LABELS, CATEGORY_ORDER, COLOR } from "@/lib/colors"; -import { TEXT } from "@/lib/fonts"; -import { EASE, SPRING } from "@/lib/motion"; -import { getToolsByCategory } from "@/lib/tools"; - -/* ------------------------------------------------------------------ */ -/* Grid layout constants (from spec) */ -/* ------------------------------------------------------------------ */ - -const COLS = 8; -const ROWS = 6; -const CELL_W = 190; -const CELL_H = 44; -const GAP = 8; -const GRID_W = COLS * CELL_W + (COLS - 1) * GAP; // 1576 -const GRID_H = ROWS * CELL_H + (ROWS - 1) * GAP; // 304 -const GRID_X = (1920 - GRID_W) / 2; -const GRID_Y = (1080 - GRID_H) / 2 + 40; - -const CX = 1920 / 2; -const CY = 1080 / 2; -const FRAMES_PER_TOOL = 10; - -/* ------------------------------------------------------------------ */ -/* Pre-compute tool order and grid positions */ -/* ------------------------------------------------------------------ */ - -interface ToolEntry { - index: number; - name: string; - category: string; - col: number; - row: number; - gridX: number; - gridY: number; -} - -const TOOL_ENTRIES: ToolEntry[] = []; -const colCounts: number[] = new Array(COLS).fill(0); - -for (const cat of CATEGORY_ORDER) { - const colIdx = CATEGORY_ORDER.indexOf(cat); - const tools = getToolsByCategory(cat); - for (const tool of tools) { - const row = colCounts[colIdx]; - colCounts[colIdx] = row + 1; - TOOL_ENTRIES.push({ - index: TOOL_ENTRIES.length, - name: tool.name, - category: tool.category, - col: colIdx, - row, - gridX: GRID_X + colIdx * (CELL_W + GAP) + CELL_W / 2, - gridY: GRID_Y + row * (CELL_H + GAP) + CELL_H / 2, - }); - } -} - -/* ------------------------------------------------------------------ */ -/* Micro-visualization categories */ -/* ------------------------------------------------------------------ */ - -type MicroViz = "scale" | "clip" | "rotate" | "sparkle" | "squeeze" | "stamp" | "badge" | "pulse"; - -function getMicroViz(name: string, category: string): MicroViz { - const lower = name.toLowerCase(); - if (lower.includes("resize") || lower.includes("upscal")) return "scale"; - if (lower.includes("crop") || lower.includes("split")) return "clip"; - if (lower.includes("rotate") || lower.includes("flip")) return "rotate"; - if ( - lower.includes("convert") || - lower.includes("svg") || - lower.includes("pdf") || - lower.includes("gif") - ) - return "badge"; - if (lower.includes("compress") || lower.includes("optimi")) return "squeeze"; - if (lower.includes("watermark") || lower.includes("stamp") || lower.includes("overlay")) - return "stamp"; - if (category === "ai") return "sparkle"; - return "pulse"; -} - -/* ------------------------------------------------------------------ */ -/* Entry direction helper */ -/* ------------------------------------------------------------------ */ - -type Direction = - | "left" - | "right" - | "top" - | "bottom" - | "diag-tl" - | "diag-tr" - | "diag-bl" - | "diag-br"; - -function getEntryDirection(index: number): Direction { - const dirs: Direction[] = [ - "left", - "right", - "top", - "bottom", - "diag-tl", - "diag-tr", - "diag-bl", - "diag-br", - ]; - return dirs[index % dirs.length]; -} - -function getEntryPosition(dir: Direction): { x: number; y: number } { - switch (dir) { - case "left": - return { x: -200, y: CY }; - case "right": - return { x: 1920 + 200, y: CY }; - case "top": - return { x: CX, y: -80 }; - case "bottom": - return { x: CX, y: 1080 + 80 }; - case "diag-tl": - return { x: -150, y: -80 }; - case "diag-tr": - return { x: 1920 + 150, y: -80 }; - case "diag-bl": - return { x: -150, y: 1080 + 80 }; - case "diag-br": - return { x: 1920 + 150, y: 1080 + 80 }; - } -} - -/* ------------------------------------------------------------------ */ -/* Sparkle dots component */ -/* ------------------------------------------------------------------ */ - -const SparkleDots: React.FC<{ progress: number }> = ({ progress }) => { - const offsets = [ - { dx: -16, dy: -16 }, - { dx: 16, dy: -12 }, - { dx: -12, dy: 16 }, - { dx: 18, dy: 14 }, - ]; - const spread = interpolate(progress, [0, 1], [0, 1]); - const opacity = interpolate(progress, [0, 0.3, 1], [0, 1, 0], { - extrapolateRight: "clamp", - }); - - return ( - <> - {offsets.map((o) => ( -
- ))} - - ); -}; - -/* ------------------------------------------------------------------ */ -/* AnimatedToolPill -- self-contained 15-frame animation */ -/* ------------------------------------------------------------------ */ - -const AnimatedToolPill: React.FC<{ tool: ToolEntry }> = ({ tool }) => { - const frame = useCurrentFrame(); - const { fps } = useVideoConfig(); - - const dir = getEntryDirection(tool.index); - const entry = getEntryPosition(dir); - const viz = getMicroViz(tool.name, tool.category); - - // Phase 1: entrance (frame 0-2) -- edge to center - // Phase 2: center flash (frame 2-6) -- micro-visualization (4 frames for visibility) - // Phase 3: grid snap (frame 6-10) -- center to grid position - - let x: number; - let y: number; - let extraTransform = ""; - let clipPath: string | undefined; - let sparkleProgress = 0; - let showSparkle = false; - - if (frame < 2) { - // Entrance: edge to center - const t = interpolate(frame, [0, 2], [0, 1], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - easing: EASE.snap, - }); - x = entry.x + (CX - entry.x) * t; - y = entry.y + (CY - entry.y) * t; - } else if (frame < 6) { - // Center flash with micro-visualization (4 frames) - x = CX; - y = CY; - const vizProgress = interpolate(frame, [2, 6], [0, 1], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - }); - - switch (viz) { - case "scale": { - const s = interpolate(vizProgress, [0, 0.5, 1], [1.3, 0.8, 1.0]); - extraTransform = ` scale(${s})`; - break; - } - case "clip": { - const inset = interpolate(vizProgress, [0, 0.5, 1], [0, 15, 0]); - clipPath = `inset(${inset}% ${inset}% ${inset}% ${inset}%)`; - break; - } - case "rotate": { - const deg = interpolate(vizProgress, [0, 1], [0, 90]); - extraTransform = ` rotate(${deg}deg)`; - break; - } - case "badge": { - const s = interpolate(vizProgress, [0, 0.3, 0.7, 1], [0.9, 1.15, 1.15, 1.0]); - extraTransform = ` scale(${s})`; - break; - } - case "squeeze": { - const sx = interpolate(vizProgress, [0, 0.5, 1], [1, 0.7, 1]); - const sy = interpolate(vizProgress, [0, 0.5, 1], [1, 1.2, 1]); - extraTransform = ` scaleX(${sx}) scaleY(${sy})`; - break; - } - case "stamp": { - const ty = interpolate(vizProgress, [0, 0.4, 0.6, 1], [-20, 4, -2, 0]); - const s = interpolate(vizProgress, [0, 0.4, 0.6, 1], [1.2, 0.95, 1.02, 1]); - extraTransform = ` translateY(${ty}px) scale(${s})`; - break; - } - case "sparkle": { - showSparkle = true; - sparkleProgress = vizProgress; - const s = interpolate(vizProgress, [0, 0.5, 1], [1.0, 1.1, 1.0]); - extraTransform = ` scale(${s})`; - break; - } - default: { - const s = interpolate(vizProgress, [0, 0.5, 1], [1.0, 1.1, 1.0]); - extraTransform = ` scale(${s})`; - break; - } - } - } else { - // Grid snap: center to grid position via spring - const s = spring({ - frame: frame - 6, - fps, - config: SPRING.settle, - }); - x = CX + (tool.gridX - CX) * s; - y = CY + (tool.gridY - CY) * s; - } - - return ( -
- - {showSparkle && } -
- ); -}; - -/* ------------------------------------------------------------------ */ -/* Entrance phase wrapper -- uses Trail for motion blur frames 0-3 */ -/* ------------------------------------------------------------------ */ - -const ToolEntrance: React.FC<{ tool: ToolEntry }> = ({ tool }) => { - const frame = useCurrentFrame(); - const inMotion = frame < 6; - - if (inMotion) { - return ( - - - - - - ); - } - - return ; -}; - -/* ------------------------------------------------------------------ */ -/* Running counter (top-right, amber) */ -/* ------------------------------------------------------------------ */ - -const RunningCounter: React.FC = () => { - const frame = useCurrentFrame(); - - // Counter shows how many tools have landed (reached their local sequence end) - let landed = 0; - for (let i = 0; i < TOOL_ENTRIES.length; i++) { - const landFrame = 30 + i * FRAMES_PER_TOOL + FRAMES_PER_TOOL; - if (frame >= landFrame) landed = i + 1; - } - - // Don't show counter before any tool has started - if (frame < 40) return null; - - // During Act 3 (frame 520-560), counter transitions to "48 tools" and moves to center-top - const isAct3Text = frame >= 520; - - // Fade in - const opacity = interpolate(frame, [40, 45], [0, 1], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - }); - - if (isAct3Text) { - const moveProgress = interpolate(frame, [520, 550], [0, 1], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - easing: EASE.enter, - }); - const xPos = interpolate(moveProgress, [0, 1], [1920 - 100, CX]); - const yPos = interpolate(moveProgress, [0, 1], [40, 30]); - - return ( -
- - 48 tools - -
- ); - } - - return ( -
- - {landed} - -
- ); -}; - -/* ------------------------------------------------------------------ */ -/* Grid glow wave (Act 3, frame 750-770) */ -/* ------------------------------------------------------------------ */ - -const GridGlowWave: React.FC = () => { - const frame = useCurrentFrame(); - - if (frame < 500 || frame > 530) return null; - - return ( - <> - {CATEGORY_ORDER.map((cat, colIdx) => { - const glowStart = 500 + colIdx * 3; - const glowOpacity = interpolate( - frame, - [glowStart, glowStart + 6, glowStart + 12], - [0, 0.2, 0], - { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - }, - ); - - const colX = GRID_X + colIdx * (CELL_W + GAP); - - return ( -
- ); - })} - - ); -}; - -/* ------------------------------------------------------------------ */ -/* Category labels (Act 3, frame 770-810) */ -/* ------------------------------------------------------------------ */ - -const CategoryLabelsRow: React.FC = () => { - const frame = useCurrentFrame(); - - if (frame < 520) return null; - - // Act 4 fade - const act4Opacity = interpolate(frame, [560, 570], [1, 0.6], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - }); - - return ( - <> - {CATEGORY_ORDER.map((cat, i) => { - const labelX = GRID_X + i * (CELL_W + GAP) + CELL_W / 2; - const labelY = GRID_Y - 24; - const catColor = COLOR.category[cat] ?? COLOR.accent; - - return ( -
- - - {CATEGORY_LABELS[cat]} - - -
- ); - })} - - ); -}; - -/* ------------------------------------------------------------------ */ -/* Main composition */ -/* ------------------------------------------------------------------ */ - -export const The48: React.FC = () => { - const frame = useCurrentFrame(); - const { fps } = useVideoConfig(); - - /* ================================================================ */ - /* Act 1: Title (frame 0-30) */ - /* ================================================================ */ - - const titleScale = - frame < 30 - ? interpolate(spring({ frame, fps, config: SPRING.settle }), [0, 1], [1.1, 1.0]) - : 1.0; - - // Title moves to top and shrinks during frames 30-60 - const titleY = interpolate(frame, [0, 30, 60], [CY - 48, CY - 48, 60], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - easing: EASE.enter, - }); - - const titleFontSize = interpolate(frame, [30, 60], [96, 48], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - easing: EASE.enter, - }); - - // Title as persistent header after frame 30 (fades down to subtle) - const headerOpacity = - frame >= 30 - ? interpolate(frame, [30, 60], [1, 0.7], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - }) - : 0; - - // During Act 4, header fades out - const headerAct4Opacity = - frame >= 560 - ? interpolate(frame, [560, 570], [0.7, 0], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - }) - : headerOpacity; - - /* ================================================================ */ - /* Act 3: Grid pulse (frame 500-520) */ - /* ================================================================ */ - - const gridPulseScale = - frame >= 500 && frame < 520 - ? 1 + - 0.015 * - Math.sin( - interpolate(frame, [500, 520], [0, Math.PI], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - }), - ) - : 1.0; - - /* ================================================================ */ - /* Act 4: Grid fades, tagline (frame 560-600) */ - /* ================================================================ */ - - const gridOpacity = - frame >= 560 - ? interpolate(frame, [560, 575], [1, 0.6], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - }) - : 1; - - return ( - - {/* ---- Act 1: Title ---- */} - {frame < 30 && ( -
- - - 48 tools. - - -
- )} - - {/* ---- Title as header (frame 30+) ---- */} - {frame >= 30 && frame < 570 && ( -
- - 48 tools. - -
- )} - - {/* ---- Act 2: Rapid montage ---- */} -
- {TOOL_ENTRIES.map((tool) => ( - - - - ))} -
- - {/* ---- Grid glow wave (Act 3) ---- */} - - - {/* ---- Category labels (Act 3) ---- */} -
- -
- - {/* ---- Running counter ---- */} - - - {/* ---- Act 4: Tagline ---- */} - {frame >= 560 && ( -
- - - One container. Zero cloud. - - -
- )} - - -
- ); -}; diff --git a/apps/videos/src/compositions/x-launch/XLaunchVideo.tsx b/apps/videos/src/compositions/x-launch/XLaunchVideo.tsx deleted file mode 100644 index 99be15d8..00000000 --- a/apps/videos/src/compositions/x-launch/XLaunchVideo.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import type React from "react"; -import { AbsoluteFill, Series, staticFile } from "remotion"; -import { GrainOverlay } from "@/components/GrainOverlay"; -import { BackgroundMusic } from "@/lib/audio"; -import { COLOR } from "@/lib/colors"; -import { AiShowcaseScene } from "./scenes/AiShowcaseScene"; -import { FeatureBurstScene } from "./scenes/FeatureBurstScene"; -import { GitHubCTAScene } from "./scenes/GitHubCTAScene"; -import { HookScene } from "./scenes/HookScene"; -import { PrivacyBeatScene } from "./scenes/PrivacyBeatScene"; -import { TerminalInstallScene } from "./scenes/TerminalInstallScene"; -import { ToolGridRevealScene } from "./scenes/ToolGridRevealScene"; - -export const XLaunchVideo: React.FC = () => { - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); -}; diff --git a/apps/videos/src/compositions/x-launch/scenes/AiShowcaseScene.tsx b/apps/videos/src/compositions/x-launch/scenes/AiShowcaseScene.tsx deleted file mode 100644 index d834ec6c..00000000 --- a/apps/videos/src/compositions/x-launch/scenes/AiShowcaseScene.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import type React from "react"; -import { AbsoluteFill, Img, interpolate, staticFile, useCurrentFrame } from "remotion"; -import { FONT } from "@/lib/fonts"; - -export const AiShowcaseScene: React.FC = () => { - const frame = useCurrentFrame(); - const totalFrames = 180; - - const crossfade = interpolate(frame, [80, 100], [0, 1], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - }); - - const scale1 = interpolate(frame, [0, 90], [1.0, 1.05], { - extrapolateRight: "clamp", - }); - const scale2 = interpolate(frame, [90, totalFrames], [1.0, 1.05], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - }); - - const textOpacity = interpolate(frame, [30, 45], [0, 1], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - }); - - return ( - - - - - - - - - -
- 15 AI models. Your hardware. No cloud. -
-
- ); -}; diff --git a/apps/videos/src/compositions/x-launch/scenes/FeatureBurstScene.tsx b/apps/videos/src/compositions/x-launch/scenes/FeatureBurstScene.tsx deleted file mode 100644 index 1d5d945b..00000000 --- a/apps/videos/src/compositions/x-launch/scenes/FeatureBurstScene.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import type React from "react"; -import { AbsoluteFill } from "remotion"; -import { FeaturePill } from "@/components/FeaturePill"; - -const FEATURES = [ - { label: "Batch Processing", col: 0, row: 0, from: "left" as const }, - { label: "Pipeline Automation", col: 1, row: 0, from: "right" as const }, - { label: "REST API", col: 0, row: 1, from: "left" as const }, - { label: "55+ Input Formats", col: 1, row: 1, from: "right" as const }, - { label: "Image Editor", col: 0, row: 2, from: "left" as const }, - { label: "One Container", col: 1, row: 2, from: "right" as const }, - { label: "Multi-arch", col: 0, row: 3, from: "left" as const }, - { label: "15 AI Models", col: 1, row: 3, from: "right" as const }, -]; - -const PILL_WIDTH = 200; -const GAP_X = 20; -const GAP_Y = 16; -const PILL_HEIGHT = 50; -const GRID_WIDTH = PILL_WIDTH * 2 + GAP_X; -const GRID_HEIGHT = PILL_HEIGHT * 4 + GAP_Y * 3; -const START_X = (1080 - GRID_WIDTH) / 2; -const START_Y = (1080 - GRID_HEIGHT) / 2; - -export const FeatureBurstScene: React.FC = () => { - return ( - - {FEATURES.map((f, i) => { - const x = START_X + f.col * (PILL_WIDTH + GAP_X); - const y = START_Y + f.row * (PILL_HEIGHT + GAP_Y); - return ( - - ); - })} - - ); -}; diff --git a/apps/videos/src/compositions/x-launch/scenes/GitHubCTAScene.tsx b/apps/videos/src/compositions/x-launch/scenes/GitHubCTAScene.tsx deleted file mode 100644 index 50bae7e6..00000000 --- a/apps/videos/src/compositions/x-launch/scenes/GitHubCTAScene.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import type React from "react"; -import { AbsoluteFill } from "remotion"; -import { GitHubCTA } from "@/components/GitHubCTA"; - -export const GitHubCTAScene: React.FC = () => { - return ( - - - - ); -}; diff --git a/apps/videos/src/compositions/x-launch/scenes/HookScene.tsx b/apps/videos/src/compositions/x-launch/scenes/HookScene.tsx deleted file mode 100644 index 4be724c5..00000000 --- a/apps/videos/src/compositions/x-launch/scenes/HookScene.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import type React from "react"; -import { AbsoluteFill, useCurrentFrame } from "remotion"; -import { ClipReveal } from "@/components/ClipReveal"; -import { COLOR } from "@/lib/colors"; -import { TEXT } from "@/lib/fonts"; - -export const HookScene: React.FC = () => { - const frame = useCurrentFrame(); - const glowOpacity = Math.sin(frame * 0.08) * 0.03 + 0.08; - - return ( - -
- -
- - Your images. - -
- - Stay yours. - -
- - ); -}; diff --git a/apps/videos/src/compositions/x-launch/scenes/PrivacyBeatScene.tsx b/apps/videos/src/compositions/x-launch/scenes/PrivacyBeatScene.tsx deleted file mode 100644 index 7e4f5fe6..00000000 --- a/apps/videos/src/compositions/x-launch/scenes/PrivacyBeatScene.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import type React from "react"; -import { AbsoluteFill, Img, interpolate, staticFile, useCurrentFrame } from "remotion"; -import { RotatingTaglines } from "@/components/RotatingTaglines"; -import { COLOR } from "@/lib/colors"; - -const TAGLINES = [ - "No uploads to the cloud. Ever.", - "100% local processing.", - "Works fully offline.", - "Air-gapped ready.", -]; - -export const PrivacyBeatScene: React.FC = () => { - const frame = useCurrentFrame(); - const totalFrames = 120; - - const bgScale = interpolate(frame, [0, totalFrames], [1.0, 1.05], { - extrapolateRight: "clamp", - }); - - return ( - - - - - - - - ); -}; diff --git a/apps/videos/src/compositions/x-launch/scenes/TerminalInstallScene.tsx b/apps/videos/src/compositions/x-launch/scenes/TerminalInstallScene.tsx deleted file mode 100644 index 74eb6c76..00000000 --- a/apps/videos/src/compositions/x-launch/scenes/TerminalInstallScene.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import type React from "react"; -import { AbsoluteFill } from "remotion"; -import { Terminal } from "@/components/Terminal"; - -const COMMAND = [ - { text: "docker", color: "#ff7b72" }, - { text: " run ", color: "#e6edf3" }, - { text: "-p ", color: "#79c0ff" }, - { text: "3000:3000", color: "#a5d6ff" }, - { text: " ", color: "#e6edf3" }, - { text: "snapotter/snapotter", color: "#7ee787" }, -]; - -const OUTPUT = [ - { text: "v2.4.0", color: "#f59e0b", delay: 0 }, - { text: "49 tools loaded", color: "#7ee787", delay: 3 }, - { text: "15 AI models ready", color: "#7ee787", delay: 6 }, - { text: "✓ Server running on :3000", color: "#3fb950", delay: 9 }, -]; - -export const TerminalInstallScene: React.FC = () => { - return ( - - - - ); -}; diff --git a/apps/videos/src/compositions/x-launch/scenes/ToolGridRevealScene.tsx b/apps/videos/src/compositions/x-launch/scenes/ToolGridRevealScene.tsx deleted file mode 100644 index c9fd507a..00000000 --- a/apps/videos/src/compositions/x-launch/scenes/ToolGridRevealScene.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import type React from "react"; -import { AbsoluteFill, Img, interpolate, staticFile, useCurrentFrame } from "remotion"; -import { AppWindow } from "@/components/AppWindow"; -import { Counter } from "@/components/Counter"; -import { COLOR } from "@/lib/colors"; -import { TEXT } from "@/lib/fonts"; - -export const ToolGridRevealScene: React.FC = () => { - const frame = useCurrentFrame(); - const totalFrames = 180; - - const scale = interpolate(frame, [0, totalFrames], [1.0, 1.08], { - extrapolateRight: "clamp", - }); - const windowOpacity = interpolate(frame, [0, 20], [0, 1], { - extrapolateLeft: "clamp", - extrapolateRight: "clamp", - }); - - return ( - -
- -
- -
-
-
- -
- -
-
- ); -}; diff --git a/apps/videos/src/index.ts b/apps/videos/src/index.ts deleted file mode 100644 index f31c790e..00000000 --- a/apps/videos/src/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { registerRoot } from "remotion"; -import { RemotionRoot } from "./Root"; - -registerRoot(RemotionRoot); diff --git a/apps/videos/src/lib/audio.tsx b/apps/videos/src/lib/audio.tsx deleted file mode 100644 index 32844ee1..00000000 --- a/apps/videos/src/lib/audio.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { Audio } from "@remotion/media"; -import type React from "react"; -import { interpolate, staticFile, useCurrentFrame } from "remotion"; - -export const BackgroundMusic: React.FC<{ - src?: string; - volume?: number; - fadeInFrames?: number; - fadeOutFrames?: number; - totalFrames: number; -}> = ({ src, volume = 0.4, fadeInFrames = 30, fadeOutFrames = 60, totalFrames }) => { - const frame = useCurrentFrame(); - if (!src) return null; - - const vol = interpolate( - frame, - [0, fadeInFrames, totalFrames - fadeOutFrames, totalFrames], - [0, volume, volume, 0], - { extrapolateLeft: "clamp", extrapolateRight: "clamp" }, - ); - - return