266 lines
7.4 KiB
TypeScript
266 lines
7.4 KiB
TypeScript
|
|
import { interpolate } from 'remotion';
|
|||
|
|
import { C } from '../lib/colors';
|
|||
|
|
import { springIn, softSpring, clampLerp } from '../lib/easings';
|
|||
|
|
|
|||
|
|
// Phase 2 (frames 75–171 global → localFrame 0..96): build log streams in
|
|||
|
|
// line-by-line, then a server card emerges.
|
|||
|
|
//
|
|||
|
|
// Log lines stagger ~12 frames apart starting at localFrame 4.
|
|||
|
|
// Server card emerges at localFrame ~64.
|
|||
|
|
|
|||
|
|
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 = 60;
|
|||
|
|
|
|||
|
|
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, -140]);
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div
|
|||
|
|
style={{
|
|||
|
|
position: 'absolute',
|
|||
|
|
inset: 0,
|
|||
|
|
display: 'flex',
|
|||
|
|
flexDirection: 'column',
|
|||
|
|
alignItems: 'center',
|
|||
|
|
justifyContent: 'center',
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
{/* Build log panel */}
|
|||
|
|
<div
|
|||
|
|
style={{
|
|||
|
|
width: 720,
|
|||
|
|
backgroundColor: C.bgElevated,
|
|||
|
|
border: `1px solid ${C.border}`,
|
|||
|
|
borderRadius: 14,
|
|||
|
|
padding: '24px 28px',
|
|||
|
|
boxShadow: '0 20px 60px rgba(0,0,0,0.5)',
|
|||
|
|
opacity: panelOpacity,
|
|||
|
|
transform: `translateY(${interpolate(panelIn, [0, 1], [20, 0]) + panelShift}px) scale(${interpolate(panelIn, [0, 1], [0.96, 1])})`,
|
|||
|
|
display: 'flex',
|
|||
|
|
flexDirection: 'column',
|
|||
|
|
gap: 14,
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
{/* Panel header — tiny status row */}
|
|||
|
|
<div
|
|||
|
|
style={{
|
|||
|
|
display: 'flex',
|
|||
|
|
alignItems: 'center',
|
|||
|
|
justifyContent: 'space-between',
|
|||
|
|
paddingBottom: 12,
|
|||
|
|
borderBottom: `1px solid ${C.border}`,
|
|||
|
|
fontFamily: 'ui-monospace, SF Mono, Menlo, monospace',
|
|||
|
|
fontSize: 14,
|
|||
|
|
color: C.fgSubtle,
|
|||
|
|
letterSpacing: 1,
|
|||
|
|
textTransform: 'uppercase',
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
<span>build · notion-search</span>
|
|||
|
|
<span style={{ color: C.fgMuted }}>● running</span>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{LOG_LINES.map((line, i) => (
|
|||
|
|
<LogLine
|
|||
|
|
key={i}
|
|||
|
|
label={line.label}
|
|||
|
|
detail={line.detail}
|
|||
|
|
localFrame={localFrame}
|
|||
|
|
startFrame={LINE_START + i * LINE_STAGGER}
|
|||
|
|
fps={fps}
|
|||
|
|
/>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Server card (emerges in second half) */}
|
|||
|
|
{cardIn > 0.01 && (
|
|||
|
|
<ServerCard
|
|||
|
|
progress={cardIn}
|
|||
|
|
localFrame={localFrame}
|
|||
|
|
/>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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 (
|
|||
|
|
<div
|
|||
|
|
style={{
|
|||
|
|
display: 'flex',
|
|||
|
|
alignItems: 'center',
|
|||
|
|
gap: 14,
|
|||
|
|
opacity,
|
|||
|
|
transform: `translateX(${x}px)`,
|
|||
|
|
fontFamily: 'ui-monospace, SF Mono, Menlo, monospace',
|
|||
|
|
fontSize: 18,
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
{/* Checkmark circle */}
|
|||
|
|
<div
|
|||
|
|
style={{
|
|||
|
|
width: 22,
|
|||
|
|
height: 22,
|
|||
|
|
borderRadius: 11,
|
|||
|
|
backgroundColor: `rgba(34, 197, 94, ${checkFill * 0.18})`,
|
|||
|
|
border: `1.5px solid ${C.success}`,
|
|||
|
|
display: 'flex',
|
|||
|
|
alignItems: 'center',
|
|||
|
|
justifyContent: 'center',
|
|||
|
|
flexShrink: 0,
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
<svg width="12" height="12" viewBox="0 0 12 12">
|
|||
|
|
<path
|
|||
|
|
d="M 2.5 6.5 L 5 9 L 9.5 3.5"
|
|||
|
|
fill="none"
|
|||
|
|
stroke={C.success}
|
|||
|
|
strokeWidth={2}
|
|||
|
|
strokeLinecap="round"
|
|||
|
|
strokeLinejoin="round"
|
|||
|
|
strokeDasharray={12}
|
|||
|
|
strokeDashoffset={(1 - checkFill) * 12}
|
|||
|
|
/>
|
|||
|
|
</svg>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<span style={{ color: C.fg, minWidth: 220 }}>{label}</span>
|
|||
|
|
<span style={{ color: C.fgMuted, flex: 1 }}>{detail}</span>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function ServerCard({ progress, localFrame }: { progress: number; localFrame: number }) {
|
|||
|
|
const scale = interpolate(progress, [0, 1], [0.85, 1]);
|
|||
|
|
const y = interpolate(progress, [0, 1], [40, 180]);
|
|||
|
|
// Pulse the live dot at ~1Hz.
|
|||
|
|
const pulsePhase = (localFrame - 60) / 30;
|
|||
|
|
const livePulse = 0.6 + 0.4 * Math.sin(pulsePhase * Math.PI * 2);
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div
|
|||
|
|
style={{
|
|||
|
|
position: 'absolute',
|
|||
|
|
left: '50%',
|
|||
|
|
top: '50%',
|
|||
|
|
transform: `translate(-50%, calc(-50% + ${y}px)) scale(${scale})`,
|
|||
|
|
opacity: progress,
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
<div
|
|||
|
|
style={{
|
|||
|
|
width: 480,
|
|||
|
|
backgroundColor: C.bgElevated,
|
|||
|
|
border: `1.5px solid ${C.accent}`,
|
|||
|
|
borderRadius: 16,
|
|||
|
|
padding: '24px 28px',
|
|||
|
|
boxShadow: `0 0 0 5px ${C.accentGlow}, 0 24px 70px rgba(0,0,0,0.6)`,
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
{/* Header row: title + live dot */}
|
|||
|
|
<div
|
|||
|
|
style={{
|
|||
|
|
display: 'flex',
|
|||
|
|
alignItems: 'center',
|
|||
|
|
justifyContent: 'space-between',
|
|||
|
|
marginBottom: 18,
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
<div
|
|||
|
|
style={{
|
|||
|
|
fontFamily: 'ui-monospace, SF Mono, Menlo, monospace',
|
|||
|
|
fontSize: 20,
|
|||
|
|
color: C.fg,
|
|||
|
|
letterSpacing: 0.2,
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
notion-search
|
|||
|
|
</div>
|
|||
|
|
<div
|
|||
|
|
style={{
|
|||
|
|
display: 'flex',
|
|||
|
|
alignItems: 'center',
|
|||
|
|
gap: 8,
|
|||
|
|
fontFamily: 'ui-monospace, SF Mono, Menlo, monospace',
|
|||
|
|
fontSize: 12,
|
|||
|
|
color: C.success,
|
|||
|
|
letterSpacing: 1.5,
|
|||
|
|
textTransform: 'uppercase',
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
<div
|
|||
|
|
style={{
|
|||
|
|
width: 8,
|
|||
|
|
height: 8,
|
|||
|
|
borderRadius: 4,
|
|||
|
|
backgroundColor: C.success,
|
|||
|
|
boxShadow: `0 0 ${10 * livePulse}px ${C.success}`,
|
|||
|
|
opacity: 0.5 + 0.5 * livePulse,
|
|||
|
|
}}
|
|||
|
|
/>
|
|||
|
|
live
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Tool rows */}
|
|||
|
|
<ToolRow name="search_pages" desc="full-text query" />
|
|||
|
|
<div style={{ height: 8 }} />
|
|||
|
|
<ToolRow name="get_page_content" desc="fetch by id" />
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function ToolRow({ name, desc }: { name: string; desc: string }) {
|
|||
|
|
return (
|
|||
|
|
<div
|
|||
|
|
style={{
|
|||
|
|
display: 'flex',
|
|||
|
|
alignItems: 'center',
|
|||
|
|
gap: 12,
|
|||
|
|
padding: '10px 14px',
|
|||
|
|
backgroundColor: C.bgSubtle,
|
|||
|
|
border: `1px solid ${C.border}`,
|
|||
|
|
borderRadius: 10,
|
|||
|
|
fontFamily: 'ui-monospace, SF Mono, Menlo, monospace',
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
<div style={{ width: 6, height: 6, borderRadius: 3, backgroundColor: C.accent }} />
|
|||
|
|
<span style={{ color: C.fg, fontSize: 16, minWidth: 200 }}>{name}</span>
|
|||
|
|
<span style={{ color: C.fgSubtle, fontSize: 14, flex: 1 }}>{desc}</span>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|