70 lines
3.6 KiB
TypeScript
70 lines
3.6 KiB
TypeScript
|
|
import React from "react";
|
||
|
|
import { AbsoluteFill, interpolate, spring, useCurrentFrame, useVideoConfig } from "remotion";
|
||
|
|
import { z } from "zod";
|
||
|
|
import { colorSchema } from "../lib/branding";
|
||
|
|
import { FONT } from "../lib/fonts";
|
||
|
|
import { useLayout } from "../lib/aspect";
|
||
|
|
import { BrandBackground, useReveal } from "../lib/kit";
|
||
|
|
import { hexToRgba } from "../lib/anim";
|
||
|
|
|
||
|
|
export const youTubeIntroSchema = z.object({
|
||
|
|
channelName: z.string(),
|
||
|
|
subtitle: z.string(),
|
||
|
|
cta: z.string(),
|
||
|
|
...colorSchema,
|
||
|
|
});
|
||
|
|
|
||
|
|
type Props = z.infer<typeof youTubeIntroSchema>;
|
||
|
|
|
||
|
|
export const YouTubeIntro: React.FC<Props> = ({
|
||
|
|
channelName,
|
||
|
|
subtitle,
|
||
|
|
cta,
|
||
|
|
accentColor,
|
||
|
|
secondaryColor,
|
||
|
|
backgroundColor,
|
||
|
|
textColor,
|
||
|
|
}) => {
|
||
|
|
const frame = useCurrentFrame();
|
||
|
|
const { fps } = useVideoConfig();
|
||
|
|
const L = useLayout();
|
||
|
|
|
||
|
|
const playPop = spring({ frame, fps, config: { damping: 11, stiffness: 130, mass: 0.7 } });
|
||
|
|
const playScale = interpolate(playPop, [0, 1], [0, 1]);
|
||
|
|
const ripple = (frame % 45) / 45;
|
||
|
|
|
||
|
|
const name = useReveal(28, { from: 40 });
|
||
|
|
const sub = useReveal(44, { from: 26 });
|
||
|
|
const bell = useReveal(60, { from: 22, damping: 11 });
|
||
|
|
const bellWiggle = Math.sin(frame / 5) * (frame > 60 && frame < 90 ? 10 : 0);
|
||
|
|
|
||
|
|
return (
|
||
|
|
<AbsoluteFill style={{ fontFamily: FONT, direction: "rtl" }}>
|
||
|
|
<BrandBackground bg={backgroundColor} accent={accentColor} secondary={secondaryColor} particles={14} />
|
||
|
|
|
||
|
|
<AbsoluteFill style={{ justifyContent: "center", alignItems: "center", flexDirection: "column" }}>
|
||
|
|
{/* Play button with ripple */}
|
||
|
|
<div style={{ position: "relative", width: L.vmin(170), height: L.vmin(170), display: "flex", alignItems: "center", justifyContent: "center", transform: `scale(${playScale})` }}>
|
||
|
|
<div style={{ position: "absolute", width: L.vmin(170) * (1 + ripple), height: L.vmin(170) * (1 + ripple), borderRadius: "50%", border: `${L.vmin(3)}px solid ${hexToRgba(accentColor, 1 - ripple)}` }} />
|
||
|
|
<div style={{ width: L.vmin(150), height: L.vmin(150), borderRadius: "50%", background: `linear-gradient(135deg, ${accentColor}, ${secondaryColor})`, display: "flex", alignItems: "center", justifyContent: "center", boxShadow: `0 0 ${L.vmin(50)}px ${hexToRgba(accentColor, 0.6)}` }}>
|
||
|
|
<div style={{ width: 0, height: 0, marginInlineStart: L.vmin(10), borderTop: `${L.vmin(34)}px solid transparent`, borderBottom: `${L.vmin(34)}px solid transparent`, borderInlineEnd: `${L.vmin(56)}px solid #fff` }} />
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div style={{ marginTop: L.vmin(46), opacity: name.opacity, transform: `translateY(${name.y}px)`, fontWeight: 900, fontSize: L.vmin(86), color: textColor, textAlign: "center", textShadow: `0 ${L.vmin(6)}px ${L.vmin(36)}px ${hexToRgba(accentColor, 0.4)}` }}>
|
||
|
|
{channelName}
|
||
|
|
</div>
|
||
|
|
<div style={{ marginTop: L.vmin(16), opacity: sub.opacity, transform: `translateY(${sub.y}px)`, fontWeight: 500, fontSize: L.vmin(30), color: hexToRgba(textColor, 0.78), textAlign: "center" }}>
|
||
|
|
{subtitle}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Subscribe pill */}
|
||
|
|
<div style={{ marginTop: L.vmin(50), opacity: bell.opacity, transform: `scale(${bell.scale})`, display: "flex", alignItems: "center", gap: L.vmin(14), padding: `${L.vmin(18)}px ${L.vmin(46)}px`, borderRadius: 999, background: "#ff0033", boxShadow: `0 0 ${L.vmin(36)}px ${hexToRgba("#ff0033", 0.5)}`, fontWeight: 800, fontSize: L.vmin(32), color: "#fff" }}>
|
||
|
|
<span style={{ display: "inline-block", transform: `rotate(${bellWiggle}deg)`, fontSize: L.vmin(34) }}>🔔</span>
|
||
|
|
{cta}
|
||
|
|
</div>
|
||
|
|
</AbsoluteFill>
|
||
|
|
</AbsoluteFill>
|
||
|
|
);
|
||
|
|
};
|