Files
clawbot/skills/remotion-prompt-video/templates/src/components/Scene.tsx

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>
);
};