diff --git a/apps/videos/src/Root.tsx b/apps/videos/src/Root.tsx index 7f6fa4a9..ed9cb33d 100644 --- a/apps/videos/src/Root.tsx +++ b/apps/videos/src/Root.tsx @@ -9,6 +9,7 @@ import { PhotoPlaceholder } from "./components/PhotoPlaceholder"; import { ToolPill } from "./components/ToolPill"; 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 { TEXT } from "./lib/fonts"; @@ -84,6 +85,14 @@ export const RemotionRoot: React.FC = () => ( width={800} height={600} /> + 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, + }); + + // Geometric pattern background + const patternBg = ` + linear-gradient(45deg, rgba(245,158,11,0.15) 25%, transparent 25%), + linear-gradient(-45deg, rgba(245,158,11,0.15) 25%, transparent 25%), + linear-gradient(45deg, transparent 75%, rgba(245,158,11,0.1) 75%), + linear-gradient(-45deg, transparent 75%, rgba(245,158,11,0.1) 75%) + `; + + // Silhouette: a head-and-shoulders shape via gradient + const silhouetteGradient = ` + radial-gradient(ellipse 90px 100px at 50% 30%, hsl(25, 65%, 65%) 0%, hsl(25, 65%, 65%) 70%, transparent 71%), + radial-gradient(ellipse 130px 200px at 50% 70%, hsl(30, 55%, 55%) 0%, hsl(30, 55%, 55%) 70%, transparent 71%) + `; + + // 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: photo with geometric background */} +
+ {/* Silhouette subject */} +
+
+ + {/* "After" layer: checkerboard + subject only */} +
+ {/* Checkerboard background */} +
+ {/* Subject only */} +
+
+ + {/* 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 */} +
+ {/* Grayscale layer (on top) */} +
+ + {/* Color layer (revealed underneath via clip) */} +
+ + {/* 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 */ +const pixelColors: string[] = Array.from({ length: PIXEL_GRID_SIZE * PIXEL_GRID_SIZE }, (_, i) => { + const hue = random(`pixel-hue-${i}`) * 60 + 15; // warm hues + const sat = 40 + random(`pixel-sat-${i}`) * 30; + const lit = 40 + 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 */} + {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 gradient */} +
+ + {/* 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 */} + + + ); +};