Spaces:
Sleeping
Sleeping
| 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} → | |
| </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> | |
| ); | |
| }; | |