marketing: v3.1 launch video — 30s Remotion render

Six-scene 30-second MP4 announcing v3.1 (competitors mode) for X.

Lives at marketing/v3.1-launch/. Renders to out/last30days-v3.1-launch.mp4
(gitignored). 1920×1080 @ 30fps, H.264, ~3MB final.

Six scenes:
1. Hook (0-3s): badge + "What if one search ran 3 at once?"
2. Old way (3-8s): single terminal /last30days OpenAI
3. Fan-out (8-14s): --competitors splits into 3 parallel panes
4. Comparison (14-21s): 3 panes collapse into Head-to-Head table
5. How (21-26s): "You pick the topic. Agent picks peers. Engine fans out."
6. CTA (26-30s): install command + repo URL

Reusable components: TerminalWindow, TypedLine, BadgeBar,
ComparisonTable. Scene timing in src/lib/timing.ts as single source of
truth — one edit to retime everything.

Marketing version 3.1 ≠ engine code version (still 3.0.14). 3.1 is the
launch label that bundles 3.0.11-3.0.14 into one shippable narrative.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matt Van Horn
2026-04-22 21:48:02 -07:00
parent 949bcf8942
commit b31ec05c74
21 changed files with 1561 additions and 0 deletions
+37
View File
@@ -0,0 +1,37 @@
# /last30days v3.1 Launch Video
30-second Remotion-rendered MP4 announcing **v3.1: Competitors mode** for posting on X.
## Quick start
```bash
cd marketing/v3.1-launch
npm install # one-time, ~200MB of node_modules
npm run preview # opens Remotion Studio at localhost:3000 to scrub frames
npm run render # writes out/last30days-v3.1-launch.mp4 (high quality, CRF 18)
npm run render:fast # writes a CRF 23 preview for fast iteration
```
## Specs
- 1920×1080, 30fps, 30 seconds (900 frames)
- H.264 / MP4
- Silent (autoplay-muted-friendly; captions baked in)
- Marketing label `v3.1` (engine code version stays 3.0.14)
## Scene timing (single source of truth: `src/lib/timing.ts`)
| Scene | Frames | Time | What |
|-------|--------|------|------|
| 1. Hook | 0-89 | 0.0-3.0s | Badge animates in + "What if one search ran 3 at once?" |
| 2. Old way | 90-239 | 3.0-8.0s | Single terminal: `/last30days OpenAI` |
| 3. Fan-out | 240-419 | 8.0-14.0s | `--competitors` types in → splits into 3 panes |
| 4. Comparison | 420-629 | 14.0-21.0s | 3 panes collapse into Head-to-Head table |
| 5. How | 630-779 | 21.0-26.0s | 3-line text card |
| 6. CTA | 780-899 | 26.0-30.0s | Install command + repo URL |
Edit `src/lib/timing.ts` to retime scenes; the `LaunchVideo` composition reads from there.
## Output
Rendered MP4 lives at `out/last30days-v3.1-launch.mp4` (gitignored). Upload directly to X.
+23
View File
@@ -0,0 +1,23 @@
{
"name": "last30days-v3-1-launch-video",
"version": "0.1.0",
"private": true,
"description": "30s Remotion launch video for /last30days v3.1 (competitors mode).",
"scripts": {
"preview": "remotion studio src/index.ts",
"render": "remotion render src/index.ts LaunchVideo out/last30days-v3.1-launch.mp4 --codec=h264 --crf=18",
"render:fast": "remotion render src/index.ts LaunchVideo out/last30days-v3.1-launch-preview.mp4 --codec=h264 --crf=23"
},
"dependencies": {
"react": "19.0.0",
"react-dom": "19.0.0",
"remotion": "4.0.250",
"@remotion/cli": "4.0.250",
"@remotion/google-fonts": "4.0.250"
},
"devDependencies": {
"@types/react": "19.0.0",
"@types/node": "22.10.0",
"typescript": "5.6.3"
}
}
+6
View File
@@ -0,0 +1,6 @@
import { Config } from "@remotion/cli/config";
Config.setVideoImageFormat("jpeg");
Config.setOverwriteOutput(true);
Config.setConcurrency(null);
Config.setCodec("h264");
+35
View File
@@ -0,0 +1,35 @@
import React from "react";
import { AbsoluteFill, Sequence } from "remotion";
import { SCENES } from "./lib/timing";
import { Scene1Hook } from "./scenes/Scene1Hook";
import { Scene2OldWay } from "./scenes/Scene2OldWay";
import { Scene3FanOut } from "./scenes/Scene3FanOut";
import { Scene4Comparison } from "./scenes/Scene4Comparison";
import { Scene5HowItWorks } from "./scenes/Scene5HowItWorks";
import { Scene6CTA } from "./scenes/Scene6CTA";
import { COLORS } from "./lib/colors";
export const LaunchVideo: React.FC = () => {
return (
<AbsoluteFill style={{ background: COLORS.bgDeep }}>
<Sequence from={SCENES.hook.from} durationInFrames={SCENES.hook.durationInFrames}>
<Scene1Hook />
</Sequence>
<Sequence from={SCENES.oldWay.from} durationInFrames={SCENES.oldWay.durationInFrames}>
<Scene2OldWay />
</Sequence>
<Sequence from={SCENES.fanOut.from} durationInFrames={SCENES.fanOut.durationInFrames}>
<Scene3FanOut />
</Sequence>
<Sequence from={SCENES.comparison.from} durationInFrames={SCENES.comparison.durationInFrames}>
<Scene4Comparison />
</Sequence>
<Sequence from={SCENES.howItWorks.from} durationInFrames={SCENES.howItWorks.durationInFrames}>
<Scene5HowItWorks />
</Sequence>
<Sequence from={SCENES.cta.from} durationInFrames={SCENES.cta.durationInFrames}>
<Scene6CTA />
</Sequence>
</AbsoluteFill>
);
};
+19
View File
@@ -0,0 +1,19 @@
import React from "react";
import { Composition } from "remotion";
import { LaunchVideo } from "./LaunchVideo";
import { FPS, TOTAL_FRAMES } from "./lib/timing";
export const RemotionRoot: React.FC = () => {
return (
<>
<Composition
id="LaunchVideo"
component={LaunchVideo}
durationInFrames={TOTAL_FRAMES}
fps={FPS}
width={1920}
height={1080}
/>
</>
);
};
@@ -0,0 +1,68 @@
import React from "react";
import { spring, useCurrentFrame, useVideoConfig } from "remotion";
import { COLORS, FONT_MONO } from "../lib/colors";
type Props = {
startFrame?: number;
size?: "small" | "large";
};
export const BadgeBar: React.FC<Props> = ({ startFrame = 0, size = "large" }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const elapsed = Math.max(0, frame - startFrame);
const scale = spring({
frame: elapsed,
fps,
config: { damping: 12, stiffness: 90 },
from: 0.85,
to: 1,
});
const opacity = spring({
frame: elapsed,
fps,
config: { damping: 20 },
from: 0,
to: 1,
});
const fontSize = size === "large" ? 56 : 28;
return (
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
transform: `scale(${scale})`,
opacity,
fontFamily: FONT_MONO,
fontSize,
color: COLORS.fgPrimary,
letterSpacing: 0.5,
}}
>
<span style={{ fontSize: fontSize * 1.1, marginRight: 16 }}>🌐</span>
<span>last30days</span>
<span
style={{
marginLeft: 14,
color: COLORS.accentCyan,
fontWeight: 600,
}}
>
v3.1
</span>
<span
style={{
marginLeft: 16,
color: COLORS.fgDim,
fontSize: fontSize * 0.55,
}}
>
· synced 2026-04-22
</span>
</div>
);
};
@@ -0,0 +1,135 @@
import React from "react";
import { interpolate, useCurrentFrame } from "remotion";
import { COLORS, FONT_MONO } from "../lib/colors";
type Row = {
dimension: string;
cells: [string, string, string];
};
type Props = {
startFrame: number;
entities: [string, string, string];
rows: Row[];
rowStaggerFrames?: number;
};
export const ComparisonTable: React.FC<Props> = ({
startFrame,
entities,
rows,
rowStaggerFrames = 18,
}) => {
const frame = useCurrentFrame();
const headerOpacity = interpolate(
frame - startFrame,
[0, 12],
[0, 1],
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" },
);
const colTemplate = "1.4fr 1fr 1fr 1fr";
const cellPad = "16px 22px";
return (
<div
style={{
width: "100%",
background: COLORS.bgPanel,
borderRadius: 16,
border: `1px solid ${COLORS.border}`,
overflow: "hidden",
fontFamily: FONT_MONO,
boxShadow: "0 24px 60px rgba(0,0,0,0.6)",
}}
>
<div
style={{
display: "grid",
gridTemplateColumns: colTemplate,
background: COLORS.bgPanelSoft,
borderBottom: `1px solid ${COLORS.border}`,
opacity: headerOpacity,
}}
>
<div
style={{
padding: cellPad,
color: COLORS.fgMuted,
fontSize: 22,
fontWeight: 500,
}}
>
Dimension
</div>
{entities.map((entity, idx) => (
<div
key={entity}
style={{
padding: cellPad,
color: idx === 0 ? COLORS.accentCyan : COLORS.fgPrimary,
fontSize: 26,
fontWeight: 600,
borderLeft: `1px solid ${COLORS.border}`,
}}
>
{entity}
</div>
))}
</div>
{rows.map((row, idx) => {
const rowStart = startFrame + 12 + idx * rowStaggerFrames;
const rowOpacity = interpolate(
frame - rowStart,
[0, 14],
[0, 1],
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" },
);
const rowSlide = interpolate(
frame - rowStart,
[0, 14],
[12, 0],
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" },
);
return (
<div
key={row.dimension}
style={{
display: "grid",
gridTemplateColumns: colTemplate,
borderBottom:
idx === rows.length - 1 ? "none" : `1px solid ${COLORS.border}`,
opacity: rowOpacity,
transform: `translateY(${rowSlide}px)`,
}}
>
<div
style={{
padding: cellPad,
color: COLORS.fgMuted,
fontSize: 22,
}}
>
{row.dimension}
</div>
{row.cells.map((cell, cellIdx) => (
<div
key={cellIdx}
style={{
padding: cellPad,
color: COLORS.fgPrimary,
fontSize: 22,
borderLeft: `1px solid ${COLORS.border}`,
lineHeight: 1.35,
}}
>
{cell}
</div>
))}
</div>
);
})}
</div>
);
};
@@ -0,0 +1,95 @@
import React from "react";
import { COLORS, FONT_MONO } from "../lib/colors";
type Props = {
title?: string;
width?: number | string;
height?: number | string;
children?: React.ReactNode;
glow?: boolean;
};
export const TerminalWindow: React.FC<Props> = ({
title = "/last30days",
width = "100%",
height = "100%",
children,
glow = false,
}) => {
return (
<div
style={{
width,
height,
background: COLORS.bgPanel,
borderRadius: 16,
border: `1px solid ${COLORS.border}`,
boxShadow: glow
? `0 0 60px ${COLORS.accentCyan}33, 0 24px 60px rgba(0,0,0,0.6)`
: "0 24px 60px rgba(0,0,0,0.6)",
overflow: "hidden",
display: "flex",
flexDirection: "column",
fontFamily: FONT_MONO,
}}
>
<div
style={{
height: 36,
background: COLORS.bgPanelSoft,
borderBottom: `1px solid ${COLORS.border}`,
display: "flex",
alignItems: "center",
padding: "0 16px",
gap: 8,
}}
>
<span
style={{
width: 12,
height: 12,
borderRadius: 12,
background: COLORS.trafficRed,
}}
/>
<span
style={{
width: 12,
height: 12,
borderRadius: 12,
background: COLORS.trafficYellow,
}}
/>
<span
style={{
width: 12,
height: 12,
borderRadius: 12,
background: COLORS.trafficGreen,
}}
/>
<span
style={{
marginLeft: 16,
color: COLORS.fgMuted,
fontSize: 14,
fontFamily: FONT_MONO,
letterSpacing: 0.5,
}}
>
{title}
</span>
</div>
<div
style={{
flex: 1,
padding: "20px 28px",
color: COLORS.fgPrimary,
overflow: "hidden",
}}
>
{children}
</div>
</div>
);
};
@@ -0,0 +1,71 @@
import React from "react";
import { interpolate, useCurrentFrame } from "remotion";
import { COLORS, FONT_MONO } from "../lib/colors";
type Props = {
text: string;
startFrame: number;
charsPerSec?: number;
fontSize?: number;
color?: string;
prefix?: string;
prefixColor?: string;
showCursor?: boolean;
fps?: number;
};
export const TypedLine: React.FC<Props> = ({
text,
startFrame,
charsPerSec = 28,
fontSize = 32,
color = COLORS.fgPrimary,
prefix,
prefixColor = COLORS.accentGreen,
showCursor = true,
fps = 30,
}) => {
const frame = useCurrentFrame();
const elapsed = Math.max(0, frame - startFrame);
const totalChars = text.length;
const lengthFrames = Math.ceil((totalChars / charsPerSec) * fps);
const visibleChars = Math.round(
interpolate(elapsed, [0, lengthFrames], [0, totalChars], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
}),
);
const visible = text.slice(0, visibleChars);
const done = visibleChars >= totalChars;
const cursorOn = showCursor && Math.floor(frame / 15) % 2 === 0;
return (
<div
style={{
fontFamily: FONT_MONO,
fontSize,
color,
whiteSpace: "pre",
lineHeight: 1.4,
}}
>
{prefix ? (
<span style={{ color: prefixColor, marginRight: 12 }}>{prefix}</span>
) : null}
<span>{visible}</span>
{(!done || cursorOn) && (
<span
style={{
display: "inline-block",
width: fontSize * 0.55,
height: fontSize * 0.95,
background: color,
verticalAlign: "text-bottom",
marginLeft: 2,
opacity: cursorOn ? 0.85 : 0,
}}
/>
)}
</div>
);
};
+4
View File
@@ -0,0 +1,4 @@
import { registerRoot } from "remotion";
import { RemotionRoot } from "./Root";
registerRoot(RemotionRoot);
+20
View File
@@ -0,0 +1,20 @@
export const COLORS = {
bgDeep: "#0a0e14",
bgPanel: "#11161d",
bgPanelSoft: "#161c25",
fgPrimary: "#e6e6e6",
fgMuted: "#8a93a3",
fgDim: "#5b6573",
border: "#2a3340",
accentCyan: "#36d6f7",
accentMagenta: "#ff55a3",
accentGreen: "#5fff9f",
accentAmber: "#ffc857",
trafficRed: "#ff5f57",
trafficYellow: "#febc2e",
trafficGreen: "#28c840",
} as const;
export const FONT_MONO = '"JetBrains Mono", "SF Mono", "Menlo", monospace';
export const FONT_SANS =
'"Inter", "SF Pro Display", -apple-system, BlinkMacSystemFont, sans-serif';
+13
View File
@@ -0,0 +1,13 @@
// Single source of truth for scene frame ranges.
// 30fps × 30s = 900 frames total.
export const FPS = 30;
export const TOTAL_FRAMES = 900;
export const SCENES = {
hook: { from: 0, durationInFrames: 90 },
oldWay: { from: 90, durationInFrames: 150 },
fanOut: { from: 240, durationInFrames: 180 },
comparison: { from: 420, durationInFrames: 210 },
howItWorks: { from: 630, durationInFrames: 150 },
cta: { from: 780, durationInFrames: 120 },
} as const;
@@ -0,0 +1,61 @@
import React from "react";
import { AbsoluteFill, interpolate, spring, useCurrentFrame, useVideoConfig } from "remotion";
import { BadgeBar } from "../components/BadgeBar";
import { COLORS, FONT_SANS } from "../lib/colors";
export const Scene1Hook: React.FC = () => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const captionOpacity = interpolate(frame, [20, 35, 75, 90], [0, 1, 1, 0], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
const captionLift = spring({
frame: frame - 20,
fps,
config: { damping: 15, stiffness: 70 },
from: 16,
to: 0,
});
const badgeFadeOut = interpolate(frame, [70, 90], [1, 0], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
return (
<AbsoluteFill
style={{
background: `radial-gradient(circle at 50% 40%, ${COLORS.bgPanelSoft} 0%, ${COLORS.bgDeep} 60%)`,
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
gap: 64,
}}
>
<div style={{ opacity: badgeFadeOut }}>
<BadgeBar />
</div>
<div
style={{
opacity: captionOpacity,
transform: `translateY(${captionLift}px)`,
fontFamily: FONT_SANS,
fontSize: 96,
fontWeight: 600,
color: COLORS.fgPrimary,
textAlign: "center",
letterSpacing: -1.5,
lineHeight: 1.1,
maxWidth: 1400,
}}
>
What if one search
<br />
ran <span style={{ color: COLORS.accentCyan }}>3 at once?</span>
</div>
</AbsoluteFill>
);
};
@@ -0,0 +1,100 @@
import React from "react";
import { AbsoluteFill, interpolate, useCurrentFrame } from "remotion";
import { TerminalWindow } from "../components/TerminalWindow";
import { TypedLine } from "../components/TypedLine";
import { COLORS, FONT_MONO, FONT_SANS } from "../lib/colors";
export const Scene2OldWay: React.FC = () => {
const frame = useCurrentFrame();
const enter = interpolate(frame, [0, 20], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
const slide = interpolate(frame, [0, 20], [40, 0], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
// Result card fades in after type completes (~70 frames)
const resultFade = interpolate(frame, [70, 95], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
// Caption appears late
const captionFade = interpolate(frame, [110, 130, 150], [0, 1, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
return (
<AbsoluteFill
style={{
background: COLORS.bgDeep,
padding: 80,
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
}}
>
<div
style={{
width: 1400,
height: 520,
opacity: enter,
transform: `translateY(${slide}px)`,
}}
>
<TerminalWindow title="bash">
<TypedLine
text="/last30days OpenAI"
startFrame={20}
prefix="$"
fontSize={42}
/>
<div
style={{
opacity: resultFade,
marginTop: 36,
padding: "20px 24px",
background: COLORS.bgPanelSoft,
borderRadius: 12,
border: `1px solid ${COLORS.border}`,
fontFamily: FONT_MONO,
fontSize: 26,
color: COLORS.fgPrimary,
lineHeight: 1.6,
}}
>
<div style={{ color: COLORS.accentGreen }}>
All agents reported back!
</div>
<div style={{ color: COLORS.fgMuted, marginTop: 6 }}>
🟠 Reddit: 14 threads
</div>
<div style={{ color: COLORS.fgMuted }}>
🔵 X: 22 posts
</div>
<div style={{ color: COLORS.fgMuted }}>
🟡 HN: 1 story
</div>
</div>
</TerminalWindow>
</div>
<div
style={{
opacity: captionFade,
marginTop: 60,
fontFamily: FONT_SANS,
fontSize: 38,
color: COLORS.fgMuted,
}}
>
The old way: <span style={{ color: COLORS.fgPrimary }}>one topic.</span>
</div>
</AbsoluteFill>
);
};
@@ -0,0 +1,221 @@
import React from "react";
import { AbsoluteFill, interpolate, spring, useCurrentFrame, useVideoConfig } from "remotion";
import { TerminalWindow } from "../components/TerminalWindow";
import { TypedLine } from "../components/TypedLine";
import { COLORS, FONT_MONO, FONT_SANS } from "../lib/colors";
const PROGRESS_LINES = [
{ source: "Reddit", color: "#ff6a3d" },
{ source: "X", color: "#36d6f7" },
{ source: "YouTube", color: "#ff5757" },
{ source: "TikTok", color: "#5fff9f" },
{ source: "Instagram", color: "#ff55a3" },
];
const ENTITIES: { label: string; tag: string; accent: string }[] = [
{ label: "OpenAI", tag: "$ /last30days OpenAI", accent: COLORS.accentCyan },
{ label: "Anthropic", tag: "$ /last30days Anthropic", accent: COLORS.accentMagenta },
{ label: "xAI", tag: "$ /last30days xAI", accent: COLORS.accentAmber },
];
const FanPane: React.FC<{
label: string;
tag: string;
accent: string;
panelStart: number;
}> = ({ label, tag, accent, panelStart }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const localFrame = Math.max(0, frame - panelStart);
const enter = spring({
frame: localFrame,
fps,
config: { damping: 18, stiffness: 80 },
from: 0,
to: 1,
});
const slide = interpolate(localFrame, [0, 20], [40, 0], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
return (
<div
style={{
flex: 1,
opacity: enter,
transform: `translateY(${slide}px)`,
height: 480,
}}
>
<TerminalWindow title={label} glow>
<div
style={{
color: accent,
fontSize: 18,
fontFamily: FONT_MONO,
marginBottom: 14,
}}
>
{tag}
</div>
<div
style={{
fontSize: 22,
color: COLORS.accentGreen,
fontFamily: FONT_MONO,
marginBottom: 12,
}}
>
[Competitors] running...
</div>
{PROGRESS_LINES.map((line, idx) => {
const lineStart = panelStart + 16 + idx * 6;
const lineFade = interpolate(
frame - lineStart,
[0, 8],
[0, 1],
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" },
);
// pulse the in-progress dot
const dotOn = Math.floor((frame - lineStart) / 6) % 2 === 0;
return (
<div
key={line.source}
style={{
opacity: lineFade,
fontFamily: FONT_MONO,
fontSize: 20,
color: COLORS.fgMuted,
display: "flex",
alignItems: "center",
marginBottom: 6,
}}
>
<span
style={{
display: "inline-block",
width: 10,
height: 10,
borderRadius: 10,
background: dotOn ? line.color : COLORS.bgPanelSoft,
marginRight: 12,
boxShadow: dotOn ? `0 0 10px ${line.color}` : "none",
}}
/>
<span style={{ color: line.color, marginRight: 8 }}>
</span>
<span>{line.source}</span>
<span style={{ marginLeft: "auto", color: COLORS.fgDim }}>
{dotOn ? "..." : "·"}
</span>
</div>
);
})}
</TerminalWindow>
</div>
);
};
export const Scene3FanOut: React.FC = () => {
const frame = useCurrentFrame();
// Phase 1 (0-30 frames): single terminal types --competitors flag
// Phase 2 (30+): split into 3 panes
const splitProgress = interpolate(frame, [30, 50], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
const singleOpacity = interpolate(frame, [0, 8, 30, 45], [0, 1, 1, 0], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
// Caption fades in shortly after panes settle so it has time to read.
const captionFade = interpolate(frame, [60, 80], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
return (
<AbsoluteFill
style={{
background: COLORS.bgDeep,
padding: 60,
display: "flex",
flexDirection: "column",
}}
>
<div
style={{
opacity: captionFade,
textAlign: "center",
fontFamily: FONT_SANS,
fontSize: 42,
color: COLORS.fgPrimary,
marginBottom: 32,
}}
>
Now it discovers competitors
<br />
<span style={{ color: COLORS.accentCyan }}>and runs all 3.</span>
</div>
<div
style={{
flex: 1,
position: "relative",
}}
>
{/* Single terminal during phase 1, fades out as panes appear */}
<div
style={{
position: "absolute",
inset: 0,
display: "flex",
alignItems: "center",
justifyContent: "center",
opacity: singleOpacity,
}}
>
<div style={{ width: 1200, height: 380 }}>
<TerminalWindow title="bash">
<TypedLine
text="/last30days OpenAI --competitors"
startFrame={0}
prefix="$"
fontSize={42}
/>
</TerminalWindow>
</div>
</div>
{/* Three panes fade in starting frame ~30 */}
<div
style={{
position: "absolute",
inset: 0,
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: 24,
opacity: splitProgress,
}}
>
{ENTITIES.map((entity, idx) => (
<FanPane
key={entity.label}
label={entity.label}
tag={entity.tag}
accent={entity.accent}
panelStart={45 + idx * 8}
/>
))}
</div>
</div>
</AbsoluteFill>
);
};
@@ -0,0 +1,105 @@
import React from "react";
import { AbsoluteFill, interpolate, useCurrentFrame } from "remotion";
import { ComparisonTable } from "../components/ComparisonTable";
import { COLORS, FONT_SANS } from "../lib/colors";
const ROWS = [
{
dimension: "What it is",
cells: [
"GPT-5 leader, Plus + API",
"Claude 4, safety-first",
"Grok, X-native, fast",
] as [string, string, string],
},
{
dimension: "30-day momentum",
cells: [
"GPT-5 launch wave",
"Claude 4.7 1M context",
"Grok 5 reveal",
] as [string, string, string],
},
{
dimension: "Community vibe",
cells: [
"Defensive but deep",
"Quiet, devs-only",
"Loud, meme-rich",
] as [string, string, string],
},
{
dimension: "Best for",
cells: [
"Mainstream + tools",
"Long-context coding",
"Live X intel",
] as [string, string, string],
},
];
export const Scene4Comparison: React.FC = () => {
const frame = useCurrentFrame();
const captionFade = interpolate(frame, [0, 12, 180, 210], [0, 1, 1, 0], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
const captionLift = interpolate(frame, [0, 14], [16, 0], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
const tableFade = interpolate(frame, [16, 32], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
return (
<AbsoluteFill
style={{
background: COLORS.bgDeep,
padding: "48px 80px",
display: "flex",
flexDirection: "column",
alignItems: "center",
}}
>
<div
style={{
opacity: captionFade,
transform: `translateY(${captionLift}px)`,
fontFamily: FONT_SANS,
fontSize: 38,
color: COLORS.fgMuted,
textAlign: "center",
marginBottom: 36,
}}
>
<span style={{ color: COLORS.accentCyan, fontWeight: 600 }}>
3 full passes.
</span>
<span style={{ marginLeft: 18, color: COLORS.accentMagenta, fontWeight: 600 }}>
3 save files.
</span>
<span style={{ marginLeft: 18, color: COLORS.fgPrimary, fontWeight: 600 }}>
1 comparison.
</span>
</div>
<div
style={{
width: "100%",
maxWidth: 1640,
opacity: tableFade,
}}
>
<ComparisonTable
startFrame={20}
entities={["OpenAI", "Anthropic", "xAI"]}
rows={ROWS}
rowStaggerFrames={22}
/>
</div>
</AbsoluteFill>
);
};
@@ -0,0 +1,54 @@
import React from "react";
import { AbsoluteFill, interpolate, useCurrentFrame } from "remotion";
import { COLORS, FONT_SANS } from "../lib/colors";
const LINES = [
{ text: "You pick the topic.", color: COLORS.fgPrimary },
{ text: "The agent picks the peers.", color: COLORS.accentCyan },
{ text: "The engine fans out.", color: COLORS.accentMagenta },
];
export const Scene5HowItWorks: React.FC = () => {
const frame = useCurrentFrame();
return (
<AbsoluteFill
style={{
background: `radial-gradient(circle at 50% 60%, ${COLORS.bgPanelSoft} 0%, ${COLORS.bgDeep} 70%)`,
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
gap: 48,
}}
>
{LINES.map((line, idx) => {
const start = 10 + idx * 28;
const fade = interpolate(frame, [start, start + 14], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
const slide = interpolate(frame, [start, start + 18], [24, 0], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
return (
<div
key={line.text}
style={{
opacity: fade,
transform: `translateY(${slide}px)`,
fontFamily: FONT_SANS,
fontSize: 78,
fontWeight: 600,
color: line.color,
letterSpacing: -1,
}}
>
{line.text}
</div>
);
})}
</AbsoluteFill>
);
};
@@ -0,0 +1,73 @@
import React from "react";
import { AbsoluteFill, interpolate, spring, useCurrentFrame, useVideoConfig } from "remotion";
import { BadgeBar } from "../components/BadgeBar";
import { COLORS, FONT_MONO, FONT_SANS } from "../lib/colors";
export const Scene6CTA: React.FC = () => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const installFade = interpolate(frame, [20, 40], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
// Stronger 1Hz pulse on the install line for end-of-video emphasis
const pulse = 0.8 + 0.2 * Math.sin((frame / fps) * 2 * Math.PI);
const glowPulse = 0.4 + 0.4 * Math.sin((frame / fps) * 2 * Math.PI);
const repoFade = interpolate(frame, [50, 70], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
const enterScale = spring({
frame,
fps,
config: { damping: 18, stiffness: 90 },
from: 0.95,
to: 1,
});
return (
<AbsoluteFill
style={{
background: `radial-gradient(circle at 50% 50%, ${COLORS.bgPanelSoft} 0%, ${COLORS.bgDeep} 70%)`,
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
gap: 56,
transform: `scale(${enterScale})`,
}}
>
<BadgeBar />
<div
style={{
opacity: installFade * pulse,
padding: "18px 36px",
background: COLORS.bgPanel,
border: `1px solid ${COLORS.accentCyan}`,
borderRadius: 14,
boxShadow: `0 0 ${40 + glowPulse * 60}px ${COLORS.accentCyan}${Math.round(40 + glowPulse * 80).toString(16)}`,
fontFamily: FONT_MONO,
fontSize: 44,
color: COLORS.fgPrimary,
}}
>
<span style={{ color: COLORS.accentGreen, marginRight: 18 }}>$</span>
/last30days <span style={{ color: COLORS.fgMuted }}>{"{topic}"}</span>{" "}
<span style={{ color: COLORS.accentCyan }}>--competitors</span>
</div>
<div
style={{
opacity: repoFade,
fontFamily: FONT_SANS,
fontSize: 28,
color: COLORS.fgMuted,
}}
>
github.com/mvanhorn/last30days-skill
</div>
</AbsoluteFill>
);
};
+19
View File
@@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"strict": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"types": ["node"]
},
"include": ["src/**/*"]
}