import { interpolate } from 'remotion';
import { C } from '../lib/colors';
import { springIn, softSpring, clampLerp } from '../lib/easings';
// Phase 2 (frames 159โ261 global โ localFrame 0..102): build log streams in
// line-by-line, then a server card emerges. Two pills slot into the card:
// โข `code` arrives from the left (the LLM side)
// โข `๐ NOTION_API_KEY` arrives from the right (the vault side)
// This visualizes the architectural moment: code and credentials are
// injected at runtime from separate paths.
//
// Then a subtle caption appears below the card:
// "your isolated container ยท only you can reach it"
//
// Log lines stagger ~10 frames apart starting at localFrame 4.
// Server card emerges at localFrame ~58.
// Slot pills fly in at localFrame ~78.
const LOG_LINES = [
{ label: 'Generating spec', detail: '2 tools detected' },
{ label: 'Static checks', detail: 'passed' },
{ label: 'Building image', detail: '17.2s' },
{ label: 'Deploying', detail: 'live' },
];
const LINE_STAGGER = 10;
const LINE_START = 4;
const CARD_START = 58;
const SLOTS_START = 78;
const CAPTION_START = 92;
export function BuildScene({ localFrame, fps }: { localFrame: number; fps: number }) {
const panelIn = springIn(localFrame, fps, 0);
const panelOpacity = clampLerp(localFrame, 0, 12);
// Card emerges late in phase.
const cardIn = softSpring(localFrame, fps, CARD_START, 24);
// Once the card is up, the log panel slides up to make room.
const panelShift = interpolate(cardIn, [0, 1], [0, -160]);
// Slot pills fly in after card is settled.
const codeSlotIn = softSpring(localFrame, fps, SLOTS_START, 18);
const secretSlotIn = softSpring(localFrame, fps, SLOTS_START + 4, 18);
// Caption appears last.
const captionIn = clampLerp(localFrame, CAPTION_START, CAPTION_START + 10);
return (
{/* Build log panel */}
{/* Panel header โ tiny status row */}
build ยท notion-search
โ running
{LOG_LINES.map((line, i) => (
))}
{/* Server card (emerges in second half) */}
{cardIn > 0.01 && (
)}
{/* Isolated container caption */}
{captionIn > 0.01 && (
your isolated container ยท only you can reach it
)}
);
}
function LogLine({
label,
detail,
localFrame,
startFrame,
fps,
}: {
label: string;
detail: string;
localFrame: number;
startFrame: number;
fps: number;
}) {
const spring = springIn(localFrame, fps, startFrame);
const opacity = clampLerp(localFrame, startFrame, startFrame + 10);
const x = interpolate(spring, [0, 1], [-30, 0]);
// Check fills in slightly after the line slides in.
const checkFill = clampLerp(localFrame, startFrame + 6, startFrame + 14);
return (
{/* Checkmark circle */}
{label}
{detail}
);
}
function ServerCard({
progress,
localFrame,
codeSlotIn,
secretSlotIn,
}: {
progress: number;
localFrame: number;
codeSlotIn: number;
secretSlotIn: number;
}) {
const scale = interpolate(progress, [0, 1], [0.85, 1]);
const y = interpolate(progress, [0, 1], [40, 180]);
// Live dot pulses once the slots have arrived.
const liveOn = secretSlotIn > 0.6;
const pulsePhase = (localFrame - (SLOTS_START + 18)) / 30;
const livePulse = liveOn
? 0.6 + 0.4 * Math.sin(pulsePhase * Math.PI * 2)
: 0;
return (
{/* Header row: title + live dot */}
notion-search
{liveOn ? 'live' : 'starting'}
{/* Slot pills row */}
{/* code slot โ arrives from the left */}
{/* secret slot โ arrives from the right */}
{/* Tool rows */}
);
}
function SlotPill({
label,
kind,
in: progress,
fromX,
}: {
label: string;
kind: 'code' | 'secret';
in: number;
fromX: number;
}) {
const x = interpolate(progress, [0, 1], [fromX, 0]);
const opacity = clampLerp(progress, 0.05, 0.6);
const isSecret = kind === 'secret';
return (
{isSecret ? (
) : (
{'<>'}
)}
{label}
);
}
function MiniLockIcon() {
return (
);
}
function ToolRow({ name, desc }: { name: string; desc: string }) {
return (
);
}