import React from 'react'; import { useCurrentFrame, useVideoConfig, interpolate, Easing, AbsoluteFill, Img, } from 'remotion'; // ============================================================================= // COMPOSITION CONFIG // ============================================================================= export const compositionConfig = { id: 'ImageMorph', durationInSeconds: 4, fps: 30, width: 1080, height: 1920, }; // ============================================================================= // ✏️ TEMPLATE CONFIG — EDIT THESE // ============================================================================= const CONFIG = { // Image URLs — replace with your own imageA: 'https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=1080&h=1920&fit=crop', imageB: 'https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=1080&h=1920&fit=crop', // Morph style: 'dissolve' | 'turbulence' | 'dreamy' | 'liquid' morphStyle: 'dissolve' as 'dissolve' | 'turbulence' | 'dreamy' | 'liquid', // Timing (in seconds) holdFirstImage: 0.5, morphDuration: 2.2, holdLastImage: 0.5, // How much the images blend/overlap during crossfade (0.1–0.5) blendOverlap: 0.35, // Turbulence / distortion strength (pixels of displacement) distortionStrength: 80, // Scale drift — subtle zoom shift during morph scaleDrift: 1.08, // Blur peak (px) during morph center blurPeak: 6, // Background backgroundColor: '#000000', }; // ============================================================================= // EASINGS // ============================================================================= const EASINGS = { smooth: Easing.bezier(0.45, 0, 0.55, 1), soft: Easing.bezier(0.25, 0.1, 0.25, 1), }; // ============================================================================= // SVG FILTER IDS // ============================================================================= const FILTER_IDS = { turbulence: 'morph-turbulence', liquid: 'morph-liquid', }; // ============================================================================= // DISSOLVE — smooth crossfade with scale & blur // ============================================================================= const DissolveMorph: React.FC<{ progress: number }> = ({ progress }) => { const { blendOverlap, scaleDrift, blurPeak } = CONFIG; const opacityA = interpolate( progress, [0, 0.5 + blendOverlap], [1, 0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: EASINGS.smooth } ); const opacityB = interpolate( progress, [0.5 - blendOverlap, 1], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: EASINGS.smooth } ); const scaleA = interpolate(progress, [0, 1], [1, scaleDrift], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: EASINGS.soft, }); const scaleB = interpolate(progress, [0, 1], [scaleDrift, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: EASINGS.soft, }); const blur = interpolate(progress, [0, 0.5, 1], [0, blurPeak, 0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); const brightness = interpolate(progress, [0, 0.5, 1], [1, 1.15, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); return ( ); }; // ============================================================================= // TURBULENCE — dissolve + SVG noise displacement // ============================================================================= const TurbulenceMorph: React.FC<{ progress: number; frame: number }> = ({ progress, frame, }) => { const { blendOverlap, distortionStrength, blurPeak } = CONFIG; const opacityA = interpolate(progress, [0, 0.5 + blendOverlap], [1, 0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: EASINGS.smooth, }); const opacityB = interpolate(progress, [0.5 - blendOverlap, 1], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: EASINGS.smooth, }); const distortion = interpolate(progress, [0, 0.5, 1], [0, distortionStrength, 0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); const blur = interpolate(progress, [0, 0.5, 1], [0, blurPeak * 0.5, 0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); const turbulenceFreq = interpolate(progress, [0, 0.5, 1], [0.005, 0.015, 0.005], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); const filterStyle = distortion > 0 ? `url(#${FILTER_IDS.turbulence}) blur(${blur}px)` : 'none'; return ( ); }; // ============================================================================= // DREAMY — heavy blur + saturation shift + hue rotation // ============================================================================= const DreamyMorph: React.FC<{ progress: number }> = ({ progress }) => { const { scaleDrift, blurPeak } = CONFIG; const opacityA = interpolate(progress, [0, 0.6], [1, 0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: EASINGS.smooth, }); const opacityB = interpolate(progress, [0.3, 1], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: EASINGS.smooth, }); const blurA = interpolate(progress, [0, 0.6], [0, blurPeak * 2.5], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); const blurB = interpolate(progress, [0.3, 1], [blurPeak * 2.5, 0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); const saturateA = interpolate(progress, [0, 0.5], [1, 1.6], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); const saturateB = interpolate(progress, [0.5, 1], [1.6, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); const hueRotate = interpolate(progress, [0, 0.5, 1], [0, 20, 0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); const scaleA = interpolate(progress, [0, 1], [1, scaleDrift * 1.05], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: EASINGS.soft, }); const scaleB = interpolate(progress, [0, 1], [scaleDrift * 1.05, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: EASINGS.soft, }); return ( {/* Soft light flash at peak */} ); }; // ============================================================================= // LIQUID — dissolve + ripple displacement with smooth noise // ============================================================================= const LiquidMorph: React.FC<{ progress: number; frame: number }> = ({ progress, frame, }) => { const { blendOverlap, distortionStrength, blurPeak } = CONFIG; const opacityA = interpolate(progress, [0, 0.5 + blendOverlap], [1, 0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: EASINGS.smooth, }); const opacityB = interpolate(progress, [0.5 - blendOverlap, 1], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: EASINGS.smooth, }); const distortion = interpolate( progress, [0, 0.4, 0.6, 1], [0, distortionStrength * 1.5, distortionStrength * 1.5, 0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' } ); const blur = interpolate(progress, [0, 0.5, 1], [0, blurPeak * 0.7, 0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); const baseFreq = interpolate(progress, [0, 0.3, 0.7, 1], [0.003, 0.012, 0.012, 0.003], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); const filterStyle = distortion > 0 ? `url(#${FILTER_IDS.liquid}) blur(${blur}px)` : 'none'; return ( ); }; // ============================================================================= // MAIN COMPONENT // ============================================================================= const ImageMorph: React.FC = () => { const frame = useCurrentFrame(); const { fps } = useVideoConfig(); const holdAFrames = Math.round(CONFIG.holdFirstImage * fps); const morphFrames = Math.round(CONFIG.morphDuration * fps); const holdBStart = holdAFrames + morphFrames; const morphProgress = interpolate( frame, [holdAFrames, holdAFrames + morphFrames], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' } ); const holdAScale = interpolate(frame, [0, holdAFrames], [1, 1.03], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: EASINGS.soft, }); const holdBScale = interpolate( frame, [holdBStart, holdBStart + Math.round(CONFIG.holdLastImage * fps)], [1.03, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: EASINGS.soft } ); const vignetteOpacity = interpolate(morphProgress, [0, 0.5, 1], [0.2, 0.5, 0.2], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); const renderContent = () => { if (morphProgress <= 0) { return ( ); } if (morphProgress >= 1) { return ( ); } switch (CONFIG.morphStyle) { case 'dissolve': return ; case 'turbulence': return ; case 'dreamy': return ; case 'liquid': return ; default: return ; } }; return ( {renderContent()} ); }; export default ImageMorph;