/** * DemoVideo — marketing/demo video composition with custom scene types. * * Extends standard slide types with specialized marketing scenes: * - terminal: typewriter terminal output * - notification: slide-in notification card * - kanban: kanban board visualization * - pipeline: flowchart with animated progress * * Format: Feed (1080×1080) for landing page * Total: 810 frames (27s at 30fps) */ import React from 'react'; import { AbsoluteFill, Sequence, useCurrentFrame, interpolate } from 'remotion'; import { BackgroundMusic } from './BackgroundMusic'; import type { AudioConfig } from '../renderer'; // ─── Types ────────────────────────────────────────────────────────────────── export interface DemoSlideData { type: 'cover' | 'terminal' | 'key_point' | 'notification' | 'steps' | 'kanban' | 'pipeline' | 'quote' | 'cta'; title: string; subtitle?: string; content?: string; items?: string[]; value?: string; label?: string; background_style?: string; cta_text?: string; } export interface DemoVideoProps { slides: DemoSlideData[]; format: 'feed' | 'reels'; audio?: AudioConfig; } // ─── Constants ────────────────────────────────────────────────────────────── const ACCENT = '#10b981'; const TEXT = '#ffffff'; const MUTED = 'rgba(255,255,255,0.6)'; const FONT = '"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif'; // Custom frame durations for different scene types const FRAME_DURATIONS: Record = { cover: 90, // 3s terminal: 150, // 5s key_point: 90, // 3s notification: 90, // 3s steps: 150, // 5s (kanban) kanban: 150, // 5s pipeline: 135, // 4.5s quote: 135, // 4.5s cta: 105, // 3.5s }; // ─── DemoVideo ────────────────────────────────────────────────────────────── export const DemoVideo: React.FC = ({ slides, audio }) => { let currentFrame = 0; return ( {slides.map((slide, i) => { const duration = FRAME_DURATIONS[slide.type] || 90; const from = currentFrame; currentFrame += duration; return ( ); })} {audio && } ); }; // ─── DemoSlide ────────────────────────────────────────────────────────────── const DemoSlide: React.FC<{ slide: DemoSlideData }> = ({ slide }) => { const frame = useCurrentFrame(); const opacity = interpolate(frame, [0, 15], [0, 1], { extrapolateRight: 'clamp' }); const y = interpolate(frame, [0, 15], [24, 0], { extrapolateRight: 'clamp' }); return ( ); }; // ─── Slide content by type ────────────────────────────────────────────────── function SlideContent({ slide }: { slide: DemoSlideData }) { const frame = useCurrentFrame(); switch (slide.type) { case 'cover': return ( <>

{slide.title}

{slide.subtitle && (

{slide.subtitle}

)} ); case 'terminal': return ; case 'notification': return ; case 'key_point': return ( <>
{slide.title}

{slide.content}

); case 'steps': case 'kanban': return ; case 'pipeline': return ; case 'quote': return ( <>

“{slide.content}”

); case 'cta': return ( <>

{slide.title}

{slide.cta_text || 'Get Started'}
); default: return
{slide.title}
; } } // ─── Custom Scene Components ──────────────────────────────────────────────── function TerminalScene({ title, content, frame }: { title: string; content: string; frame: number }) { // Typewriter effect const lines = content.split('\n'); const charsPerFrame = 2; const visibleChars = Math.floor(frame * charsPerFrame); let charCount = 0; const visibleLines = lines.map((line) => { const lineStart = charCount; charCount += line.length + 1; // +1 for newline const lineEnd = lineStart + line.length; if (visibleChars < lineStart) return ''; if (visibleChars >= lineEnd) return line; return line.slice(0, visibleChars - lineStart); }); return (
{title}
{visibleLines.map((line, i) => (
{line || '\u00A0'}
))}
); } function NotificationScene({ title, content, frame }: { title: string; content: string; frame: number }) { // Slide in from right const x = interpolate(frame, [0, 20], [100, 0], { extrapolateRight: 'clamp' }); return (
{title}
{content}
); } function KanbanScene({ title, items, frame }: { title: string; items: string[]; frame: number }) { return (
{title}
{items.map((item, i) => { const delay = i * 10; const opacity = interpolate(frame, [delay, delay + 15], [0, 1], { extrapolateRight: 'clamp' }); const scale = interpolate(frame, [delay, delay + 15], [0.8, 1], { extrapolateRight: 'clamp' }); return (
{item}
); })}
); } function PipelineScene({ title, content, label, frame }: { title: string; content: string; label?: string; frame: number }) { const steps = content.split(' → '); const progress = interpolate(frame, [0, 100], [0, 1], { extrapolateRight: 'clamp' }); return (
{title}
{steps.map((step, i) => (
{step}
{i < steps.length - 1 && (
)}
))}
{label && (
{label}
)}
); }