230 lines
6.0 KiB
TypeScript
230 lines
6.0 KiB
TypeScript
|
|
import React from "react";
|
||
|
|
import {
|
||
|
|
AbsoluteFill,
|
||
|
|
interpolate,
|
||
|
|
spring,
|
||
|
|
useCurrentFrame,
|
||
|
|
useVideoConfig,
|
||
|
|
Easing,
|
||
|
|
} from "remotion";
|
||
|
|
import { zColor } from "@remotion/zod-types";
|
||
|
|
import { z } from "zod";
|
||
|
|
import { hexToRgba, mixHex, rand } from "../lib/anim";
|
||
|
|
|
||
|
|
export const verticalStorySchema = z.object({
|
||
|
|
kicker: z.string(),
|
||
|
|
line1: z.string(),
|
||
|
|
line2: z.string(),
|
||
|
|
line3: z.string(),
|
||
|
|
ctaText: z.string(),
|
||
|
|
accentColor: zColor(),
|
||
|
|
secondaryColor: zColor(),
|
||
|
|
backgroundColor: zColor(),
|
||
|
|
});
|
||
|
|
|
||
|
|
type Props = z.infer<typeof verticalStorySchema>;
|
||
|
|
|
||
|
|
// ── Diagonal animated gradient + floating dust ───────────────────────────────
|
||
|
|
|
||
|
|
const StoryBackground: React.FC<{
|
||
|
|
bg: string;
|
||
|
|
accent: string;
|
||
|
|
secondary: string;
|
||
|
|
}> = ({ bg, accent, secondary }) => {
|
||
|
|
const frame = useCurrentFrame();
|
||
|
|
const { width, height } = useVideoConfig();
|
||
|
|
const shift = interpolate(frame, [0, 180], [0, 60]);
|
||
|
|
return (
|
||
|
|
<AbsoluteFill style={{ backgroundColor: bg, overflow: "hidden" }}>
|
||
|
|
<AbsoluteFill
|
||
|
|
style={{
|
||
|
|
background: `linear-gradient(160deg, ${hexToRgba(
|
||
|
|
accent,
|
||
|
|
0.32
|
||
|
|
)} 0%, ${bg} 45%, ${hexToRgba(secondary, 0.3)} 100%)`,
|
||
|
|
transform: `translateY(${-shift}px)`,
|
||
|
|
}}
|
||
|
|
/>
|
||
|
|
<AbsoluteFill
|
||
|
|
style={{
|
||
|
|
background: `radial-gradient(circle at 50% 30%, ${hexToRgba(
|
||
|
|
accent,
|
||
|
|
0.25
|
||
|
|
)} 0%, transparent 45%)`,
|
||
|
|
}}
|
||
|
|
/>
|
||
|
|
{Array.from({ length: 22 }).map((_, i) => {
|
||
|
|
const x = rand(i) * width;
|
||
|
|
const baseY = rand(i + 9) * height;
|
||
|
|
const y = (baseY - frame * (0.6 + rand(i) * 1.2)) % height;
|
||
|
|
const size = 2 + rand(i + 3) * 5;
|
||
|
|
const tw = 0.2 + 0.6 * Math.abs(Math.sin((frame + i * 20) / 16));
|
||
|
|
return (
|
||
|
|
<div
|
||
|
|
key={i}
|
||
|
|
style={{
|
||
|
|
position: "absolute",
|
||
|
|
left: x,
|
||
|
|
top: (y + height) % height,
|
||
|
|
width: size,
|
||
|
|
height: size,
|
||
|
|
borderRadius: "50%",
|
||
|
|
background: i % 2 ? secondary : accent,
|
||
|
|
opacity: tw,
|
||
|
|
filter: `blur(0.5px) drop-shadow(0 0 ${size * 2}px ${
|
||
|
|
i % 2 ? secondary : accent
|
||
|
|
})`,
|
||
|
|
}}
|
||
|
|
/>
|
||
|
|
);
|
||
|
|
})}
|
||
|
|
<AbsoluteFill
|
||
|
|
style={{ boxShadow: "inset 0 0 400px 120px rgba(0,0,0,0.6)" }}
|
||
|
|
/>
|
||
|
|
</AbsoluteFill>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
|
||
|
|
const StoryLine: React.FC<{
|
||
|
|
text: string;
|
||
|
|
delay: number;
|
||
|
|
highlight?: string;
|
||
|
|
}> = ({ text, delay, highlight }) => {
|
||
|
|
const frame = useCurrentFrame();
|
||
|
|
const { fps } = useVideoConfig();
|
||
|
|
const s = spring({
|
||
|
|
frame: frame - delay,
|
||
|
|
fps,
|
||
|
|
config: { damping: 16, mass: 0.8, stiffness: 100 },
|
||
|
|
});
|
||
|
|
const y = interpolate(s, [0, 1], [70, 0]);
|
||
|
|
const op = interpolate(s, [0, 1], [0, 1]);
|
||
|
|
return (
|
||
|
|
<div
|
||
|
|
style={{
|
||
|
|
transform: `translateY(${y}px)`,
|
||
|
|
opacity: op,
|
||
|
|
fontFamily: "'Helvetica Neue', Helvetica, Arial, sans-serif",
|
||
|
|
fontWeight: 800,
|
||
|
|
fontSize: 104,
|
||
|
|
lineHeight: 1.04,
|
||
|
|
letterSpacing: -2,
|
||
|
|
color: highlight ?? "#fff",
|
||
|
|
textShadow: highlight
|
||
|
|
? `0 0 40px ${hexToRgba(highlight, 0.6)}`
|
||
|
|
: "0 4px 24px rgba(0,0,0,0.4)",
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
{text}
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
|
||
|
|
export const VerticalStory: React.FC<Props> = ({
|
||
|
|
kicker,
|
||
|
|
line1,
|
||
|
|
line2,
|
||
|
|
line3,
|
||
|
|
ctaText,
|
||
|
|
accentColor,
|
||
|
|
secondaryColor,
|
||
|
|
backgroundColor,
|
||
|
|
}) => {
|
||
|
|
const frame = useCurrentFrame();
|
||
|
|
const { fps } = useVideoConfig();
|
||
|
|
|
||
|
|
const kickerOp = interpolate(frame, [4, 20], [0, 1], {
|
||
|
|
extrapolateRight: "clamp",
|
||
|
|
});
|
||
|
|
|
||
|
|
const ctaSpring = spring({
|
||
|
|
frame: frame - 64,
|
||
|
|
fps,
|
||
|
|
config: { damping: 12, stiffness: 120 },
|
||
|
|
});
|
||
|
|
const ctaScale = interpolate(ctaSpring, [0, 1], [0.6, 1]);
|
||
|
|
const ctaOp = interpolate(ctaSpring, [0, 1], [0, 1]);
|
||
|
|
const arrowBounce = Math.sin(frame / 8) * 8;
|
||
|
|
|
||
|
|
return (
|
||
|
|
<AbsoluteFill>
|
||
|
|
<StoryBackground
|
||
|
|
bg={backgroundColor}
|
||
|
|
accent={accentColor}
|
||
|
|
secondary={secondaryColor}
|
||
|
|
/>
|
||
|
|
<AbsoluteFill
|
||
|
|
style={{
|
||
|
|
flexDirection: "column",
|
||
|
|
justifyContent: "center",
|
||
|
|
padding: 90,
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
<div
|
||
|
|
style={{
|
||
|
|
opacity: kickerOp,
|
||
|
|
display: "inline-block",
|
||
|
|
alignSelf: "flex-start",
|
||
|
|
padding: "10px 24px",
|
||
|
|
marginBottom: 40,
|
||
|
|
borderRadius: 999,
|
||
|
|
border: `2px solid ${hexToRgba(accentColor, 0.7)}`,
|
||
|
|
background: hexToRgba(accentColor, 0.12),
|
||
|
|
fontFamily: "'Helvetica Neue', Helvetica, Arial, sans-serif",
|
||
|
|
fontWeight: 700,
|
||
|
|
fontSize: 26,
|
||
|
|
letterSpacing: 6,
|
||
|
|
textTransform: "uppercase",
|
||
|
|
color: "#fff",
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
{kicker}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<StoryLine text={line1} delay={14} />
|
||
|
|
<StoryLine
|
||
|
|
text={line2}
|
||
|
|
delay={26}
|
||
|
|
highlight={mixHex(accentColor, secondaryColor, 0.35)}
|
||
|
|
/>
|
||
|
|
<StoryLine text={line3} delay={38} />
|
||
|
|
</AbsoluteFill>
|
||
|
|
|
||
|
|
{/* Swipe-up CTA pinned near the bottom */}
|
||
|
|
<div
|
||
|
|
style={{
|
||
|
|
position: "absolute",
|
||
|
|
bottom: 150,
|
||
|
|
left: 0,
|
||
|
|
right: 0,
|
||
|
|
display: "flex",
|
||
|
|
flexDirection: "column",
|
||
|
|
alignItems: "center",
|
||
|
|
opacity: ctaOp,
|
||
|
|
transform: `scale(${ctaScale})`,
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
<div style={{ transform: `translateY(${-arrowBounce}px)`, fontSize: 56 }}>
|
||
|
|
⌃
|
||
|
|
</div>
|
||
|
|
<div
|
||
|
|
style={{
|
||
|
|
marginTop: -6,
|
||
|
|
padding: "20px 60px",
|
||
|
|
borderRadius: 999,
|
||
|
|
background: `linear-gradient(135deg, ${accentColor}, ${secondaryColor})`,
|
||
|
|
boxShadow: `0 0 50px ${hexToRgba(accentColor, 0.6)}`,
|
||
|
|
fontFamily: "'Helvetica Neue', Helvetica, Arial, sans-serif",
|
||
|
|
fontWeight: 800,
|
||
|
|
fontSize: 34,
|
||
|
|
letterSpacing: 1,
|
||
|
|
color: "#fff",
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
{ctaText}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</AbsoluteFill>
|
||
|
|
);
|
||
|
|
};
|