import React from "react"; import { AbsoluteFill, interpolate, useCurrentFrame, useVideoConfig, spring, } from "remotion"; import { Scene as SceneType } from "../lib/types"; interface SceneProps { scene: SceneType; } export const SceneComponent: React.FC = ({ scene }) => { const frame = useCurrentFrame(); const { fps } = useVideoConfig(); const totalFrames = scene.durationInSeconds * fps; // Entrance animation (first 20% of scene) const entranceDuration = Math.floor(totalFrames * 0.2); // Exit animation (last 20% of scene) const exitStart = totalFrames - Math.floor(totalFrames * 0.2); // Calculate entrance opacity and transform const getEntranceStyles = (): React.CSSProperties => { const progress = Math.min(frame / entranceDuration, 1); const springProgress = spring({ frame: Math.min(frame, entranceDuration), fps, config: { damping: 15, stiffness: 100, mass: 0.8 }, }); switch (scene.animation.entrance) { case "fadeIn": return { opacity: interpolate(frame, [0, entranceDuration], [0, 1], { extrapolateRight: "clamp" }) }; case "slideUp": return { opacity: interpolate(frame, [0, entranceDuration * 0.5], [0, 1], { extrapolateRight: "clamp" }), transform: `translateY(${interpolate(springProgress, [0, 1], [80, 0])}px)`, }; case "slideLeft": return { opacity: interpolate(frame, [0, entranceDuration * 0.5], [0, 1], { extrapolateRight: "clamp" }), transform: `translateX(${interpolate(springProgress, [0, 1], [100, 0])}px)`, }; case "slideRight": return { opacity: interpolate(frame, [0, entranceDuration * 0.5], [0, 1], { extrapolateRight: "clamp" }), transform: `translateX(${interpolate(springProgress, [0, 1], [-100, 0])}px)`, }; case "zoomIn": return { opacity: interpolate(frame, [0, entranceDuration * 0.5], [0, 1], { extrapolateRight: "clamp" }), transform: `scale(${interpolate(springProgress, [0, 1], [0.5, 1])})`, }; case "typewriter": return { opacity: 1 }; case "none": default: return { opacity: 1 }; } }; // Calculate exit styles const getExitStyles = (): React.CSSProperties => { if (frame < exitStart) return {}; const exitProgress = (frame - exitStart) / (totalFrames - exitStart); switch (scene.animation.exit) { case "fadeOut": return { opacity: interpolate(exitProgress, [0, 1], [1, 0]) }; case "slideDown": return { opacity: interpolate(exitProgress, [0.5, 1], [1, 0], { extrapolateLeft: "clamp" }), transform: `translateY(${interpolate(exitProgress, [0, 1], [0, 80])}px)`, }; case "slideLeft": return { opacity: interpolate(exitProgress, [0.5, 1], [1, 0], { extrapolateLeft: "clamp" }), transform: `translateX(${interpolate(exitProgress, [0, 1], [0, -100])}px)`, }; case "slideRight": return { opacity: interpolate(exitProgress, [0.5, 1], [1, 0], { extrapolateLeft: "clamp" }), transform: `translateX(${interpolate(exitProgress, [0, 1], [0, 100])}px)`, }; case "zoomOut": return { opacity: interpolate(exitProgress, [0.5, 1], [1, 0], { extrapolateLeft: "clamp" }), transform: `scale(${interpolate(exitProgress, [0, 1], [1, 1.5])})`, }; case "none": default: return {}; } }; // Typewriter effect for text const getDisplayText = (text: string): string => { if (scene.animation.entrance !== "typewriter") return text; const charsToShow = Math.floor( interpolate(frame, [0, entranceDuration * 1.5], [0, text.length], { extrapolateRight: "clamp", }) ); return text.slice(0, charsToShow); }; // Background const backgroundStyle: React.CSSProperties = scene.backgroundGradient ? { background: `linear-gradient(${scene.backgroundGradient.direction}, ${scene.backgroundGradient.from}, ${scene.backgroundGradient.to})`, } : { backgroundColor: scene.style.backgroundColor, }; const entranceStyles = getEntranceStyles(); const exitStyles = getExitStyles(); // Merge entrance and exit const combinedStyles: React.CSSProperties = { ...entranceStyles, ...(frame >= exitStart ? exitStyles : {}), }; // Subtle background animation const bgScale = interpolate(frame, [0, totalFrames], [1, 1.05], { extrapolateRight: "clamp", }); return ( {/* Decorative elements */}
{/* Content */}
{/* Icon */} {scene.icon && (
{scene.icon}
)} {/* Title (small label above main text) */} {scene.title && scene.title !== scene.text && (
{scene.title}
)} {/* Main text */}
{getDisplayText(scene.text)} {scene.animation.entrance === "typewriter" && frame < entranceDuration * 1.5 && ( 0 ? 1 : 0, marginLeft: "2px", }} > | )}
{/* Subtitle */} {scene.subtitle && (
{getDisplayText(scene.subtitle)}
)}
); };