import React from "react"; import { AbsoluteFill, Img, interpolate, spring, useCurrentFrame, useVideoConfig, Easing, } from "remotion"; import { z } from "zod"; import { colorSchema } from "../lib/branding"; import { FONT } from "../lib/fonts"; import { useLayout } from "../lib/aspect"; import { hexToRgba, mixHex, rand } from "../lib/anim"; export const glitterRevealSchema = z.object({ brandText: z.string(), tagline: z.string(), /** Optional logo image URL. When empty the FlatRender brand mark is used. */ logoUrl: z.string(), ...colorSchema, }); type Props = z.infer; // ── Default FlatRender brand mark (used when the user hasn't uploaded a logo) ── const DefaultLogo: React.FC<{ size: number }> = ({ size }) => ( ); // Deterministic glitter field — each particle flies in from the edge, gathers at // the logo, then disperses into an ambient orbit (the classic glitter-dust reveal). const GLITTER = Array.from({ length: 150 }).map((_, i) => ({ i, angleIn: rand(i) * Math.PI * 2, distIn: 520 + rand(i + 7) * 460, // gather target: a tight cluster over the logo tx: (rand(i + 11) - 0.5) * 360, ty: (rand(i + 19) - 0.5) * 240, // ambient orbit it settles into ambAngle: rand(i + 23) * Math.PI * 2, ambR: 230 + rand(i + 29) * 320, size: 1.6 + rand(i + 3) * 4.5, delay: (i % 18) * 0.9, speed: 0.4 + rand(i + 5) * 1.2, })); const Glitter: React.FC<{ accent: string; secondary: string; gold: string }> = ({ accent, secondary, gold, }) => { const frame = useCurrentFrame(); const { width, height } = useVideoConfig(); const L = useLayout(); const cx = width / 2; const cy = height / 2; return ( {GLITTER.map((p) => { const conv = interpolate(frame, [p.delay, p.delay + 34], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp", easing: Easing.out(Easing.cubic), }); const disp = interpolate(frame, [46, 86], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp", easing: Easing.inOut(Easing.quad), }); // start (far out) → gather cluster → ambient orbit const sx = cx + Math.cos(p.angleIn) * L.vmin(p.distIn); const sy = cy + Math.sin(p.angleIn) * L.vmin(p.distIn); const gx = cx + L.vmin(p.tx); const gy = cy + L.vmin(p.ty); const ax = cx + Math.cos(p.ambAngle + frame * 0.004 * p.speed) * L.vmin(p.ambR); const ay = cy + Math.sin(p.ambAngle + frame * 0.004 * p.speed) * L.vmin(p.ambR); const tgtX = gx + (ax - gx) * disp; const tgtY = gy + (ay - gy) * disp; const x = sx + (tgtX - sx) * conv; const y = sy + (tgtY - sy) * conv; const twinkle = 0.3 + 0.7 * Math.abs(Math.sin((frame + p.i * 13) / (6 + (p.i % 5)))); const appear = interpolate(frame, [p.delay, p.delay + 10], [0, 1], { extrapolateRight: "clamp" }); const c = p.i % 4 === 0 ? gold : p.i % 3 === 0 ? secondary : accent; const r = L.vmin(p.size) * (0.7 + conv * 0.5); return ( ); })} ); }; export const GlitterReveal: React.FC = ({ brandText, tagline, logoUrl, accentColor, secondaryColor, backgroundColor, textColor, }) => { const frame = useCurrentFrame(); const { fps } = useVideoConfig(); const L = useLayout(); const gold = "#fcd34d"; // Logo reveal (the glitter gathers ~frame 44, then the logo emerges). const logoSpring = spring({ frame: frame - 42, fps, config: { damping: 13, stiffness: 95, mass: 0.9 } }); const logoScale = interpolate(logoSpring, [0, 1], [0.55, 1]); const logoOpacity = interpolate(frame, [42, 60], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" }); // Bright convergence flash. const flash = interpolate(frame, [40, 47, 60], [0, 0.85, 0], { extrapolateLeft: "clamp", extrapolateRight: "clamp" }); // Core glow that breathes behind the logo. const glow = interpolate(frame, [44, 70], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" }); const breathe = 1 + 0.05 * Math.sin(frame / 16); // Shine sweep across the logo at reveal. const sweepX = interpolate(frame, [58, 88], [-L.vmin(360), L.vmin(360)], { extrapolateLeft: "clamp", extrapolateRight: "clamp", easing: Easing.inOut(Easing.cubic) }); const sweepOp = interpolate(frame, [58, 66, 82, 90], [0, 0.9, 0.9, 0], { extrapolateLeft: "clamp", extrapolateRight: "clamp" }); // Text. const brandY = interpolate(frame, [70, 92], [L.vmin(70), 0], { extrapolateLeft: "clamp", extrapolateRight: "clamp", easing: Easing.out(Easing.cubic) }); const brandOpacity = interpolate(frame, [70, 90], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" }); const tagOpacity = interpolate(frame, [92, 112], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" }); const tagSpacing = interpolate(frame, [92, 120], [L.vmin(14), L.vmin(6)], { extrapolateLeft: "clamp", extrapolateRight: "clamp" }); const logoSize = L.vmin(240); const hasLogo = Boolean(logoUrl && logoUrl.trim().length > 0); return ( {/* Deep radial backdrop */} {/* Core glow */}
{/* Logo */}
{hasLogo ? ( ) : ( )}
{/* Convergence flash */}
{/* Shine sweep */}
{/* Brand text + tagline */}
{brandText}
{tagline}
); };