buildmymcpserver/remotion/src/scenes/BuildScene.tsx

266 lines
7.4 KiB
TypeScript
Raw Normal View History

feat(web): mobile-fit hero tiles + voluminous calmer particle field + FAQ accordion Three coordinated polish items requested: 1. **Hero step-rotator tiles fit mobile without horizontal scroll.** The previous snippets contained a 50+ char `Live at https://notion-x9.mcp.buildmymcpserver.com` URL that overflowed the ~295 px text area on a 375 px viewport. Rewrote all three snippets to be naturally short — same product story, no full URLs. The <pre> drops `overflow-x-auto` and gains `whitespace-pre-wrap break-words` so any token that does exceed the column wraps gracefully instead of forcing a scrollbar. 2. **ParticleHero — more volumetric, slower, steadier at load-in.** The "stuttery / too fast" feedback came from two issues compounding: tiny dots (1.8 px on 256-tier, with 0.42 base alpha) gave the eye too few pixels to track between frames, so individual particles read as snapping rather than drifting; and the simplex-noise drift evolved at 0.08 time-scale with 0.045 velocity, fast enough that frame-to-frame deltas exceeded a tracked particle's diameter. Render uniforms tuned: - `uPointSize` 1.8 → 2.8 (256-tier), 2.4 → 3.6 (128-tier) - `uBaseAlpha` 0.42 → 0.60 Simulation shader tuned: - Drift noise time scale 0.08 → 0.045 (the most impactful single change — particles now move at half the previous speed) - Drift velocity magnitude 0.045 → 0.028 - Ring breathing noise time scale 0.35 → 0.22 - Ring polar-wave time scales 1.2 / 0.7 → 0.7 / 0.42 Net effect: same number of particles (65k) but each individually larger, brighter, and moving more slowly. The cumulative additive bloom is denser without the jitter that read as visual stutter. 3. **FAQ collapsed into a native `<details>` accordion.** Crawlers and screen readers still see every Q+A in the SSR'd HTML — `<details><summary>...</summary><p>answer</p></details>` is the standard semantic pattern for disclosure widgets. Users see one question at a time and expand on demand, which keeps the page from feeling like an endless wall of marketing text below the fold. Container narrowed `max-w-6xl` → `max-w-3xl` for accordion typography (long-form prose reads better single-column). The default WebKit disclosure-triangle marker is suppressed with `list-none` + `[&_summary::-webkit-details-marker]:hidden`, and a `lucide-react` `ChevronDown` icon rotates 180° via `group-open:rotate-180` to indicate state. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 12:35:03 +02:00
import { interpolate } from 'remotion';
import { C } from '../lib/colors';
import { springIn, softSpring, clampLerp } from '../lib/easings';
// Phase 2 (frames 75171 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>
);
}