Spaces:
Runtime error
Runtime error
Download renderer/src/compositions/LaunchTrailer.tsx from firebird-technologies/NoteStack-Test: direct link, hf CLI and curl.
- Browser
- Download file 42.3 kB
-
https://huggingface.co/spaces/firebird-technologies/NoteStack-Test/resolve/main/renderer/src/compositions/LaunchTrailer.tsx
- Command line
-
hf download hf://spaces/firebird-technologies/NoteStack-Test/renderer/src/compositions/LaunchTrailer.tsx
-
curl -L -o LaunchTrailer.tsx https://huggingface.co/spaces/firebird-technologies/NoteStack-Test/resolve/main/renderer/src/compositions/LaunchTrailer.tsx
42.3 kB
| import type { CSSProperties, ReactNode } from "react"; | |
| import { AbsoluteFill, Audio as SoundTrack, Easing, interpolate, random, Sequence, spring, staticFile, useCurrentFrame, useVideoConfig } from "remotion"; | |
| import { theme } from "../theme"; | |
| import { Flare, WarpField } from "./LandingDemo"; | |
| import { Lockup } from "./Lockup"; | |
| import { LAUNCH_BPM, LAUNCH_SCENES, LAUNCH_SECONDS, type LaunchSceneId } from "./launchTimeline"; | |
| /** Launch trailer: a journey of knowledge through the galaxies of untapped potential. | |
| * One galaxy per feature, cut on the beat (see launchTimeline.ts, generated with the score), | |
| * narrated by Nora Vale, ending on "Notestack AI. Coming soon." 1920x1080, 30 fps. */ | |
| export const LAUNCH_FPS = 30; | |
| const S = (seconds: number) => Math.round(seconds * LAUNCH_FPS); | |
| export const LAUNCH_DURATION = S(LAUNCH_SECONDS); | |
| const BLUE = theme.blue; | |
| const WHITE = theme.white; | |
| const BEAT = 60 / LAUNCH_BPM; | |
| const XF = 0.3; // each scene fades in over the tail of the previous one | |
| const ease = Easing.bezier(0.22, 1, 0.36, 1); | |
| const clamp = { extrapolateLeft: "clamp", extrapolateRight: "clamp" } as const; | |
| // ---------------------------------------------------------------- audio | |
| const VO_WINDOWS: [number, number][] = LAUNCH_SCENES.filter((s) => s.vo !== null).map((s) => [ | |
| S(s.start + (s.vo ?? 0)), | |
| S(s.start + (s.vo ?? 0) + (s.voLen ?? 0)), | |
| ]); | |
| const MUSIC = 0.62; | |
| const DUCKED = 0.24; | |
| function musicVolume(frame: number): number { | |
| const ramp = 8; | |
| let level = MUSIC; | |
| for (const [a, b] of VO_WINDOWS) { | |
| if (frame >= a - ramp && frame <= b + ramp) { | |
| const into = interpolate(frame, [a - ramp, a, b, b + ramp], [0, 1, 1, 0], clamp); | |
| level = Math.min(level, MUSIC - (MUSIC - DUCKED) * into); | |
| } | |
| } | |
| return level; | |
| } | |
| // ---------------------------------------------------------------- shared pieces | |
| /** 1 on each beat, decaying quickly. Scenes start on bar lines, so local time is beat aligned. */ | |
| function usePulse(sharp = 8) { | |
| const frame = useCurrentFrame(); | |
| const t = frame / LAUNCH_FPS; | |
| return Math.exp(-(t % BEAT) * sharp); | |
| } | |
| /** A spiral galaxy with a white core; rotates slowly, drifts toward the camera over the scene. */ | |
| function Galaxy({ seed, x, y, size, tilt = 0.55, arms = 2, spin = 1, coreBoost = 0 }: { | |
| seed: string; x: number; y: number; size: number; tilt?: number; arms?: number; spin?: number; coreBoost?: number; | |
| }) { | |
| const frame = useCurrentFrame(); | |
| const rot = frame * 0.0035 * spin + random(`${seed}-rot`) * 6; | |
| const count = 520; | |
| const stars = []; | |
| for (let i = 0; i < count; i++) { | |
| const arm = i % arms; | |
| const u = Math.sqrt(random(`${seed}-u${i}`)); | |
| const r = u * size; | |
| const theta = u * 5.2 + (arm * Math.PI * 2) / arms + rot + (random(`${seed}-j${i}`) - 0.5) * 0.7 * (1 - u * 0.4); | |
| const spread = (random(`${seed}-s${i}`) - 0.5) * size * 0.12; | |
| const px = Math.cos(theta) * (r + spread); | |
| const py = Math.sin(theta) * (r + spread) * tilt; | |
| const near = 1 - u; | |
| const fill = near > 0.55 ? WHITE : random(`${seed}-c${i}`) < 0.55 ? BLUE : "#9cc4ff"; | |
| stars.push(<circle key={i} cx={px} cy={py} r={0.6 + random(`${seed}-r${i}`) * 1.8 * (0.5 + near)} fill={fill} opacity={0.35 + 0.65 * random(`${seed}-o${i}`)} />); | |
| } | |
| const core = 0.85 + coreBoost; | |
| return ( | |
| <svg style={{ position: "absolute", left: x - size * 1.3, top: y - size * 1.3, overflow: "visible" }} width={size * 2.6} height={size * 2.6}> | |
| <defs> | |
| <radialGradient id={`${seed}-halo`}> | |
| <stop offset="0%" stopColor={BLUE} stopOpacity={0.35} /> | |
| <stop offset="100%" stopColor={BLUE} stopOpacity={0} /> | |
| </radialGradient> | |
| <radialGradient id={`${seed}-core`}> | |
| <stop offset="0%" stopColor={WHITE} stopOpacity={Math.min(1, core)} /> | |
| <stop offset="18%" stopColor={WHITE} stopOpacity={0.45 * core} /> | |
| <stop offset="45%" stopColor="#9cc4ff" stopOpacity={0.12 * core} /> | |
| <stop offset="100%" stopColor={BLUE} stopOpacity={0} /> | |
| </radialGradient> | |
| </defs> | |
| <g transform={`translate(${size * 1.3} ${size * 1.3})`}> | |
| <ellipse rx={size * 1.25} ry={size * 1.25 * tilt} fill={`url(#${seed}-halo)`} /> | |
| {stars} | |
| <ellipse rx={size * 0.42} ry={size * 0.42 * Math.max(tilt, 0.7)} fill={`url(#${seed}-core)`} /> | |
| <circle r={size * 0.035} fill={WHITE} style={{ filter: `drop-shadow(0 0 ${size * 0.06}px ${WHITE}) drop-shadow(0 0 ${size * 0.15}px ${BLUE})` }} /> | |
| </g> | |
| </svg> | |
| ); | |
| } | |
| /** Deep space backdrop: drifting stars and a faint far galaxy. */ | |
| function Space({ drift = 1, children }: { drift?: number; children?: ReactNode }) { | |
| const frame = useCurrentFrame(); | |
| const { width, height } = useVideoConfig(); | |
| const t = frame / LAUNCH_FPS; | |
| return ( | |
| <AbsoluteFill style={{ backgroundColor: theme.black, overflow: "hidden" }}> | |
| <AbsoluteFill style={{ background: `radial-gradient(50% 40% at 70% 50%, ${BLUE}1f, transparent 70%), radial-gradient(35% 30% at 15% 85%, ${BLUE}14, transparent 70%)` }} /> | |
| <svg width={width} height={height} style={{ position: "absolute" }}> | |
| {Array.from({ length: 260 }).map((_, i) => { | |
| const depth = 0.3 + random(`d${i}`) * 0.7; | |
| const x = (((random(`x${i}`) * width - t * 22 * depth * drift) % width) + width) % width; | |
| const y = random(`y${i}`) * height; | |
| const tw = 0.4 + 0.6 * Math.abs(Math.sin(t * (0.5 + random(`p${i}`)) + i)); | |
| return <circle key={i} cx={x} cy={y} r={0.4 + depth * 1.4} fill={random(`b${i}`) < 0.2 ? "#9cc4ff" : WHITE} opacity={tw * depth} />; | |
| })} | |
| </svg> | |
| {children} | |
| </AbsoluteFill> | |
| ); | |
| } | |
| const panel: CSSProperties = { | |
| border: "1px solid rgba(255,255,255,0.16)", | |
| borderRadius: 24, | |
| background: "rgba(0,0,0,0.7)", | |
| boxShadow: `0 0 60px ${BLUE}33, 0 0 2px ${BLUE}`, | |
| backdropFilter: "blur(2px)", | |
| }; | |
| function useIn(delay: number, damping = 16) { | |
| const frame = useCurrentFrame(); | |
| const { fps } = useVideoConfig(); | |
| return spring({ frame: frame - delay, fps, config: { damping } }); | |
| } | |
| function typed(text: string, frame: number, start: number, cps = 30): string { | |
| return text.slice(0, Math.max(0, Math.floor(((frame - start) / LAUNCH_FPS) * cps))); | |
| } | |
| // ---------------------------------------------------------------- journey HUD | |
| const STOPS: { id: LaunchSceneId; short: string; name: string; line: string }[] = [ | |
| { id: "sources", short: "Sources", name: "The Source Nebula", line: "Everything you made, glowing back at you." }, | |
| { id: "research", short: "Research", name: "The Research Cluster", line: "The spark of a thought you forgot you had." }, | |
| { id: "map", short: "Topic map", name: "The Constellation Field", line: "Drift through your own ideas. Stay a while." }, | |
| { id: "voice", short: "Voice", name: "The Voice Pulsar", line: "It feels like you. Only braver." }, | |
| { id: "audio", short: "Audio", name: "The Audio Belt", line: "Lean back. Let it wash over you." }, | |
| { id: "video", short: "Studio", name: "The Studio Galaxy", line: "Color, motion, wonder. Watch it twice." }, | |
| { id: "launchkit", short: "Launch Kit", name: "The Launch Kit Spiral", line: "Your heart races. The good kind of nervous." }, | |
| { id: "launchpad", short: "Launchpad", name: "The Launchpad", line: "Three, two, one, and you are flying." }, | |
| { id: "resurface", short: "Resurface", name: "The Evergreen Belt", line: "An old favorite, shining again." }, | |
| ]; | |
| function Hud({ index }: { index: number }) { | |
| const frame = useCurrentFrame(); | |
| const pulse = usePulse(10); | |
| const inn = useIn(2, 20); | |
| const travel = interpolate(frame, [0, 24], [index - 1, index], { ...clamp, easing: ease }); | |
| const x0 = 360; | |
| const x1 = 1560; | |
| const px = (i: number) => x0 + ((x1 - x0) * i) / (STOPS.length - 1); | |
| return ( | |
| <AbsoluteFill style={{ opacity: inn }}> | |
| <div style={{ position: "absolute", left: 80, top: 44 }}> | |
| <Lockup size={44} glow={0.6} /> | |
| </div> | |
| <div style={{ position: "absolute", right: 80, top: 56, fontFamily: theme.mono, fontSize: 20, letterSpacing: 5, color: BLUE }}> | |
| GALAXY {String(index + 1).padStart(2, "0")} / {String(STOPS.length).padStart(2, "0")} | |
| </div> | |
| <svg width={1920} height={1080} style={{ position: "absolute" }}> | |
| <line x1={x0} y1={990} x2={x1} y2={990} stroke="rgba(255,255,255,0.18)" strokeWidth={2} strokeDasharray="6 8" /> | |
| <line x1={x0} y1={990} x2={px(travel)} y2={990} stroke={BLUE} strokeWidth={3} style={{ filter: `drop-shadow(0 0 6px ${BLUE})` }} /> | |
| {STOPS.map((s, i) => { | |
| const done = i <= index; | |
| const here = i === index; | |
| return ( | |
| <g key={s.id}> | |
| <circle cx={px(i)} cy={990} r={here ? 9 + pulse * 3 : 6} fill={done ? (here ? WHITE : BLUE) : "#000"} stroke={done ? BLUE : "rgba(255,255,255,0.35)"} strokeWidth={2} | |
| style={here ? { filter: `drop-shadow(0 0 10px ${WHITE}) drop-shadow(0 0 18px ${BLUE})` } : undefined} /> | |
| <text x={px(i)} y={1030} textAnchor="middle" fontFamily={theme.mono} fontSize={15} letterSpacing={2} fill={here ? WHITE : done ? "rgba(156,196,255,0.8)" : "rgba(255,255,255,0.35)"}> | |
| {s.short.toUpperCase()} | |
| </text> | |
| </g> | |
| ); | |
| })} | |
| <circle cx={px(travel)} cy={990} r={4} fill={WHITE} /> | |
| </svg> | |
| </AbsoluteFill> | |
| ); | |
| } | |
| /** Frame for every feature stop: galaxy flying in on the right, name and line on the left, the visual on top. */ | |
| function Stop({ id, children }: { id: LaunchSceneId; children: ReactNode }) { | |
| const frame = useCurrentFrame(); | |
| const { durationInFrames } = useVideoConfig(); | |
| const index = STOPS.findIndex((s) => s.id === id); | |
| const stop = STOPS[index]; | |
| const pulse = usePulse(9); | |
| const approach = interpolate(frame, [0, durationInFrames], [0.55, 1.08], { easing: Easing.out(Easing.quad) }); | |
| const eyebrow = useIn(4, 18); | |
| const name = useIn(8, 15); | |
| const line = useIn(16, 20); | |
| const visual = useIn(10, 17); | |
| return ( | |
| <Space drift={1.4}> | |
| <div style={{ position: "absolute", inset: 0, transform: `scale(${approach})`, transformOrigin: "1350px 520px" }}> | |
| <Galaxy seed={`g-${id}`} x={1350} y={520} size={430} tilt={0.42 + random(`t-${id}`) * 0.3} arms={2 + (index % 2)} coreBoost={pulse * 0.25} /> | |
| </div> | |
| <AbsoluteFill style={{ background: "linear-gradient(90deg, rgba(0,0,0,0.85) 0%, rgba(0,0,0,0.55) 38%, transparent 62%)" }} /> | |
| <div style={{ position: "absolute", left: 120, top: 250, width: 700 }}> | |
| <p style={{ margin: 0, fontFamily: theme.mono, fontSize: 24, letterSpacing: 7, color: BLUE, opacity: eyebrow, transform: `translateX(${(1 - eyebrow) * -30}px)` }}> | |
| GALAXY {String(index + 1).padStart(2, "0")} | |
| </p> | |
| <h2 style={{ margin: "18px 0 0", fontFamily: theme.display, fontWeight: 700, fontSize: 92, lineHeight: 1.0, color: WHITE, opacity: name, transform: `translateY(${(1 - name) * 30}px)`, textShadow: `0 0 40px ${BLUE}88` }}> | |
| {stop.name} | |
| </h2> | |
| <p style={{ margin: "28px 0 0", fontFamily: theme.body, fontSize: 36, lineHeight: 1.35, color: "rgba(255,255,255,0.82)", opacity: line, transform: `translateY(${(1 - line) * 20}px)` }}> | |
| {stop.line} | |
| </p> | |
| </div> | |
| <div style={{ position: "absolute", left: 900, top: 190, width: 900, height: 660, opacity: visual, transform: `translateY(${(1 - visual) * 40}px) scale(${0.94 + visual * 0.06})` }}> | |
| {children} | |
| </div> | |
| <Hud index={index} /> | |
| </Space> | |
| ); | |
| } | |
| // ---------------------------------------------------------------- feature visuals (900 x 660 box) | |
| /** A ringed planet, like the one in the logo: lit sphere, atmosphere, and a tilted ring whose far | |
| * side passes behind the body and near side in front. Drawn in its own layer above the orbits. */ | |
| function Planet({ cx, cy, r }: { cx: number; cy: number; r: number }) { | |
| const frame = useCurrentFrame(); | |
| const tilt = -18; | |
| const rx = r * 1.75; | |
| const ry = r * 0.36; | |
| const shimmer = 0.75 + 0.25 * Math.sin(frame * 0.08); | |
| const ring = (half: "back" | "front") => ( | |
| <path | |
| d={half === "back" ? `M ${-rx} 0 A ${rx} ${ry} 0 0 1 ${rx} 0` : `M ${rx} 0 A ${rx} ${ry} 0 0 1 ${-rx} 0`} | |
| fill="none" | |
| stroke={half === "back" ? "#9cc4ff" : WHITE} | |
| strokeOpacity={half === "back" ? 0.45 : 0.95} | |
| strokeWidth={r * 0.1} | |
| strokeLinecap="round" | |
| /> | |
| ); | |
| return ( | |
| <svg width={900} height={660} style={{ position: "absolute", left: 0, top: 0, overflow: "visible", zIndex: 1 }}> | |
| <defs> | |
| <radialGradient id="planet-body" cx="36%" cy="32%" r="75%"> | |
| <stop offset="0%" stopColor="#ffffff" /> | |
| <stop offset="14%" stopColor="#bcd8ff" /> | |
| <stop offset="42%" stopColor={BLUE} /> | |
| <stop offset="78%" stopColor="#0b2f75" /> | |
| <stop offset="100%" stopColor="#020a1c" /> | |
| </radialGradient> | |
| <radialGradient id="planet-air" cx="50%" cy="50%" r="50%"> | |
| <stop offset="78%" stopColor={BLUE} stopOpacity={0} /> | |
| <stop offset="90%" stopColor={BLUE} stopOpacity={0.55} /> | |
| <stop offset="100%" stopColor={BLUE} stopOpacity={0} /> | |
| </radialGradient> | |
| <radialGradient id="planet-halo" cx="50%" cy="50%" r="50%"> | |
| <stop offset="30%" stopColor={BLUE} stopOpacity={0.3} /> | |
| <stop offset="100%" stopColor={BLUE} stopOpacity={0} /> | |
| </radialGradient> | |
| <clipPath id="planet-clip"> | |
| <circle r={r} /> | |
| </clipPath> | |
| </defs> | |
| <g transform={`translate(${cx} ${cy})`}> | |
| <circle r={r * 2.4} fill="url(#planet-halo)" opacity={shimmer} /> | |
| <g transform={`rotate(${tilt})`}>{ring("back")}</g> | |
| <circle r={r} fill="url(#planet-body)" /> | |
| {/* Soft cloud bands, clipped to the sphere, drifting slowly. */} | |
| <g clipPath="url(#planet-clip)" opacity={0.22}> | |
| {[-0.45, -0.1, 0.3].map((y, i) => ( | |
| <ellipse key={i} cx={((frame * 0.6 + i * 40) % (r * 2)) - r} cy={y * r} rx={r * 1.1} ry={r * 0.09} fill={WHITE} /> | |
| ))} | |
| </g> | |
| {/* Night side. */} | |
| <circle r={r} fill="url(#planet-shadow)" /> | |
| <radialGradient id="planet-shadow" cx="78%" cy="80%" r="70%"> | |
| <stop offset="0%" stopColor="#000" stopOpacity={0.55} /> | |
| <stop offset="100%" stopColor="#000" stopOpacity={0} /> | |
| </radialGradient> | |
| <circle r={r * 1.14} fill="url(#planet-air)" /> | |
| <g transform={`rotate(${tilt})`}>{ring("front")}</g> | |
| </g> | |
| </svg> | |
| ); | |
| } | |
| function SourcesVisual() { | |
| const frame = useCurrentFrame(); | |
| const kinds = ["Blog", "Newsletter", "Website", "Markdown", "PDF", "Substack", "Ghost", "WordPress"]; | |
| const cx = 450; | |
| const cy = 330; | |
| const count = Math.round(interpolate(frame, [40, 240], [0, 248], { ...clamp, easing: ease })); | |
| return ( | |
| <div style={{ position: "absolute", inset: 0 }}> | |
| <svg width={900} height={660} style={{ position: "absolute", overflow: "visible" }}> | |
| <ellipse cx={cx} cy={cy} rx={330} ry={130} fill="none" stroke={`${BLUE}88`} strokeWidth={2} strokeDasharray="4 10" /> | |
| <ellipse cx={cx} cy={cy} rx={220} ry={86} fill="none" stroke="rgba(255,255,255,0.2)" strokeWidth={1.5} /> | |
| </svg> | |
| <Planet cx={cx} cy={cy} r={82} /> | |
| {kinds.map((k, i) => { | |
| const arrive = interpolate(frame, [12 + i * 9, 50 + i * 9], [0, 1], { ...clamp, easing: ease }); | |
| const ring = i % 2 ? 220 : 330; | |
| const ringY = i % 2 ? 86 : 130; | |
| const a = (i / kinds.length) * Math.PI * 2 + frame * 0.012; | |
| const tx = cx + Math.cos(a) * ring; | |
| const ty = cy + Math.sin(a) * ringY; | |
| const sx = cx + Math.cos(a) * 1100; | |
| const sy = cy + Math.sin(a) * 700; | |
| const x = sx + (tx - sx) * arrive; | |
| const y = sy + (ty - sy) * arrive; | |
| const front = Math.sin(a) > 0; | |
| return ( | |
| <div key={k} style={{ position: "absolute", left: x, top: y, transform: `translate(-50%, -50%) scale(${front ? 1 : 0.82})`, opacity: arrive * (front ? 1 : 0.7), zIndex: front ? 2 : 0, padding: "12px 22px", borderRadius: 999, background: front ? "rgba(0,0,0,0.85)" : "rgba(0,0,0,0.6)", border: `1.5px solid ${front ? BLUE : "rgba(255,255,255,0.3)"}`, color: WHITE, fontFamily: theme.body, fontWeight: 600, fontSize: 24, whiteSpace: "nowrap", boxShadow: front ? `0 0 24px ${BLUE}66` : undefined }}> | |
| {k} | |
| </div> | |
| ); | |
| })} | |
| <div style={{ position: "absolute", left: 0, right: 0, bottom: 0, textAlign: "center", fontFamily: theme.mono, fontSize: 28, letterSpacing: 3, color: WHITE }}> | |
| <span style={{ color: BLUE }}>{count}</span> POSTS IN ORBIT | |
| </div> | |
| </div> | |
| ); | |
| } | |
| function ResearchVisual() { | |
| const frame = useCurrentFrame(); | |
| const q = "What have I said about pricing?"; | |
| const answer = "You argue for charging early and raising prices with each new tier"; | |
| const cite1 = useIn(62, 14); | |
| const cite2 = useIn(72, 14); | |
| const lines = useIn(80, 18); | |
| return ( | |
| <div style={{ ...panel, position: "absolute", left: 40, right: 0, top: 20, padding: 36, display: "grid", gap: 22, alignContent: "start" }}> | |
| <div style={{ justifySelf: "end", padding: "14px 22px", borderRadius: 18, background: BLUE, color: WHITE, fontFamily: theme.body, fontSize: 28 }}> | |
| {typed(q, frame, 4, 34)} | |
| <span style={{ opacity: frame % 20 < 10 ? 1 : 0 }}>|</span> | |
| </div> | |
| <div style={{ fontFamily: theme.body, fontSize: 28, lineHeight: 1.5, color: WHITE, minHeight: 90 }}> | |
| {typed(answer, frame, 30, 40)} | |
| <span style={{ display: "inline-block", marginLeft: 10, padding: "0 10px", borderRadius: 8, background: BLUE, fontFamily: theme.mono, fontSize: 20, transform: `scale(${cite1})` }}>1</span> | |
| <span style={{ display: "inline-block", marginLeft: 8, padding: "0 10px", borderRadius: 8, background: BLUE, fontFamily: theme.mono, fontSize: 20, transform: `scale(${cite2})` }}>2</span> | |
| </div> | |
| <div style={{ opacity: lines, transform: `translateY(${(1 - lines) * 20}px)`, border: "1px solid rgba(255,255,255,0.14)", borderRadius: 16, padding: 20, fontFamily: theme.mono, fontSize: 19, lineHeight: 1.7 }}> | |
| <div style={{ color: BLUE, marginBottom: 6 }}>sources/blog/2024-03-11-price-early.md</div> | |
| {["12 Charge from day one. Free teaches", "13 people your work is worth nothing.", "14 Raise the price with every tier you", "15 add, not every year."].map((l, i) => ( | |
| <div key={l} style={{ color: i < 3 ? WHITE : "rgba(255,255,255,0.55)", background: i < 3 ? `${BLUE}33` : undefined, borderLeft: i < 3 ? `3px solid ${BLUE}` : "3px solid transparent", paddingLeft: 12 }}> | |
| {l} | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| ); | |
| } | |
| function MapVisual() { | |
| const frame = useCurrentFrame(); | |
| const pulse = usePulse(6); | |
| const nodes = [ | |
| { x: 450, y: 320, r: 26, name: "Writing" }, | |
| { x: 250, y: 200, r: 16, name: "Pricing" }, | |
| { x: 660, y: 190, r: 20, name: "AI tools", rising: true }, | |
| { x: 700, y: 430, r: 14, name: "Newsletters" }, | |
| { x: 230, y: 470, r: 12, name: "Habits", dormant: true }, | |
| { x: 470, y: 560, r: 15, name: "Audience" }, | |
| { x: 120, y: 330, r: 9, name: "Design" }, | |
| { x: 820, y: 300, r: 10, name: "Research" }, | |
| ]; | |
| const edges = [[0, 1], [0, 2], [0, 3], [0, 4], [0, 5], [1, 6], [2, 7], [3, 5], [2, 3], [1, 4]]; | |
| return ( | |
| <svg width={900} height={660} style={{ position: "absolute", overflow: "visible" }}> | |
| <defs> | |
| <radialGradient id="map-core"> | |
| <stop offset="0%" stopColor={WHITE} stopOpacity={0.75} /> | |
| <stop offset="20%" stopColor={WHITE} stopOpacity={0.3} /> | |
| <stop offset="55%" stopColor={BLUE} stopOpacity={0.1} /> | |
| <stop offset="100%" stopColor={BLUE} stopOpacity={0} /> | |
| </radialGradient> | |
| </defs> | |
| <circle cx={450} cy={330} r={260 + pulse * 10} fill="url(#map-core)" /> | |
| {edges.map(([a, b], i) => { | |
| const p = interpolate(frame, [18 + i * 5, 40 + i * 5], [0, 1], clamp); | |
| const A = nodes[a]; | |
| const B = nodes[b]; | |
| return <line key={i} x1={A.x} y1={A.y} x2={A.x + (B.x - A.x) * p} y2={A.y + (B.y - A.y) * p} stroke={WHITE} strokeOpacity={0.45} strokeWidth={2} />; | |
| })} | |
| {nodes.map((n, i) => { | |
| const s = spring({ frame: frame - 6 - i * 4, fps: LAUNCH_FPS, config: { damping: 12 } }); | |
| return ( | |
| <g key={n.name} opacity={Math.min(1, s)}> | |
| <circle cx={n.x} cy={n.y} r={n.r * 2.4 * s} fill={BLUE} opacity={0.25} /> | |
| <circle cx={n.x} cy={n.y} r={n.r * 0.6 * s} fill={n.rising ? WHITE : n.dormant ? "rgba(255,255,255,0.45)" : "#9cc4ff"} style={n.rising ? { filter: `drop-shadow(0 0 12px ${WHITE})` } : undefined} /> | |
| {n.dormant && <circle cx={n.x} cy={n.y} r={n.r + 8} fill="none" stroke={WHITE} strokeOpacity={0.6} strokeDasharray="3 5" strokeWidth={1.5} />} | |
| <text x={n.x} y={n.y + n.r + 30} textAnchor="middle" fontFamily={theme.body} fontWeight={600} fontSize={22} fill={WHITE} stroke="#000" strokeWidth={5} paintOrder="stroke"> | |
| {n.name} | |
| </text> | |
| </g> | |
| ); | |
| })} | |
| <g opacity={interpolate(frame, [80, 96], [0, 1], clamp)}> | |
| <rect x={700} y={100} width={190} height={46} rx={23} fill={WHITE} /> | |
| <text x={795} y={131} textAnchor="middle" fontFamily={theme.mono} fontSize={20} fill="#000">RISING 2.4x</text> | |
| </g> | |
| <g opacity={interpolate(frame, [110, 126], [0, 1], clamp)}> | |
| <rect x={80} y={520} width={170} height={46} rx={23} fill="#000" stroke="rgba(255,255,255,0.6)" strokeDasharray="4 5" /> | |
| <text x={165} y={551} textAnchor="middle" fontFamily={theme.mono} fontSize={20} fill={WHITE}>DORMANT</text> | |
| </g> | |
| </svg> | |
| ); | |
| } | |
| function VoiceVisual() { | |
| const frame = useCurrentFrame(); | |
| const traits = [["Tone", "warm, direct"], ["Openers", "a sharp question"], ["Rhythm", "short, punchy lines"], ["Avoids", "jargon, hype"]]; | |
| const match = Math.round(interpolate(frame, [70, 140], [0, 96], { ...clamp, easing: ease })); | |
| return ( | |
| <div style={{ ...panel, position: "absolute", left: 40, right: 0, top: 30, padding: 40, display: "grid", gap: 26, alignContent: "start" }}> | |
| <svg width={780} height={150}> | |
| {Array.from({ length: 60 }).map((_, i) => { | |
| const h = 16 + Math.abs(Math.sin(i * 0.55 + frame * 0.22) * Math.sin(i * 0.13 + frame * 0.07)) * 120; | |
| return <rect key={i} x={i * 13} y={75 - h / 2} width={7} height={h} rx={3.5} fill={i % 7 === 0 ? WHITE : BLUE} opacity={0.5 + 0.5 * (h / 136)} />; | |
| })} | |
| </svg> | |
| <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 14 }}> | |
| {traits.map(([k, v], i) => { | |
| const s = spring({ frame: frame - 20 - i * 10, fps: LAUNCH_FPS, config: { damping: 15 } }); | |
| return ( | |
| <div key={k} style={{ opacity: s, transform: `translateY(${(1 - s) * 16}px)`, border: `1px solid ${BLUE}88`, borderRadius: 14, padding: "14px 18px", fontFamily: theme.body, fontSize: 24, color: WHITE }}> | |
| <span style={{ fontFamily: theme.mono, fontSize: 18, color: BLUE, letterSpacing: 2 }}>{k.toUpperCase()}</span> | |
| <br /> | |
| {v} | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| <div style={{ fontFamily: theme.mono, fontSize: 28, color: WHITE, letterSpacing: 2 }}> | |
| VOICE MATCH <span style={{ color: BLUE }}>{match}%</span> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| function AudioVisual() { | |
| const frame = useCurrentFrame(); | |
| const speaker = Math.floor(frame / 36) % 2; | |
| const lines = ["So the big idea here is simple.", "Charge early. It changes who shows up.", "And it is all from their own posts.", "Every line, cited."]; | |
| const host = (label: string, active: boolean, x: number) => ( | |
| <div style={{ position: "absolute", left: x, top: 120, width: 170, height: 170, borderRadius: "50%", display: "grid", placeItems: "center", background: active ? WHITE : "#000", color: active ? "#000" : WHITE, border: `3px solid ${BLUE}`, fontFamily: theme.display, fontWeight: 700, fontSize: 64, boxShadow: active ? `0 0 0 ${10 + Math.sin(frame * 0.5) * 6}px ${BLUE}55, 0 0 60px ${BLUE}` : undefined }}> | |
| {label} | |
| </div> | |
| ); | |
| return ( | |
| <div style={{ position: "absolute", inset: 0 }}> | |
| {host("A", speaker === 0, 60)} | |
| {host("B", speaker === 1, 670)} | |
| <svg width={900} height={660} style={{ position: "absolute" }}> | |
| {Array.from({ length: 36 }).map((_, i) => { | |
| const h = 10 + Math.abs(Math.sin(i * 0.8 + frame * 0.35) * Math.cos(i * 0.21 - frame * 0.1)) * 150; | |
| return <rect key={i} x={265 + i * 10.5} y={205 - h / 2} width={6} height={h} rx={3} fill={BLUE} opacity={0.85} />; | |
| })} | |
| </svg> | |
| <div style={{ ...panel, position: "absolute", left: 60, right: 50, top: 400, padding: "26px 32px", fontFamily: theme.body, fontSize: 32, color: WHITE }}> | |
| <span style={{ fontFamily: theme.mono, fontSize: 20, color: BLUE, letterSpacing: 3 }}>HOST {speaker ? "B" : "A"}</span> | |
| <br /> | |
| {lines[Math.floor(frame / 36) % lines.length]} | |
| </div> | |
| </div> | |
| ); | |
| } | |
| function VideoVisual() { | |
| const frame = useCurrentFrame(); | |
| const card = (delay: number, style: CSSProperties, label: string, body: ReactNode) => { | |
| const s = spring({ frame: frame - delay, fps: LAUNCH_FPS, config: { damping: 14 } }); | |
| return ( | |
| <div style={{ ...panel, position: "absolute", overflow: "hidden", opacity: s, transform: `translateY(${(1 - s) * 80}px) rotate(${(1 - s) * 4}deg)`, ...style }}> | |
| <div style={{ position: "absolute", inset: 0, background: `radial-gradient(80% 60% at 50% 110%, ${BLUE}55, transparent 70%)` }} /> | |
| <div style={{ position: "absolute", left: 18, top: 14, fontFamily: theme.mono, fontSize: 16, letterSpacing: 3, color: BLUE }}>{label}</div> | |
| <div style={{ position: "absolute", inset: "50px 22px 22px", display: "grid", placeItems: "center", textAlign: "center", color: WHITE, fontFamily: theme.display, fontWeight: 600 }}>{body}</div> | |
| </div> | |
| ); | |
| }; | |
| const play = <div style={{ width: 0, height: 0, borderLeft: `34px solid ${WHITE}`, borderTop: "20px solid transparent", borderBottom: "20px solid transparent" }} />; | |
| return ( | |
| <div style={{ position: "absolute", inset: 0 }}> | |
| {card(4, { left: 40, top: 60, width: 250, height: 444 }, "SHORT 9:16", <div style={{ fontSize: 34, lineHeight: 1.15 }}>Your archive already is a podcast</div>)} | |
| {card(14, { left: 320, top: 40, width: 520, height: 292 }, "EXPLAINER 16:9", <div style={{ display: "grid", gap: 16, placeItems: "center" }}>{play}<span style={{ fontSize: 30 }}>Why pricing early works</span></div>)} | |
| {card(24, { left: 320, top: 360, width: 250, height: 250 }, "AUDIOGRAM 1:1", | |
| <svg width={200} height={120}>{Array.from({ length: 16 }).map((_, i) => { const h = 12 + Math.abs(Math.sin(i + frame * 0.3)) * 90; return <rect key={i} x={i * 12.5} y={60 - h / 2} width={7} height={h} rx={3} fill={i % 4 ? BLUE : WHITE} />; })}</svg>)} | |
| {card(34, { left: 590, top: 360, width: 250, height: 250 }, "QUOTE CARD", <div style={{ fontSize: 26, lineHeight: 1.25 }}>"Free teaches people your work is worth nothing."</div>)} | |
| </div> | |
| ); | |
| } | |
| function LaunchKitVisual() { | |
| const frame = useCurrentFrame(); | |
| const items = [["X THREAD", "1/7 Most writers undercharge. Here is why."], ["LINKEDIN", "I raised my prices three times last year."], ["SUBSTACK NOTES", "Charge from day one. Seriously."], ["BLUESKY", "Pricing is a signal, not a number."], ["SEO PACK", "Title, meta, slug, keywords, links"], ["CAROUSEL", "8 slides: How to price your writing"]]; | |
| return ( | |
| <div style={{ position: "absolute", inset: "20px 0 0 30px", display: "grid", gridTemplateColumns: "1fr 1fr", gap: 18 }}> | |
| {items.map(([k, v], i) => { | |
| const s = spring({ frame: frame - 8 - i * S(BEAT), fps: LAUNCH_FPS, config: { damping: 13 } }); | |
| return ( | |
| <div key={k} style={{ ...panel, opacity: Math.min(1, s), transform: `translateY(${(1 - s) * 60}px) scale(${0.9 + 0.1 * s})`, padding: "22px 24px", minHeight: 160 }}> | |
| <div style={{ fontFamily: theme.mono, fontSize: 18, letterSpacing: 3, color: BLUE }}>{k}</div> | |
| <div style={{ marginTop: 12, fontFamily: theme.body, fontSize: 26, lineHeight: 1.35, color: WHITE }}>{v}</div> | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| ); | |
| } | |
| function LaunchpadVisual() { | |
| const frame = useCurrentFrame(); | |
| const days = ["MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN"]; | |
| const posts: [number, number, string][] = [[0, 0, "X"], [1, 1, "LinkedIn"], [2, 0, "Bluesky"], [3, 2, "X"], [4, 1, "Notes"], [5, 0, "LinkedIn"], [6, 2, "X"]]; | |
| const lift = interpolate(frame, [120, 200], [0, 1], { ...clamp, easing: Easing.in(Easing.cubic) }); | |
| return ( | |
| <div style={{ position: "absolute", inset: 0 }}> | |
| <div style={{ ...panel, position: "absolute", left: 30, top: 20, width: 620, padding: 26 }}> | |
| <div style={{ display: "grid", gridTemplateColumns: "repeat(7, 1fr)", gap: 10 }}> | |
| {days.map((d) => ( | |
| <div key={d} style={{ fontFamily: theme.mono, fontSize: 16, color: "rgba(255,255,255,0.6)", textAlign: "center" }}>{d}</div> | |
| ))} | |
| {Array.from({ length: 21 }).map((_, cell) => { | |
| const col = cell % 7; | |
| const row = Math.floor(cell / 7); | |
| const post = posts.find(([c, r]) => c === col && r === row); | |
| const s = post ? spring({ frame: frame - 10 - posts.indexOf(post) * 8, fps: LAUNCH_FPS, config: { damping: 12 } }) : 0; | |
| const sent = post && frame > 90 + posts.indexOf(post) * 5; | |
| return ( | |
| <div key={cell} style={{ height: 96, borderRadius: 12, border: "1px solid rgba(255,255,255,0.12)", display: "grid", placeItems: "center" }}> | |
| {post && ( | |
| <div style={{ transform: `scale(${s})`, padding: "6px 8px", borderRadius: 8, background: sent ? WHITE : BLUE, color: sent ? "#000" : WHITE, fontFamily: theme.body, fontWeight: 600, fontSize: 15 }}> | |
| {post[2]} | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| <div style={{ marginTop: 18, fontFamily: theme.mono, fontSize: 20, letterSpacing: 2, color: frame > 100 ? WHITE : "rgba(255,255,255,0.6)" }}> | |
| {frame > 100 ? "7 POSTS LIVE" : "7 POSTS SCHEDULED"} | |
| </div> | |
| </div> | |
| <svg width={260} height={660} style={{ position: "absolute", left: 660, top: 0, overflow: "visible" }}> | |
| <g transform={`translate(130 ${470 - lift * 700})`}> | |
| <path d={`M -14 60 Q 0 ${120 + lift * 260} 14 60 Z`} fill={WHITE} opacity={0.5 + lift * 0.5} style={{ filter: `drop-shadow(0 0 16px ${BLUE})` }} /> | |
| <path d="M 0 -80 C 34 -40 34 20 24 60 L -24 60 C -34 20 -34 -40 0 -80 Z" fill={WHITE} /> | |
| <circle cx={0} cy={-18} r={12} fill={BLUE} /> | |
| <path d="M -24 30 L -46 70 L -22 58 Z M 24 30 L 46 70 L 22 58 Z" fill={BLUE} /> | |
| </g> | |
| <line x1={40} y1={560} x2={220} y2={560} stroke="rgba(255,255,255,0.4)" strokeWidth={3} /> | |
| </svg> | |
| </div> | |
| ); | |
| } | |
| function ResurfaceVisual() { | |
| const frame = useCurrentFrame(); | |
| const years = [2019, 2020, 2021, 2022, 2023, 2024, 2025, 2026]; | |
| const rise = interpolate(frame, [30, 80], [0, 1], { ...clamp, easing: ease }); | |
| const score = Math.round(interpolate(frame, [60, 110], [0, 92], clamp)); | |
| const x = (i: number) => 60 + i * 110; | |
| return ( | |
| <div style={{ position: "absolute", inset: 0 }}> | |
| <svg width={900} height={660} style={{ position: "absolute" }}> | |
| <line x1={40} y1={560} x2={860} y2={560} stroke="rgba(255,255,255,0.3)" strokeWidth={2} /> | |
| {years.map((y, i) => ( | |
| <g key={y}> | |
| <circle cx={x(i)} cy={560} r={6} fill={i === 2 ? WHITE : BLUE} /> | |
| <text x={x(i)} y={600} textAnchor="middle" fontFamily={theme.mono} fontSize={18} fill="rgba(255,255,255,0.6)">{y}</text> | |
| </g> | |
| ))} | |
| <path d={`M ${x(2)} 560 C ${x(2)} ${560 - 200 * rise}, ${x(4)} ${420 - 200 * rise}, ${x(4)} ${360 - 180 * rise}`} fill="none" stroke={WHITE} strokeOpacity={0.6} strokeWidth={2} strokeDasharray="5 7" /> | |
| </svg> | |
| <div style={{ ...panel, position: "absolute", left: x(2) + 60 + rise * 140, top: 420 - rise * 330, width: 480, padding: 26, boxShadow: `0 0 ${40 + rise * 60}px ${BLUE}${rise > 0.5 ? "99" : "44"}` }}> | |
| <div style={{ fontFamily: theme.mono, fontSize: 18, color: BLUE, letterSpacing: 2 }}>MARCH 2021</div> | |
| <div style={{ marginTop: 10, fontFamily: theme.display, fontWeight: 600, fontSize: 34, color: WHITE, lineHeight: 1.15 }}>The quiet power of a small audience</div> | |
| <div style={{ marginTop: 18, display: "flex", gap: 14, fontFamily: theme.mono, fontSize: 20 }}> | |
| <span style={{ padding: "6px 14px", borderRadius: 999, background: WHITE, color: "#000" }}>EVERGREEN {score}</span> | |
| <span style={{ padding: "6px 14px", borderRadius: 999, border: `1px solid ${BLUE}`, color: WHITE }}>RESHARE</span> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| // ---------------------------------------------------------------- bookend scenes | |
| function Intro() { | |
| const frame = useCurrentFrame(); | |
| const a = useIn(24, 22); | |
| const b = useIn(S(3.4), 22); | |
| const c = useIn(S(6.2), 20); | |
| const push = interpolate(frame, [0, S(8)], [1, 1.12]); | |
| return ( | |
| <Space drift={0.4}> | |
| <div style={{ position: "absolute", inset: 0, transform: `scale(${push})`, opacity: interpolate(frame, [0, 40], [0, 1], clamp) }}> | |
| <Galaxy seed="far-1" x={1500} y={260} size={150} tilt={0.35} /> | |
| <Galaxy seed="far-2" x={330} y={820} size={110} tilt={0.6} arms={3} /> | |
| <Galaxy seed="far-3" x={1650} y={860} size={70} tilt={0.5} /> | |
| <Galaxy seed="far-4" x={260} y={200} size={60} tilt={0.7} /> | |
| </div> | |
| <AbsoluteFill style={{ justifyContent: "center", alignItems: "center", textAlign: "center", gap: 28 }}> | |
| <h1 style={{ margin: 0, fontFamily: theme.display, fontWeight: 600, fontSize: 80, color: WHITE, opacity: a, transform: `translateY(${(1 - a) * 30}px)` }}> | |
| At the edge of something vast. | |
| </h1> | |
| <h2 style={{ margin: 0, fontFamily: theme.display, fontWeight: 500, fontSize: 64, color: "rgba(255,255,255,0.75)", opacity: b, transform: `translateY(${(1 - b) * 24}px)` }}> | |
| Curious. <span style={{ color: WHITE, textShadow: `0 0 30px ${BLUE}, 0 0 70px ${BLUE}` }}>A little breathless.</span> | |
| </h2> | |
| <p style={{ margin: "30px 0 0", fontFamily: theme.mono, fontSize: 24, letterSpacing: 8, color: BLUE, opacity: c }}>A JOURNEY OF KNOWLEDGE</p> | |
| </AbsoluteFill> | |
| </Space> | |
| ); | |
| } | |
| function Launch() { | |
| const frame = useCurrentFrame(); | |
| const speed = interpolate(frame, [0, 20, 90, 120], [2, 22, 18, 8], clamp); | |
| const streak = interpolate(frame, [0, 12, 95, 120], [0, 1, 1, 0.3], clamp); | |
| const title = useIn(10, 14); | |
| const sub = useIn(28, 18); | |
| const shake = frame < 14 ? Math.sin(frame * 3.1) * (14 - frame) * 0.8 : 0; | |
| return ( | |
| <AbsoluteFill style={{ transform: `translate(${shake}px, ${shake * 0.6}px)` }}> | |
| <WarpField speed={speed} streak={streak} count={520} /> | |
| <AbsoluteFill style={{ background: `radial-gradient(circle at 50% 50%, rgba(255,255,255,${interpolate(frame, [0, 10], [0.8, 0], clamp)}), transparent 60%)` }} /> | |
| <AbsoluteFill style={{ justifyContent: "center", alignItems: "center", textAlign: "center", gap: 26 }}> | |
| <h1 style={{ margin: 0, fontFamily: theme.display, fontWeight: 700, fontSize: 132, lineHeight: 1, color: WHITE, opacity: title, transform: `scale(${0.85 + title * 0.15})`, textShadow: `0 0 50px ${BLUE}` }}> | |
| A journey of knowledge | |
| </h1> | |
| <p style={{ margin: 0, fontFamily: theme.body, fontSize: 40, color: "rgba(255,255,255,0.85)", opacity: sub, transform: `translateY(${(1 - sub) * 20}px)` }}> | |
| exploring the galaxies of untapped potential | |
| </p> | |
| </AbsoluteFill> | |
| <Flare at={6} duration={30} y={47} /> | |
| </AbsoluteFill> | |
| ); | |
| } | |
| function Montage() { | |
| const frame = useCurrentFrame(); | |
| const { durationInFrames } = useVideoConfig(); | |
| const speed = interpolate(frame, [0, durationInFrames], [4, 40], { easing: Easing.in(Easing.quad) }); | |
| const lit = Math.floor(frame / S(BEAT / 2)); // one galaxy per eighth note | |
| const whiteout = interpolate(frame, [durationInFrames - 14, durationInFrames], [0, 1], clamp); | |
| const go = useIn(S(2.6), 12); | |
| return ( | |
| <AbsoluteFill> | |
| <WarpField speed={speed} streak={interpolate(frame, [0, 60], [0.2, 1], clamp)} count={560} /> | |
| <AbsoluteFill style={{ justifyContent: "center", alignItems: "center" }}> | |
| <div style={{ display: "grid", gridTemplateColumns: "repeat(3, 440px)", gap: 22 }}> | |
| {STOPS.map((s, i) => { | |
| const on = i < lit; | |
| return ( | |
| <div key={s.id} style={{ ...panel, padding: "22px 26px", opacity: on ? 1 : 0.18, borderColor: on ? BLUE : "rgba(255,255,255,0.16)", background: on ? "rgba(0,0,0,0.8)" : "rgba(0,0,0,0.4)", boxShadow: on ? `0 0 40px ${BLUE}88` : "none", transform: `scale(${on ? 1 : 0.95})` }}> | |
| <div style={{ fontFamily: theme.mono, fontSize: 18, letterSpacing: 3, color: BLUE }}>GALAXY {String(i + 1).padStart(2, "0")}</div> | |
| <div style={{ marginTop: 6, fontFamily: theme.display, fontWeight: 600, fontSize: 32, color: WHITE }}>{s.short}</div> | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| <p style={{ marginTop: 44, fontFamily: theme.mono, fontSize: 34, letterSpacing: 12, color: WHITE, opacity: go, textShadow: `0 0 24px ${BLUE}` }}>ALL SYSTEMS GO</p> | |
| </AbsoluteFill> | |
| <AbsoluteFill style={{ background: WHITE, opacity: whiteout }} /> | |
| </AbsoluteFill> | |
| ); | |
| } | |
| function Outro() { | |
| const frame = useCurrentFrame(); | |
| const { durationInFrames } = useVideoConfig(); | |
| const flash = interpolate(frame, [0, 22], [1, 0], clamp); | |
| const logo = useIn(4, 13); | |
| const name = useIn(12, 15); | |
| const soon = useIn(S(3.2), 12); | |
| const tag = useIn(S(4.4), 20); | |
| const pulse = usePulse(5); | |
| const end = interpolate(frame, [durationInFrames - 30, durationInFrames], [1, 0], clamp); | |
| const letters = "COMING SOON".split(""); | |
| return ( | |
| <AbsoluteFill style={{ opacity: end }}> | |
| <Space drift={0.3}> | |
| <div style={{ position: "absolute", inset: 0, transform: `scale(${interpolate(frame, [0, durationInFrames], [1.15, 1])})`, transformOrigin: "960px 540px" }}> | |
| <Galaxy seed="home" x={960} y={560} size={620} tilt={0.3} arms={2} spin={0.6} coreBoost={0.15 + pulse * 0.1} /> | |
| </div> | |
| <AbsoluteFill style={{ background: "radial-gradient(45% 45% at 50% 50%, rgba(0,0,0,0.55), transparent 80%)" }} /> | |
| <AbsoluteFill style={{ justifyContent: "center", alignItems: "center", textAlign: "center" }}> | |
| <div style={{ opacity: Math.min(1, logo), transform: `scale(${0.85 + 0.15 * logo}) translateY(${(1 - name) * 20}px)` }}> | |
| <Lockup size={176} suffix="AI" glow={1 + pulse * 0.3} /> | |
| </div> | |
| <div style={{ marginTop: 44, display: "flex", gap: 6 }}> | |
| {letters.map((ch, i) => { | |
| const s = spring({ frame: frame - S(3.2) - i * 2, fps: LAUNCH_FPS, config: { damping: 11 } }); | |
| return ( | |
| <span key={i} style={{ display: "inline-block", minWidth: ch === " " ? 30 : undefined, fontFamily: theme.mono, fontWeight: 400, fontSize: 64, letterSpacing: 10, color: WHITE, opacity: Math.min(1, s) * soon, transform: `translateY(${(1 - s) * 40}px)`, textShadow: `0 0 ${20 + pulse * 20}px ${BLUE}, 0 0 60px ${BLUE}` }}> | |
| {ch} | |
| </span> | |
| ); | |
| })} | |
| </div> | |
| <p style={{ margin: "40px 0 0", fontFamily: theme.body, fontSize: 34, color: "rgba(255,255,255,0.8)", opacity: tag }}> | |
| Your knowledge, in orbit. <span style={{ color: BLUE }}>notestack.ai</span> | |
| </p> | |
| </AbsoluteFill> | |
| <Flare at={S(3.1)} duration={34} y={64} /> | |
| </Space> | |
| <AbsoluteFill style={{ background: WHITE, opacity: flash }} /> | |
| </AbsoluteFill> | |
| ); | |
| } | |
| // ---------------------------------------------------------------- composition | |
| const RENDER: Record<LaunchSceneId, () => JSX.Element> = { | |
| intro: Intro, | |
| launch: Launch, | |
| sources: () => <Stop id="sources"><SourcesVisual /></Stop>, | |
| research: () => <Stop id="research"><ResearchVisual /></Stop>, | |
| map: () => <Stop id="map"><MapVisual /></Stop>, | |
| voice: () => <Stop id="voice"><VoiceVisual /></Stop>, | |
| audio: () => <Stop id="audio"><AudioVisual /></Stop>, | |
| video: () => <Stop id="video"><VideoVisual /></Stop>, | |
| launchkit: () => <Stop id="launchkit"><LaunchKitVisual /></Stop>, | |
| launchpad: () => <Stop id="launchpad"><LaunchpadVisual /></Stop>, | |
| resurface: () => <Stop id="resurface"><ResurfaceVisual /></Stop>, | |
| montage: Montage, | |
| outro: Outro, | |
| }; | |
| function FadeIn({ skip, children }: { skip: boolean; children: ReactNode }) { | |
| const frame = useCurrentFrame(); | |
| const opacity = skip ? 1 : interpolate(frame, [0, S(XF)], [0, 1], clamp); | |
| return <AbsoluteFill style={{ opacity }}>{children}</AbsoluteFill>; | |
| } | |
| export function LaunchTrailer() { | |
| return ( | |
| <AbsoluteFill style={{ backgroundColor: theme.black }}> | |
| <SoundTrack src={staticFile("launch-vo/music.wav")} volume={musicVolume} /> | |
| {LAUNCH_SCENES.map((scene, i) => { | |
| const last = i === LAUNCH_SCENES.length - 1; | |
| const Scene = RENDER[scene.id]; | |
| // Hard cut into the outro (the montage whites out); crossfade everywhere else. | |
| const skipFade = i === 0 || scene.id === "outro" || scene.id === "launch"; | |
| return ( | |
| <Sequence key={scene.id} from={S(scene.start)} durationInFrames={S(scene.len + (last ? 0 : XF))} name={scene.id}> | |
| <FadeIn skip={skipFade}> | |
| <Scene /> | |
| </FadeIn> | |
| {scene.vo !== null && ( | |
| <Sequence from={S(scene.vo)} name={`vo-${scene.id}`}> | |
| <SoundTrack src={staticFile(`launch-vo/${scene.id}.mp3`)} volume={1} /> | |
| </Sequence> | |
| )} | |
| </Sequence> | |
| ); | |
| })} | |
| </AbsoluteFill> | |
| ); | |
| } | |