diff --git a/apps/videos/src/Root.tsx b/apps/videos/src/Root.tsx index a136b6cc..92eb0807 100644 --- a/apps/videos/src/Root.tsx +++ b/apps/videos/src/Root.tsx @@ -1,12 +1,6 @@ import "./style.css"; import type React from "react"; -import { AbsoluteFill, Composition } from "remotion"; -import { ClipReveal } from "./components/ClipReveal"; -import { Counter } from "./components/Counter"; -import { GradientBlob } from "./components/GradientBlob"; -import { GrainOverlay } from "./components/GrainOverlay"; -import { PhotoPlaceholder } from "./components/PhotoPlaceholder"; -import { ToolPill } from "./components/ToolPill"; +import { Composition } from "remotion"; import { BrandGradient } from "./compositions/ambient/BrandGradient"; import { FloatingTools } from "./compositions/ambient/FloatingTools"; import { AiMagicReel } from "./compositions/features/AiMagicReel"; @@ -18,57 +12,13 @@ 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"; -import { TEXT } from "./lib/fonts"; - -const ComponentTest: React.FC = () => ( - -
- - Hello SnapOtter - -
-
- -
-
- -
- - - -
-); export const RemotionRoot: React.FC = () => ( <> - + {/* Ambient */} ( width={800} height={600} /> + + {/* Features */} ( width={800} height={600} /> - ( width={800} height={600} /> + + + {/* Hero */} ( width={1920} height={1080} /> + + {/* Showcase */} + + + + {/* Promo / Marketing */} ( width={1080} height={1080} /> - = ({ 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 */ + const escalatedLeftTint = interpolate(frame, [160, 320], [0.03, 0.15], { + extrapolateLeft: "clamp", + extrapolateRight: "clamp", + }); + + /* Shake animation (frame 300-320) */ + const shakeX = frame >= 300 && frame <= 320 ? Math.sin(frame * 2.5) * 2 : 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.15, 0.18], { + extrapolateLeft: "clamp", + extrapolateRight: "clamp", + }); + + /* Left vibrate */ + const vibrateX = frame >= 320 && frame < 420 ? Math.sin(frame * 3) * 1.5 : 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 && ( +
+ +
+ )} + + + + ); +};