import { AbsoluteFill, Img, interpolate, spring, useCurrentFrame, useVideoConfig, } from "remotion"; import { random } from "remotion"; // ───────────────────────────────────────────── // CONSTANTS // ───────────────────────────────────────────── const SCENE_DURATION = 90; // 3 seconds at 30 fps const TOTAL_SCENES = 3; const TRANSITION_FRAMES = 12; // ~0.4s cross-fade overlap // ───────────────────────────────────────────── // TYPES // ───────────────────────────────────────────── interface BrandColors { primary?: string; background?: string; text?: string; secondary?: string; } interface SceneProps { displayText?: string; narrationText?: string; imageUrl?: string | null; logoUrl?: string | null; brandColors?: BrandColors; titleFontSize?: number; descriptionFontSize?: number; /** Injected by SceneSequence — always starts at 0 for each scene */ localFrame: number; } interface TransitionSlideProps { sceneIndex: number; localFrame: number; sceneDuration: number; props: SceneProps; } // ───────────────────────────────────────────── // SCENE 1 — Green / White brand // ───────────────────────────────────────────── const Scene1: React.FC = (props) => { const frame = props.localFrame; const { fps, width, height } = useVideoConfig(); const { displayText = "", narrationText = "", imageUrl, logoUrl, brandColors = {}, titleFontSize, descriptionFontSize, } = props; const primary = brandColors.primary ?? "#00eb79"; const textColor = brandColors.text ?? "#000000"; const hasImage = !!(imageUrl && typeof imageUrl === "string"); const hasLogo = !!(logoUrl && typeof logoUrl === "string"); // Exit animation const exitStart = SCENE_DURATION - 22; const exitProgress = interpolate(frame, [exitStart, SCENE_DURATION], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp", }); const exitOpacity = 1 - exitProgress; const exitScale = interpolate(exitProgress, [0, 1], [1, 0.94]); const logoSpring = spring({ frame, fps, config: { damping: 14, stiffness: 220, mass: 1.1 }, from: 0, to: 1, }); const barSpring = spring({ frame: frame - 10, fps, config: { damping: 22, stiffness: 140, mass: 1.2 }, from: 0, to: 1, }); const subtitleSpring = spring({ frame: frame - 35, fps, config: { damping: 20, stiffness: 70 }, from: 0, to: 1, }); const words = displayText ? displayText.split(" ") : []; const wordAnimations = words.map((_, i) => { const delay = 14 + i * 9; return spring({ frame: frame - delay, fps, config: { damping: 18, stiffness: 160, mass: 1.1 }, from: 0, to: 1, }); }); const numParticles = 18; const particles = Array.from({ length: numParticles }, (_, i) => { const seedX = random(`px-${i}`) * (width - 60); const seedY = random(`py-${i}`) * (height - 60); const seedR = 4 + random(`pr-${i}`) * 18; const seedSpeed = 0.4 + random(`ps-${i}`) * 0.8; const seedPhase = random(`pp-${i}`) * Math.PI * 2; const opacity = 0.08 + random(`po-${i}`) * 0.12; const floatY = Math.sin(frame * seedSpeed * 0.04 + seedPhase) * 12; const floatX = Math.cos(frame * seedSpeed * 0.03 + seedPhase) * 8; const isGreen = i % 3 === 0; return { x: seedX + floatX, y: seedY + floatY, r: seedR, opacity, color: isGreen ? primary : "#cccccc", }; }); const glowPulse = interpolate(Math.sin(frame * 0.06), [-1, 1], [0.4, 0.9]); const kbScale = interpolate(frame, [0, SCENE_DURATION], [1, 1.07], { extrapolateLeft: "clamp", extrapolateRight: "clamp", }); const heroFontSize = titleFontSize ?? 88; const subFontSize = descriptionFontSize ?? 44; const imageSpring = spring({ frame: frame - 8, fps, config: { damping: 22, stiffness: 100 }, from: 0, to: 1, }); return ( {/* Floating particles */} {particles.map((p, i) => (
))} {/* Decorative circle — top right */}
{/* Decorative circle — bottom left */}
{/* Text side */}
{/* Logo */} {hasLogo && (
)} {/* Accent bar */}
{/* Title — word by word */}
{words.map((word, i) => { const anim = wordAnimations[i]; const isAccentWord = i === 0 || (words.length > 2 && i === words.length - 1); return ( {word} ); })}
{/* Subtitle */} {narrationText && (
{narrationText}
)} {/* Bottom accent dots */}
{[ { w: 48, opacity: 1, bg: primary }, { w: 24, opacity: 0.4, bg: `rgba(0,235,121,0.4)` }, { w: 12, opacity: 0.2, bg: `rgba(0,235,121,0.2)` }, ].map((dot, i) => (
))}
{/* Image side */} {hasImage && (
)} {/* Bottom glow strip */}
); }; // ───────────────────────────────────────────── // SCENE 2 — Red brand panel (Coca-Cola style) // ───────────────────────────────────────────── const Scene2: React.FC = (props) => { const frame = props.localFrame; const { fps, width, height } = useVideoConfig(); const { displayText = "", narrationText = "", logoUrl, imageUrl, titleFontSize, descriptionFontSize, } = props; const hasImage = !!(imageUrl && typeof imageUrl === "string"); const exitStart = SCENE_DURATION - 22; const globalOpacity = frame >= exitStart ? interpolate(frame, [exitStart, SCENE_DURATION - 4], [1, 0], { extrapolateRight: "clamp", }) : 1; const panelSpring = spring({ frame, fps, config: { damping: 22, stiffness: 90, mass: 1.1 }, }); const panelX = interpolate(panelSpring, [0, 1], [-width * 0.48, 0]); const logoSpring = spring({ frame: Math.max(0, frame - 12), fps, config: { damping: 14, stiffness: 180, mass: 1.0 }, }); const logoScale = interpolate(logoSpring, [0, 1], [0.4, 1]); const logoOpacity = interpolate(logoSpring, [0, 1], [0, 1]); const rightPanelOpacity = interpolate(frame, [14, 32], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp", }); const rightPanelX = interpolate( spring({ frame: Math.max(0, frame - 14), fps, config: { damping: 20, stiffness: 80 }, }), [0, 1], [60, 0] ); const accentLineProgress = interpolate( spring({ frame: Math.max(0, frame - 28), fps, config: { damping: 20, stiffness: 100 }, }), [0, 1], [0, 1] ); const words = displayText ? displayText.split(" ") : []; const narrationOpacity = interpolate(frame, [40, 58], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp", }); const narrationY = interpolate( spring({ frame: Math.max(0, frame - 40), fps, config: { damping: 20, stiffness: 90 }, }), [0, 1], [18, 0] ); const glowPulse = interpolate( Math.sin((frame / fps) * Math.PI * 0.8), [-1, 1], [0.18, 0.32] ); interface CircleConfig { seed: number; size: number; x: number; y: number; speed: number; } const circles: CircleConfig[] = [ { seed: 10, size: 180, x: 0.15, y: 0.72, speed: 0.28 }, { seed: 20, size: 100, x: 0.72, y: 0.15, speed: 0.18 }, { seed: 30, size: 60, x: 0.85, y: 0.8, speed: 0.22 }, { seed: 40, size: 130, x: 0.4, y: 0.9, speed: 0.14 }, { seed: 50, size: 45, x: 0.92, y: 0.45, speed: 0.3 }, ]; const kbScale = interpolate(frame, [0, SCENE_DURATION], [1, 1.07], { extrapolateRight: "clamp", }); const kbX = interpolate(frame, [0, SCENE_DURATION], [0, -12], { extrapolateRight: "clamp", }); const panelW = width * 0.5; interface PanelCircle { r: number; cx: string; cy: string; op: number; } const panelCircles: PanelCircle[] = [ { r: 220, cx: "10%", cy: "85%", op: 0.08 }, { r: 140, cx: "80%", cy: "10%", op: 0.1 }, { r: 80, cx: "60%", cy: "80%", op: 0.07 }, ]; return ( {/* Red brand panel */}
{/* Inner glow */}
{/* Decorative circles inside panel */} {panelCircles.map((c, i) => (
))} {/* Logo area */}
{logoUrl && typeof logoUrl === "string" && ( )}
Coca-Cola
{/* Right content panel */}
{hasImage ? (
) : (
{circles.map((c, i) => { const floatY = interpolate( frame, [0, SCENE_DURATION], [0, -c.speed * 60] ); const floatOpacity = interpolate(frame, [8, 24], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp", }); return (
); })}
)} {/* Text content */}
{/* Accent line */}
{/* Title — word by word */}
{words.map((word, i) => { const wordSpring = spring({ frame: Math.max(0, frame - 18 - i * 6), fps, config: { damping: 20, stiffness: 110, mass: 1.0 }, }); return ( {word} ); })}
{/* Narration */} {narrationText && (
{narrationText}
)} {/* Dot row */}
{([0, 1, 2] as const).map((i) => (
))}
{/* Diagonal shine */}
); }; // ───────────────────────────────────────────── // SCENE 3 — Dark / FireBird Tech // ───────────────────────────────────────────── const Scene3: React.FC = (props) => { const frame = props.localFrame; const { fps, width, height } = useVideoConfig(); const { displayText = "", narrationText = "", logoUrl, imageUrl, brandColors = {}, titleFontSize, descriptionFontSize, } = props; const primary = brandColors.primary ?? "#FF6B35"; const hasImage = !!(imageUrl && typeof imageUrl === "string"); const hasLogo = !!(logoUrl && typeof logoUrl === "string"); const exitStart = SCENE_DURATION - 22; const exitProgress = interpolate( frame, [exitStart, SCENE_DURATION - 2], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" } ); const globalOpacity = 1 - exitProgress; const bgPulse = interpolate( Math.sin((frame / fps) * 1.2), [-1, 1], [0, 0.04], { extrapolateLeft: "clamp", extrapolateRight: "clamp" } ); const accentBarWidth = interpolate( spring({ frame: frame - 2, fps, config: { damping: 22, stiffness: 90, mass: 1 } }), [0, 1], [0, 100], { extrapolateLeft: "clamp", extrapolateRight: "clamp" } ); const accentBarBottomWidth = interpolate( spring({ frame: frame - 8, fps, config: { damping: 22, stiffness: 90, mass: 1 } }), [0, 1], [0, 100], { extrapolateLeft: "clamp", extrapolateRight: "clamp" } ); const logoSpring = spring({ frame: frame - 8, fps, config: { damping: 14, stiffness: 220, mass: 1.1 }, }); const titleWords = displayText ? displayText.split(" ") : []; const narrationSpring = spring({ frame: frame - 50, fps, config: { damping: 20, stiffness: 80, mass: 1.2 }, }); const glowPulse = interpolate( Math.sin((frame / fps) * 2.4), [-1, 1], [0.3, 0.7], { extrapolateLeft: "clamp", extrapolateRight: "clamp" } ); const particleCount = 18; const particles = Array.from({ length: particleCount }, (_, i) => { const seedX = random(`px-${i}`); const seedY = random(`py-${i}`); const seedSize = random(`ps-${i}`); const seedSpeed = random(`psp-${i}`); const seedDelay = random(`pd-${i}`); const x = seedX * width; const baseY = seedY * height; const size = 3 + seedSize * 8; const speed = 0.3 + seedSpeed * 0.8; const delay = seedDelay * 40; const yOffset = interpolate( frame - delay, [0, SCENE_DURATION], [0, -height * speed], { extrapolateLeft: "clamp", extrapolateRight: "clamp" } ); const opacity = interpolate( frame - delay, [0, 15, SCENE_DURATION - 20, SCENE_DURATION], [0, 0.25 + seedSize * 0.15, 0.15, 0], { extrapolateLeft: "clamp", extrapolateRight: "clamp" } ); return { x, y: baseY + yOffset, size, opacity, idx: i }; }); const ringSpring = spring({ frame: frame - 15, fps, config: { damping: 18, stiffness: 60, mass: 1.5 }, }); const ringScale = interpolate(ringSpring, [0, 1], [0.5, 1]); const ringOpacity = interpolate(ringSpring, [0, 1], [0, 0.12]); const imageReveal = spring({ frame: frame - 5, fps, config: { damping: 22, stiffness: 80, mass: 1.2 }, }); const heroFontSize = titleFontSize ?? 88; const subFontSize = descriptionFontSize ?? 44; const labelSpring = spring({ frame: frame - 18, fps, config: { damping: 20, stiffness: 110, mass: 1 }, }); const pillSpring = spring({ frame: frame - 65, fps, config: { damping: 20, stiffness: 100, mass: 1 }, }); const cornerSpring = spring({ frame: frame - 10, fps, config: { damping: 20, stiffness: 80 }, }); const cornerSpring2 = spring({ frame: frame - 12, fps, config: { damping: 20, stiffness: 80 }, }); return ( {/* Animated bg tint */} {/* Floating particles */} {particles.map((p) => (
))} {/* Decorative rings */}
{/* Top accent bar */}
{/* Bottom accent bar */}
{/* Main layout */}
{/* Left content */}
{/* Logo */} {hasLogo && (
FireBirdTech
)} {/* Tagline */}
{[0, 1].map((side) => (
))} AI · Tech · Fire
{/* Title — word by word */}
{titleWords.map((word, i) => { const wordSpring = spring({ frame: frame - (22 + i * 8), fps, config: { damping: 14, stiffness: 200, mass: 1.0 }, }); const isAccent = i === 0 || i === titleWords.length - 1; return ( {word} ); })}
{/* Narration */} {narrationText && (

{narrationText}

)} {/* CTA pill */}
firebird-technologies.com
{/* Right image panel */} {hasImage && (
)}
{/* Corner accent — bottom right */}
{/* Corner accent — top left */}
); }; // ───────────────────────────────────────────── // SCENE REGISTRY // ───────────────────────────────────────────── const SCENE_COMPONENTS: React.FC[] = [Scene1, Scene2, Scene3]; // ───────────────────────────────────────────── // CROSS-FADE TRANSITION WRAPPER // ───────────────────────────────────────────── const TransitionSlide: React.FC = ({ sceneIndex, localFrame, sceneDuration, props, }) => { const ActiveScene = SCENE_COMPONENTS[sceneIndex % TOTAL_SCENES]; const NextScene = SCENE_COMPONENTS[(sceneIndex + 1) % TOTAL_SCENES]; const isTransitioning = localFrame > sceneDuration - TRANSITION_FRAMES; const transitionProgress = isTransitioning ? interpolate( localFrame, [sceneDuration - TRANSITION_FRAMES, sceneDuration], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" } ) : 0; return ( {isTransitioning && ( )} ); }; // ───────────────────────────────────────────── // ROOT COMPOSITION — export this to Remotion // ───────────────────────────────────────────── /** Props exposed to the Remotion composition (localFrame is injected internally) */ export type SceneSequenceProps = Omit; export const SceneSequence: React.FC = (props) => { const frame = useCurrentFrame(); const sceneIndex = Math.min( Math.floor(frame / SCENE_DURATION), TOTAL_SCENES - 1 ); const localFrame = frame % SCENE_DURATION; return ( ); }; // ───────────────────────────────────────────── // REMOTION ROOT (register in your Root.tsx) // ───────────────────────────────────────────── // // import { Composition } from "remotion"; // import { SceneSequence, type SceneSequenceProps } from "./SceneSequence"; // // export const RemotionRoot: React.FC = () => ( // // );