import React from 'react'; import { AbsoluteFill, Sequence, Video, Img, interpolate, useCurrentFrame, useVideoConfig, } from 'remotion'; // CapCut-style template presets. // Each preset defines pacing, visual behavior, typography, and motion — not just colors. export const TEMPLATE_PRESETS = { viral: { clipDuration: 5, transitionDuration: 0.3, zoom: { start: 1.05, end: 1.15 }, overlay: { color: '#000000', opacity: 0.12 }, subtitle: { fontFamily: '"Impact", "Arial Black", sans-serif', fontSize: 52, fontWeight: '900', color: '#FFE600', stroke: '#000000', strokeWidth: 5, position: 'bottom', animation: 'pop', letterSpacing: '1px', textTransform: 'uppercase', }, cinematicBars: false, vignette: false, grain: false, }, luxury: { clipDuration: 7, transitionDuration: 1.0, zoom: { start: 1.0, end: 1.08 }, overlay: { color: '#0d0800', opacity: 0.38 }, subtitle: { fontFamily: '"Georgia", "Palatino Linotype", serif', fontSize: 38, fontWeight: 'normal', color: '#D4AF37', stroke: '#1a1000', strokeWidth: 2, position: 'bottom', animation: 'fade', letterSpacing: '3px', textTransform: 'uppercase', }, cinematicBars: true, vignette: true, grain: false, }, business: { clipDuration: 6, transitionDuration: 0.5, zoom: { start: 1.0, end: 1.05 }, overlay: { color: '#001020', opacity: 0.22 }, subtitle: { fontFamily: '"Helvetica Neue", "Arial", sans-serif', fontSize: 42, fontWeight: '700', color: '#FFFFFF', stroke: '#003399', strokeWidth: 3, position: 'bottom', animation: 'slide', letterSpacing: '0.5px', textTransform: 'none', }, cinematicBars: false, vignette: false, grain: false, }, gym: { clipDuration: 3, transitionDuration: 0.12, zoom: { start: 1.0, end: 1.22 }, overlay: { color: '#200000', opacity: 0.28 }, subtitle: { fontFamily: '"Impact", "Arial Black", sans-serif', fontSize: 56, fontWeight: '900', color: '#FF2020', stroke: '#000000', strokeWidth: 6, position: 'middle', animation: 'slam', letterSpacing: '2px', textTransform: 'uppercase', }, cinematicBars: false, vignette: false, grain: false, }, cinematic: { clipDuration: 8, transitionDuration: 1.2, zoom: { start: 1.0, end: 1.06 }, overlay: { color: '#050510', opacity: 0.45 }, subtitle: { fontFamily: '"Georgia", "Times New Roman", serif', fontSize: 34, fontWeight: 'normal', color: '#EEE0C0', stroke: '#000000', strokeWidth: 1, position: 'bottom', animation: 'fade', letterSpacing: '2px', textTransform: 'none', }, cinematicBars: true, vignette: true, grain: true, }, motivation: { clipDuration: 5, transitionDuration: 0.4, zoom: { start: 1.02, end: 1.12 }, overlay: { color: '#0a0020', opacity: 0.2 }, subtitle: { fontFamily: '"Impact", "Arial Black", sans-serif', fontSize: 50, fontWeight: '900', color: '#FFFFFF', stroke: '#6600CC', strokeWidth: 4, position: 'bottom', animation: 'pop', letterSpacing: '1px', textTransform: 'uppercase', }, cinematicBars: false, vignette: false, grain: false, }, dark: { clipDuration: 4, transitionDuration: 0.25, zoom: { start: 1.0, end: 1.10 }, overlay: { color: '#000000', opacity: 0.55 }, subtitle: { fontFamily: '"Impact", "Arial Black", sans-serif', fontSize: 48, fontWeight: '900', color: '#FFFFFF', stroke: '#000000', strokeWidth: 4, position: 'bottom', animation: 'fade', letterSpacing: '2px', textTransform: 'uppercase', }, cinematicBars: true, vignette: true, grain: false, }, }; // Calculate composition duration based on template + clip count. export const calculateMetadata = ({ props }) => { const fps = 30; const preset = TEMPLATE_PRESETS[props.template] || TEMPLATE_PRESETS.viral; const clips = props.clips || []; const clipCount = Math.max(clips.length, 1); const transSec = preset.transitionDuration; const totalSec = Math.min( clipCount * preset.clipDuration - Math.max(0, clipCount - 1) * transSec, 30 ); return { fps, durationInFrames: Math.max(Math.round(totalSec * fps), fps), width: 540, height: 960, }; }; // ─── Clip with zoom + fade transitions ─────────────────────────────────────── const ClipLayer = ({ src, preset, clipDurFrames }) => { const frame = useCurrentFrame(); const fps = 30; const transFrames = Math.round(preset.transitionDuration * fps); const fadeIn = interpolate(frame, [0, Math.max(transFrames, 1)], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); const fadeOut = interpolate( frame, [clipDurFrames - Math.max(transFrames, 1), clipDurFrames], [1, 0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' } ); const opacity = Math.min(fadeIn, fadeOut); const scale = interpolate(frame, [0, clipDurFrames], [preset.zoom.start, preset.zoom.end], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); return ( ); }; // ─── Subtitle overlay with template-specific animation ─────────────────────── const SubtitleLayer = ({ subtitles, preset }) => { const frame = useCurrentFrame(); const fps = 30; const currentTime = frame / fps; const activeSub = subtitles.find( (s) => currentTime >= s.start && currentTime < s.end ); if (!activeSub) return null; const style = preset.subtitle; const subStartFrame = Math.round(activeSub.start * fps); const subFrame = frame - subStartFrame; const IN_FRAMES = 7; let opacity = 1; let scale = 1; let translateY = 0; if (style.animation === 'pop') { opacity = interpolate(subFrame, [0, 3], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); scale = interpolate( subFrame, [0, Math.round(IN_FRAMES * 0.6), IN_FRAMES], [0.5, 1.12, 1.0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' } ); } else if (style.animation === 'fade') { opacity = interpolate(subFrame, [0, IN_FRAMES], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); } else if (style.animation === 'slide') { opacity = interpolate(subFrame, [0, IN_FRAMES], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); translateY = interpolate(subFrame, [0, IN_FRAMES], [24, 0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); } else if (style.animation === 'slam') { opacity = interpolate(subFrame, [0, 2], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); scale = interpolate( subFrame, [0, Math.round(IN_FRAMES * 0.4), IN_FRAMES], [2.2, 0.88, 1.0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' } ); } const topPct = style.position === 'top' ? '18%' : style.position === 'middle' ? '50%' : '82%'; return (
{activeSub.text}
); }; // ─── Cinematic letterbox bars ──────────────────────────────────────────────── const CinematicBars = () => ( <>
); // ─── Film grain (CSS noise simulation) ─────────────────────────────────────── const FilmGrain = () => { const frame = useCurrentFrame(); // Alternate between two grain seeds per frame for animation feel const opacity = 0.04 + (frame % 2) * 0.02; return ( ); }; // ─── Main composition ───────────────────────────────────────────────────────── export const ReelsComposition = ({ clips = [], subtitles = [], template = 'viral', backgroundImage = null, backgroundVideo = null, }) => { const { fps, durationInFrames } = useVideoConfig(); const preset = TEMPLATE_PRESETS[template] || TEMPLATE_PRESETS.viral; const clipDurFrames = Math.round(preset.clipDuration * fps); const transFrames = Math.round(preset.transitionDuration * fps); return ( {/* ── LAYER 1: Background content ── */} {backgroundVideo ? ( ) : backgroundImage ? ( ) : ( // Stock clips with template-driven transitions + zoom clips.map((clip, i) => { const from = i * (clipDurFrames - transFrames); return ( ); }) )} {/* ── LAYER 2: Template color grade overlay ── */} {/* ── LAYER 3: Vignette ── */} {preset.vignette && ( )} {/* ── LAYER 4: Film grain ── */} {preset.grain && } {/* ── LAYER 5: Subtitles ── */} {/* ── LAYER 6: Cinematic bars (on top of subtitles for consistency) ── */} {preset.cinematicBars && } ); };