import React from "react";
import {
AbsoluteFill,
Img,
interpolate,
spring,
useCurrentFrame,
useVideoConfig,
Sequence,
staticFile,
Easing,
} from "remotion";
// ============================================
// CANVAS VIEWPORT - Zoom and pan WITHIN an image
// ============================================
const CanvasViewport: React.FC<{
src: string;
startX: number; // Starting viewport position (0-1)
startY: number;
startZoom: number; // Starting zoom level
endX: number; // Ending viewport position
endY: number;
endZoom: number;
progress: number; // Animation progress (0-1)
}> = ({ src, startX, startY, startZoom, endX, endY, endZoom, progress }) => {
const { width, height } = useVideoConfig();
// Interpolate current position
const x = interpolate(progress, [0, 1], [startX, endX]);
const y = interpolate(progress, [0, 1], [startY, endY]);
const zoom = interpolate(progress, [0, 1], [startZoom, endZoom]);
// Canvas size (image is rendered larger than viewport)
const canvasWidth = width * zoom;
const canvasHeight = height * zoom;
// Calculate offset to pan to the target position
const offsetX = (canvasWidth - width) * x;
const offsetY = (canvasHeight - height) * y;
return (
);
};
// ============================================
// STEP BADGE
// ============================================
const StepBadge: React.FC<{
step: number;
text: string;
}> = ({ step, text }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const progress = spring({ frame: frame - 10, fps, config: { damping: 20 } });
return (
);
};
// ============================================
// TITLE CARD
// ============================================
const TitleCard: React.FC<{
title: string;
subtitle?: string;
}> = ({ title, subtitle }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const progress = spring({ frame: frame - 15, fps, config: { damping: 25 } });
const subtitleProgress = spring({ frame: frame - 40, fps, config: { damping: 25 } });
return (
{title}
{subtitle && (
{subtitle}
)}
);
};
// ============================================
// SCENE WRAPPER with crossfade
// ============================================
const Scene: React.FC<{
children: React.ReactNode;
fadeIn?: number;
fadeOut?: number;
}> = ({ children, fadeIn = 20, fadeOut = 20 }) => {
const frame = useCurrentFrame();
const { durationInFrames } = useVideoConfig();
const opacity = Math.min(
interpolate(frame, [0, fadeIn], [0, 1], { extrapolateRight: "clamp" }),
interpolate(frame, [durationInFrames - fadeOut, durationInFrames], [1, 0], { extrapolateLeft: "clamp" })
);
return {children};
};
// ============================================
// INDIVIDUAL SCENES with canvas viewport movement
// ============================================
const SceneLogin: React.FC = () => {
const frame = useCurrentFrame();
const { durationInFrames } = useVideoConfig();
// Eased progress through the scene
const progress = interpolate(frame, [0, durationInFrames], [0, 1], {
easing: Easing.inOut(Easing.cubic),
});
return (
{/* Vignette */}
);
};
const SceneSearch: React.FC = () => {
const frame = useCurrentFrame();
const { durationInFrames } = useVideoConfig();
const progress = interpolate(frame, [0, durationInFrames], [0, 1], {
easing: Easing.inOut(Easing.cubic),
});
return (
);
};
const SceneProperty: React.FC = () => {
const frame = useCurrentFrame();
const { durationInFrames } = useVideoConfig();
const progress = interpolate(frame, [0, durationInFrames], [0, 1], {
easing: Easing.inOut(Easing.cubic),
});
return (
);
};
const SceneOwner: React.FC = () => {
const frame = useCurrentFrame();
const { durationInFrames } = useVideoConfig();
const progress = interpolate(frame, [0, durationInFrames], [0, 1], {
easing: Easing.inOut(Easing.cubic),
});
return (
);
};
const SceneContacts: React.FC = () => {
const frame = useCurrentFrame();
const { durationInFrames } = useVideoConfig();
const progress = interpolate(frame, [0, durationInFrames], [0, 1], {
easing: Easing.inOut(Easing.cubic),
});
return (
);
};
const SceneResults: React.FC = () => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const phones = [
"919-469-9553",
"727-341-0186",
"903-566-9506",
"407-671-2400",
"407-382-2683",
];
const emails = [
"berrizoro@gmail.com",
"aberriz@hotmail.com",
"jasonhitch1@gmail.com",
"albert@annarborusa.org",
"albertb@sterlinghousing.com",
];
return (
{/* Title */}
{/* Two columns */}
{/* Phones */}
📞 Phone Numbers
{phones.map((phone, i) => {
const p = spring({ frame: frame - 40 - i * 8, fps, config: { damping: 18 } });
return (
{phone}
);
})}
{/* Emails */}
📧 Email Addresses
{emails.map((email, i) => {
const p = spring({ frame: frame - 45 - i * 8, fps, config: { damping: 18 } });
return (
{email}
);
})}
{/* Stats */}
{[
{ value: "10", label: "Contacts", color: "#6366f1" },
{ value: "5", label: "Phones", color: "#10b981" },
{ value: "5", label: "Emails", color: "#f59e0b" },
{ value: "60s", label: "Time", color: "#ec4899" },
].map((stat, i) => {
const p = spring({ frame: frame - 100 - i * 10, fps, config: { damping: 20 } });
return (
{stat.value}
{stat.label}
);
})}
);
};
// ============================================
// MAIN COMPOSITION
// ============================================
export const ReonomyCanvasDemo: React.FC = () => {
return (
{/* Intro */}
{/* Login - camera moves down to login button */}
{/* Search - camera pans across search results */}
{/* Property - camera explores property details */}
{/* Owner - camera moves to owner section */}
{/* Contacts - camera pans down contact list */}
{/* Results */}
);
};