b31ec05c74
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>
136 lines
3.3 KiB
TypeScript
136 lines
3.3 KiB
TypeScript
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>
|
|
);
|
|
};
|