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;