remotion-studio / src /compositions /TextExplainer.tsx
VinOS Agent
Initial commit: Remotion Studio — AI Video Generator HF Space
8a6b85c
import React from "react";
import {
AbsoluteFill,
interpolate,
useCurrentFrame,
useVideoConfig,
spring,
Sequence,
} from "remotion";
type Props = {
title: string;
hook: string;
bullets: string[];
cta: string;
ctaUrl: string;
brandColor: string;
};
const TitleScene: React.FC<{ title: string; brandColor: string }> = ({
title,
brandColor,
}) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const scale = spring({ frame, fps, config: { damping: 12, stiffness: 80 } });
const opacity = interpolate(frame, [0, 20], [0, 1], { extrapolateRight: "clamp" });
return (
<AbsoluteFill
style={{
background: `linear-gradient(135deg, #090c12 0%, #111622 50%, #161e2e 100%)`,
justifyContent: "center",
alignItems: "center",
padding: 60,
}}
>
<div
style={{
width: 120,
height: 4,
background: brandColor,
marginBottom: 30,
opacity,
transform: `scaleX(${scale})`,
}}
/>
<div
style={{
fontSize: 52,
fontWeight: 700,
color: "#e2e8f0",
textAlign: "center",
lineHeight: 1.2,
opacity,
transform: `translateY(${interpolate(frame, [0, 25], [30, 0], { extrapolateRight: "clamp" })}px)`,
fontFamily: "sans-serif",
}}
>
{title}
</div>
<div
style={{
position: "absolute",
bottom: 40,
right: 50,
fontSize: 14,
color: "#64748b",
fontFamily: "sans-serif",
}}
>
powered by VinOS
</div>
</AbsoluteFill>
);
};
const HookScene: React.FC<{ hook: string; brandColor: string }> = ({
hook,
brandColor,
}) => {
const frame = useCurrentFrame();
const opacity = interpolate(frame, [0, 15], [0, 1], { extrapolateRight: "clamp" });
const y = interpolate(frame, [0, 20], [40, 0], { extrapolateRight: "clamp" });
return (
<AbsoluteFill
style={{
background: "#090c12",
justifyContent: "center",
alignItems: "center",
padding: 80,
}}
>
<div
style={{
fontSize: 44,
fontWeight: 600,
color: brandColor,
textAlign: "center",
lineHeight: 1.4,
opacity,
transform: `translateY(${y}px)`,
fontFamily: "sans-serif",
}}
>
{hook}
</div>
</AbsoluteFill>
);
};
const BulletScene: React.FC<{
bullet: string;
index: number;
total: number;
brandColor: string;
}> = ({ bullet, index, total, brandColor }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const slideIn = spring({ frame, fps, config: { damping: 14, stiffness: 100 } });
const opacity = interpolate(frame, [0, 15], [0, 1], { extrapolateRight: "clamp" });
return (
<AbsoluteFill
style={{
background: "#090c12",
justifyContent: "center",
alignItems: "center",
padding: 80,
}}
>
<div
style={{
fontSize: 14,
color: "#64748b",
marginBottom: 20,
fontFamily: "sans-serif",
opacity,
}}
>
{index + 1} / {total}
</div>
<div
style={{
display: "flex",
alignItems: "center",
gap: 20,
opacity,
transform: `translateX(${interpolate(slideIn, [0, 1], [-60, 0])}px)`,
}}
>
<div
style={{
width: 48,
height: 48,
borderRadius: "50%",
background: brandColor,
display: "flex",
justifyContent: "center",
alignItems: "center",
fontSize: 24,
fontWeight: 700,
color: "#090c12",
fontFamily: "sans-serif",
flexShrink: 0,
}}
>
{index + 1}
</div>
<div
style={{
fontSize: 38,
fontWeight: 500,
color: "#e2e8f0",
lineHeight: 1.3,
fontFamily: "sans-serif",
}}
>
{bullet}
</div>
</div>
</AbsoluteFill>
);
};
const CtaScene: React.FC<{ cta: string; brandColor: string }> = ({
cta,
brandColor,
}) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const scale = spring({ frame, fps, config: { damping: 10, stiffness: 120 } });
const opacity = interpolate(frame, [0, 15], [0, 1], { extrapolateRight: "clamp" });
const pulse = Math.sin(frame / 8) * 0.03 + 1;
return (
<AbsoluteFill
style={{
background: `linear-gradient(135deg, #090c12, #111622)`,
justifyContent: "center",
alignItems: "center",
}}
>
<div
style={{
padding: "24px 60px",
background: brandColor,
borderRadius: 16,
fontSize: 40,
fontWeight: 700,
color: "#090c12",
opacity,
transform: `scale(${scale * pulse})`,
fontFamily: "sans-serif",
}}
>
{cta} &rarr;
</div>
</AbsoluteFill>
);
};
export const TextExplainer: React.FC<Props> = ({
title,
hook,
bullets,
cta,
brandColor,
}) => {
const sceneDuration = 90; // 3 seconds at 30fps
let currentFrame = 0;
return (
<AbsoluteFill>
<Sequence from={currentFrame} durationInFrames={sceneDuration}>
<TitleScene title={title} brandColor={brandColor} />
</Sequence>
{(() => { currentFrame += sceneDuration; return null; })()}
<Sequence from={currentFrame} durationInFrames={sceneDuration}>
<HookScene hook={hook} brandColor={brandColor} />
</Sequence>
{bullets.map((bullet, i) => {
currentFrame += sceneDuration;
return (
<Sequence key={i} from={currentFrame} durationInFrames={sceneDuration}>
<BulletScene
bullet={bullet}
index={i}
total={bullets.length}
brandColor={brandColor}
/>
</Sequence>
);
})}
{(() => { currentFrame += sceneDuration; return null; })()}
<Sequence from={currentFrame} durationInFrames={sceneDuration}>
<CtaScene cta={cta} brandColor={brandColor} />
</Sequence>
</AbsoluteFill>
);
};