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 (
{step}
{text}
); }; // ============================================ // 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 */}
🎉 Contacts Extracted
{/* 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 */} ); };