260 lines
7.8 KiB
TypeScript
260 lines
7.8 KiB
TypeScript
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<SceneProps> = ({ 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 (
|
|
<AbsoluteFill
|
|
style={{
|
|
...backgroundStyle,
|
|
transform: `scale(${bgScale})`,
|
|
display: "flex",
|
|
justifyContent: "center",
|
|
alignItems: "center",
|
|
overflow: "hidden",
|
|
}}
|
|
>
|
|
{/* Decorative elements */}
|
|
<div
|
|
style={{
|
|
position: "absolute",
|
|
top: "-20%",
|
|
right: "-10%",
|
|
width: "40%",
|
|
height: "40%",
|
|
borderRadius: "50%",
|
|
background: "rgba(255,255,255,0.05)",
|
|
filter: "blur(60px)",
|
|
}}
|
|
/>
|
|
<div
|
|
style={{
|
|
position: "absolute",
|
|
bottom: "-15%",
|
|
left: "-10%",
|
|
width: "35%",
|
|
height: "35%",
|
|
borderRadius: "50%",
|
|
background: "rgba(255,255,255,0.03)",
|
|
filter: "blur(40px)",
|
|
}}
|
|
/>
|
|
|
|
{/* Content */}
|
|
<div
|
|
style={{
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
alignItems: scene.style.textAlign === "left" ? "flex-start" : scene.style.textAlign === "right" ? "flex-end" : "center",
|
|
justifyContent: "center",
|
|
padding: "80px",
|
|
maxWidth: "85%",
|
|
textAlign: scene.style.textAlign,
|
|
...combinedStyles,
|
|
}}
|
|
>
|
|
{/* Icon */}
|
|
{scene.icon && (
|
|
<div
|
|
style={{
|
|
fontSize: scene.style.fontSize * 1.2,
|
|
marginBottom: "20px",
|
|
filter: "drop-shadow(0 4px 8px rgba(0,0,0,0.3))",
|
|
}}
|
|
>
|
|
{scene.icon}
|
|
</div>
|
|
)}
|
|
|
|
{/* Title (small label above main text) */}
|
|
{scene.title && scene.title !== scene.text && (
|
|
<div
|
|
style={{
|
|
color: scene.style.textColor,
|
|
fontSize: Math.max(scene.style.fontSize * 0.35, 18),
|
|
fontFamily: scene.style.fontFamily,
|
|
fontWeight: 600,
|
|
letterSpacing: "3px",
|
|
textTransform: "uppercase",
|
|
marginBottom: "16px",
|
|
opacity: 0.7,
|
|
}}
|
|
>
|
|
{scene.title}
|
|
</div>
|
|
)}
|
|
|
|
{/* Main text */}
|
|
<div
|
|
style={{
|
|
color: scene.style.textColor,
|
|
fontSize: scene.style.fontSize,
|
|
fontFamily: scene.style.fontFamily,
|
|
fontWeight: 700,
|
|
lineHeight: 1.2,
|
|
textShadow: "0 2px 20px rgba(0,0,0,0.3)",
|
|
letterSpacing: "-0.5px",
|
|
}}
|
|
>
|
|
{getDisplayText(scene.text)}
|
|
{scene.animation.entrance === "typewriter" && frame < entranceDuration * 1.5 && (
|
|
<span
|
|
style={{
|
|
opacity: Math.sin(frame * 0.3) > 0 ? 1 : 0,
|
|
marginLeft: "2px",
|
|
}}
|
|
>
|
|
|
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
{/* Subtitle */}
|
|
{scene.subtitle && (
|
|
<div
|
|
style={{
|
|
color: scene.style.textColor,
|
|
fontSize: Math.max(scene.style.fontSize * 0.45, 20),
|
|
fontFamily: scene.style.fontFamily,
|
|
fontWeight: 400,
|
|
marginTop: "20px",
|
|
opacity: 0.8,
|
|
lineHeight: 1.5,
|
|
}}
|
|
>
|
|
{getDisplayText(scene.subtitle)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</AbsoluteFill>
|
|
);
|
|
};
|