fix(video): make Beat 2 visible — bigger particles, parallel schematic stroke
All checks were successful
Deploy to Production / deploy (push) Successful in 52s
All checks were successful
Deploy to Production / deploy (push) Successful in 52s
User report: "I only see 'Search our Notion workspace' — no video." Cause: Beat 2 (frames 55-165) was a near-empty dead moment. Particles were 1.5-2.5px on a 1080p canvas (nearly invisible), and the server schematic didn't start drawing until local frame 30 (= global 85), leaving a 30-frame gap of empty space mid-clip. The viewer's brain correctly registered "the video stops after Beat 1." Fixes: - 60 particles (was 36) at radius 6→3 with SVG Gaussian-blur glow filter, always indigo (was an indecisive two-color split). - Schematic stroke starts at local frame 8 (was 30) so the box draws IN PARALLEL with particle convergence — eye always has something to track. - Central radial-glow attractor visible the whole beat — gives the "something is forming here" cue before the schematic appears. - Server schematic enlarged 460×300 → 720×420 so it commands attention rather than feeling small. - Inner tool-row dots and port dots doubled in size with stronger drop-shadow. - Beat 3 schematic + client panel sizes scaled to match, and the wire base position adjusted (server CX moved from 960 to 760 so the wire has room to breathe before reaching the client). - Poster frame moved from 60 (mid-fade dead spot) to 180 (Beat 3 Connection layout — the most "this is a real product" shot). File sizes still well under budget: 514 KB mp4, 319 KB webm, 29 KB poster. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
fd147f9998
commit
22ba23f353
Binary file not shown.
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 29 KiB |
Binary file not shown.
Binary file not shown.
@ -6,7 +6,7 @@
|
|||||||
"studio": "remotion studio src/index.ts",
|
"studio": "remotion studio src/index.ts",
|
||||||
"render:mp4": "remotion render src/index.ts HeroVideo out/hero.mp4 --codec h264 --crf 28 --pixel-format yuv420p",
|
"render:mp4": "remotion render src/index.ts HeroVideo out/hero.mp4 --codec h264 --crf 28 --pixel-format yuv420p",
|
||||||
"render:webm": "remotion render src/index.ts HeroVideo out/hero.webm --codec vp9 --crf 32",
|
"render:webm": "remotion render src/index.ts HeroVideo out/hero.webm --codec vp9 --crf 32",
|
||||||
"render:poster": "remotion still src/index.ts HeroVideo out/hero-poster.jpg --frame 60 --image-format jpeg --jpeg-quality 85",
|
"render:poster": "remotion still src/index.ts HeroVideo out/hero-poster.jpg --frame 180 --image-format jpeg --jpeg-quality 85",
|
||||||
"render:all": "pnpm render:mp4 && pnpm render:webm && pnpm render:poster",
|
"render:all": "pnpm render:mp4 && pnpm render:webm && pnpm render:poster",
|
||||||
"to-web": "node scripts/publish-to-web.mjs",
|
"to-web": "node scripts/publish-to-web.mjs",
|
||||||
"build": "pnpm render:all && pnpm to-web"
|
"build": "pnpm render:all && pnpm to-web"
|
||||||
|
|||||||
@ -11,15 +11,15 @@ import { BEAT } from '../HeroVideo';
|
|||||||
// status tag flashes briefly. Live-dot in the server's top-right corner
|
// status tag flashes briefly. Live-dot in the server's top-right corner
|
||||||
// pulses through the whole scene.
|
// pulses through the whole scene.
|
||||||
|
|
||||||
const CX = 960;
|
const CX = 760;
|
||||||
const CY = 540;
|
const CY = 540;
|
||||||
const SERVER_W = 460;
|
const SERVER_W = 600;
|
||||||
const SERVER_H = 300;
|
const SERVER_H = 360;
|
||||||
|
|
||||||
// Claude client panel — anchored right of the server
|
// Claude client panel — anchored right of the server
|
||||||
const CLIENT_W = 280;
|
const CLIENT_W = 360;
|
||||||
const CLIENT_H = 200;
|
const CLIENT_H = 240;
|
||||||
const CLIENT_CX = 1500;
|
const CLIENT_CX = 1480;
|
||||||
const CLIENT_CY = 540;
|
const CLIENT_CY = 540;
|
||||||
|
|
||||||
export function ServerScene({ fps }: { fps: number }) {
|
export function ServerScene({ fps }: { fps: number }) {
|
||||||
@ -101,7 +101,7 @@ export function ServerScene({ fps }: { fps: number }) {
|
|||||||
|
|
||||||
{/* Tool rows inside server */}
|
{/* Tool rows inside server */}
|
||||||
{[0, 1, 2].map((r) => {
|
{[0, 1, 2].map((r) => {
|
||||||
const y = CY - 60 + r * 60;
|
const y = CY - 90 + r * 90;
|
||||||
return (
|
return (
|
||||||
<g key={r}>
|
<g key={r}>
|
||||||
<line
|
<line
|
||||||
|
|||||||
@ -5,27 +5,23 @@ import { BEAT } from '../HeroVideo';
|
|||||||
|
|
||||||
// Beat 2 — the wow moment.
|
// Beat 2 — the wow moment.
|
||||||
//
|
//
|
||||||
// The prompt words detonate into ~30 particle dots. Each particle has
|
// Prompt words detonate into ~60 chunky glowing particles that drift, then
|
||||||
// (a) a random scatter velocity that decays, (b) a magnetic pull toward
|
// magnetically snap into target slots along a SERVER SCHEMATIC. The
|
||||||
// a designated target slot on the SERVER SCHEMATIC that materialises in
|
// schematic strokes on IN PARALLEL with the convergence so the eye always
|
||||||
// the centre. As particles arrive, the schematic strokes itself on
|
// has something to anchor to — earlier versions had a dead frame ~3s in
|
||||||
// line-by-line. A scan-line sweeps over the formed server right before
|
// where particles were too small and the box hadn't drawn yet.
|
||||||
// it settles.
|
|
||||||
|
|
||||||
const PARTICLE_COUNT = 36;
|
const PARTICLE_COUNT = 60;
|
||||||
|
|
||||||
// Target slots on the server schematic — points on the rectangle's
|
const SERVER_W = 720;
|
||||||
// perimeter plus internal "row" markers. Particles slot into these.
|
const SERVER_H = 420;
|
||||||
const SERVER_W = 460;
|
|
||||||
const SERVER_H = 300;
|
|
||||||
const CX = 960;
|
const CX = 960;
|
||||||
const CY = 540;
|
const CY = 540;
|
||||||
|
|
||||||
function targetSlot(i: number) {
|
function targetSlot(i: number) {
|
||||||
const N = PARTICLE_COUNT;
|
const N = PARTICLE_COUNT;
|
||||||
// Distribute slots: half on the perimeter, half on internal "tool rows".
|
|
||||||
if (i < N / 2) {
|
if (i < N / 2) {
|
||||||
// perimeter — walk around the rectangle
|
// perimeter walk
|
||||||
const t = i / (N / 2);
|
const t = i / (N / 2);
|
||||||
const perim = 2 * (SERVER_W + SERVER_H);
|
const perim = 2 * (SERVER_W + SERVER_H);
|
||||||
const d = t * perim;
|
const d = t * perim;
|
||||||
@ -33,87 +29,105 @@ function targetSlot(i: number) {
|
|||||||
const top = CY - SERVER_H / 2;
|
const top = CY - SERVER_H / 2;
|
||||||
let px = left;
|
let px = left;
|
||||||
let py = top;
|
let py = top;
|
||||||
if (d < SERVER_W) {
|
if (d < SERVER_W) { px = left + d; py = top; }
|
||||||
px = left + d;
|
else if (d < SERVER_W + SERVER_H) { px = left + SERVER_W; py = top + (d - SERVER_W); }
|
||||||
py = top;
|
else if (d < 2 * SERVER_W + SERVER_H) { px = left + SERVER_W - (d - SERVER_W - SERVER_H); py = top + SERVER_H; }
|
||||||
} else if (d < SERVER_W + SERVER_H) {
|
else { px = left; py = top + SERVER_H - (d - 2 * SERVER_W - SERVER_H); }
|
||||||
px = left + SERVER_W;
|
|
||||||
py = top + (d - SERVER_W);
|
|
||||||
} else if (d < 2 * SERVER_W + SERVER_H) {
|
|
||||||
px = left + SERVER_W - (d - SERVER_W - SERVER_H);
|
|
||||||
py = top + SERVER_H;
|
|
||||||
} else {
|
|
||||||
px = left;
|
|
||||||
py = top + SERVER_H - (d - 2 * SERVER_W - SERVER_H);
|
|
||||||
}
|
|
||||||
return { x: px, y: py };
|
return { x: px, y: py };
|
||||||
}
|
}
|
||||||
// Internal tool rows — three horizontal lines inside the server
|
// Inside the box: three tool rows
|
||||||
const j = i - N / 2;
|
const j = i - N / 2;
|
||||||
const row = j % 3;
|
const perRow = Math.ceil(N / 2 / 3);
|
||||||
const col = Math.floor(j / 3) / (N / 2 / 3 - 1);
|
const row = Math.floor(j / perRow);
|
||||||
const rowY = CY - 60 + row * 60;
|
const col = (j % perRow) / Math.max(1, perRow - 1);
|
||||||
const rowX = CX - SERVER_W / 2 + 30 + col * (SERVER_W - 60);
|
const rowY = CY - 90 + row * 90;
|
||||||
|
const rowX = CX - SERVER_W / 2 + 50 + col * (SERVER_W - 100);
|
||||||
return { x: rowX, y: rowY };
|
return { x: rowX, y: rowY };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TransformScene() {
|
export function TransformScene() {
|
||||||
const frame = useCurrentFrame();
|
const frame = useCurrentFrame();
|
||||||
const { fps } = useVideoConfig();
|
const { fps } = useVideoConfig();
|
||||||
// Local frame within the beat — frame 0 is the start of Transform.
|
const local = frame - BEAT.transform.in;
|
||||||
const local = frame - BEAT.transform.in; // 0..110
|
|
||||||
const total = BEAT.transform.out - BEAT.transform.in;
|
|
||||||
|
|
||||||
// Entrance + exit envelopes for the scene as a whole.
|
|
||||||
const sceneIn = clampLerp(frame, BEAT.transform.in, BEAT.transform.in + 6);
|
const sceneIn = clampLerp(frame, BEAT.transform.in, BEAT.transform.in + 6);
|
||||||
const sceneOut = 1 - clampLerp(frame, BEAT.transform.out - 8, BEAT.transform.out);
|
const sceneOut = 1 - clampLerp(frame, BEAT.transform.out - 8, BEAT.transform.out);
|
||||||
const sceneAlpha = Math.min(sceneIn, sceneOut);
|
const sceneAlpha = Math.min(sceneIn, sceneOut);
|
||||||
|
|
||||||
// Schematic stroke-on: 0→1 between frames 35..65 of the beat.
|
// Schematic strokes on IN PARALLEL with the convergence — starts at
|
||||||
const strokeT = easeInOut(clampLerp(local, 30, 70));
|
// local 8 instead of 30 so the box is visible before particles arrive.
|
||||||
// Port pulse readiness — only after schematic strokes complete.
|
const strokeT = easeInOut(clampLerp(local, 8, 55));
|
||||||
const portPulse = clampLerp(local, 70, 95);
|
// Ports + rows show up as schematic completes
|
||||||
|
const innerT = clampLerp(local, 35, 65);
|
||||||
|
const portPulse = clampLerp(local, 55, 90);
|
||||||
|
|
||||||
// Scan-line sweep — diagonal gradient passes once across the formed server
|
// Scan-line — diagonal pass once the schematic is fully drawn
|
||||||
const scanT = clampLerp(local, 70, 100);
|
const scanT = clampLerp(local, 55, 90);
|
||||||
|
|
||||||
|
// Central core glow — visible throughout Beat 2 so the eye has an
|
||||||
|
// anchor even when particles are mid-flight. Pulses softly.
|
||||||
|
const coreAlpha = clampLerp(local, 0, 12) * (1 - clampLerp(local, 95, 110) * 0.4);
|
||||||
|
const corePulse = 1 + 0.25 * Math.sin(local * 0.18);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ position: 'absolute', inset: 0, opacity: sceneAlpha }}>
|
<div style={{ position: 'absolute', inset: 0, opacity: sceneAlpha }}>
|
||||||
{/* Particles */}
|
{/* Central core — radial glow that's always-on during Beat 2. Sells
|
||||||
|
"something is building here" before the schematic is drawn. */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
left: CX - 200,
|
||||||
|
top: CY - 200,
|
||||||
|
width: 400,
|
||||||
|
height: 400,
|
||||||
|
background: `radial-gradient(circle, ${C.accentGlow} 0%, transparent 60%)`,
|
||||||
|
opacity: coreAlpha * 0.7,
|
||||||
|
transform: `scale(${corePulse})`,
|
||||||
|
pointerEvents: 'none',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Particles — 60 chunky glowing dots */}
|
||||||
<svg width={1920} height={1080} style={{ position: 'absolute', inset: 0 }}>
|
<svg width={1920} height={1080} style={{ position: 'absolute', inset: 0 }}>
|
||||||
|
<defs>
|
||||||
|
<filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
|
||||||
|
<feGaussianBlur stdDeviation="2.5" result="blur" />
|
||||||
|
<feMerge>
|
||||||
|
<feMergeNode in="blur" />
|
||||||
|
<feMergeNode in="SourceGraphic" />
|
||||||
|
</feMerge>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
{Array.from({ length: PARTICLE_COUNT }).map((_, i) => {
|
{Array.from({ length: PARTICLE_COUNT }).map((_, i) => {
|
||||||
// Source: prompt word approximate position spread across the line
|
|
||||||
const wordIndex = i % 4;
|
const wordIndex = i % 4;
|
||||||
const wordX = 760 + wordIndex * 130 + rand(i * 7.13) * 60 - 30;
|
const wordX = 760 + wordIndex * 130 + rand(i * 7.13) * 60 - 30;
|
||||||
const wordY = 540 + rand(i * 3.71) * 20 - 10;
|
const wordY = 540 + rand(i * 3.71) * 24 - 12;
|
||||||
|
|
||||||
// Target slot on server
|
|
||||||
const slot = targetSlot(i);
|
const slot = targetSlot(i);
|
||||||
|
// Velocity vectors — particles fly outward in a roughly radial
|
||||||
|
// pattern from the prompt baseline. Magnitude varied per particle.
|
||||||
|
const angle = rand(i * 1.71) * Math.PI * 2;
|
||||||
|
const speed = 240 + rand(i * 4.13) * 380;
|
||||||
|
const vx = Math.cos(angle) * speed;
|
||||||
|
const vy = Math.sin(angle) * speed - 60; // bias slightly upward
|
||||||
|
|
||||||
// Scatter velocity — frames 0..25 the particle drifts outward;
|
const explode = clampLerp(local, 0, 18);
|
||||||
// then frames 25..60 it pulls toward the target with spring.
|
// Pull starts earlier (frame 14 instead of 25) so particles
|
||||||
const vx = (rand(i * 1.31) - 0.5) * 600;
|
// are visible converging rather than just drifting.
|
||||||
const vy = (rand(i * 2.71) - 0.5) * 400;
|
const pull = softSpring(frame, fps, BEAT.transform.in + 14, 42);
|
||||||
|
|
||||||
// Phase 1: explosion 0..25
|
|
||||||
const explode = clampLerp(local, 0, 25);
|
|
||||||
// Phase 2: magnetic pull 25..60
|
|
||||||
const pull = softSpring(frame, fps, BEAT.transform.in + 25, 36);
|
|
||||||
|
|
||||||
// Position is: (wordPos) + (vx,vy * explode) lerped toward target by pull
|
|
||||||
const driftX = wordX + vx * explode * (1 - pull);
|
const driftX = wordX + vx * explode * (1 - pull);
|
||||||
const driftY = wordY + vy * explode * (1 - pull);
|
const driftY = wordY + vy * explode * (1 - pull);
|
||||||
const x = driftX + (slot.x - driftX) * pull;
|
const x = driftX + (slot.x - driftX) * pull;
|
||||||
const y = driftY + (slot.y - driftY) * pull;
|
const y = driftY + (slot.y - driftY) * pull;
|
||||||
|
|
||||||
// Size grows as particles "lock in"
|
// Radius: 6→3 as particles lock in. Big enough at 1080p that
|
||||||
const r = interpolate(pull, [0, 1], [2.5, 1.8]);
|
// every particle is clearly visible.
|
||||||
// Color shifts from white → indigo as they lock to schematic
|
const r = interpolate(pull, [0, 1], [6, 3]);
|
||||||
const color = pull > 0.6 ? C.accent : C.fg;
|
// Always indigo — earlier two-color split was indecisive
|
||||||
// Fade in fast at the start so the explosion is visible
|
const color = C.accent;
|
||||||
const alpha = clampLerp(local, 0, 4);
|
const alpha = clampLerp(local, 0, 4);
|
||||||
// Slight fade-out near end as the schematic takes visual primacy
|
const fadeOut = 1 - clampLerp(local, 88, 108) * 0.4;
|
||||||
const fadeOut = 1 - clampLerp(local, 85, 105) * 0.3;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<circle
|
<circle
|
||||||
@ -123,15 +137,13 @@ export function TransformScene() {
|
|||||||
r={r}
|
r={r}
|
||||||
fill={color}
|
fill={color}
|
||||||
opacity={alpha * fadeOut * 0.95}
|
opacity={alpha * fadeOut * 0.95}
|
||||||
style={{
|
filter="url(#glow)"
|
||||||
filter: pull > 0.5 ? `drop-shadow(0 0 4px ${C.accentGlow})` : undefined,
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
{/* Server schematic — strokes on as particles arrive */}
|
{/* Server schematic */}
|
||||||
<svg
|
<svg
|
||||||
width={1920}
|
width={1920}
|
||||||
height={1080}
|
height={1080}
|
||||||
@ -139,98 +151,101 @@ export function TransformScene() {
|
|||||||
>
|
>
|
||||||
<defs>
|
<defs>
|
||||||
<linearGradient id="scanline" x1="0%" y1="0%" x2="100%" y2="100%">
|
<linearGradient id="scanline" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
<stop offset={`${Math.max(0, scanT * 100 - 8)}%`} stopColor={C.accent} stopOpacity="0" />
|
<stop offset={`${Math.max(0, scanT * 100 - 6)}%`} stopColor={C.accent} stopOpacity="0" />
|
||||||
<stop offset={`${scanT * 100}%`} stopColor={C.accent} stopOpacity="0.85" />
|
<stop offset={`${scanT * 100}%`} stopColor={C.accent} stopOpacity="0.95" />
|
||||||
<stop offset={`${Math.min(100, scanT * 100 + 8)}%`} stopColor={C.accent} stopOpacity="0" />
|
<stop offset={`${Math.min(100, scanT * 100 + 6)}%`} stopColor={C.accent} stopOpacity="0" />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
</defs>
|
</defs>
|
||||||
|
|
||||||
{/* Outer rectangle — stroke draws perimeter from top-left clockwise */}
|
{/* Faint inner panel as the box draws — gives volume immediately */}
|
||||||
<rect
|
<rect
|
||||||
x={CX - SERVER_W / 2}
|
x={CX - SERVER_W / 2}
|
||||||
y={CY - SERVER_H / 2}
|
y={CY - SERVER_H / 2}
|
||||||
width={SERVER_W}
|
width={SERVER_W}
|
||||||
height={SERVER_H}
|
height={SERVER_H}
|
||||||
rx={6}
|
rx={8}
|
||||||
fill="none"
|
fill={C.bgElevated}
|
||||||
stroke={C.accent}
|
opacity={strokeT * 0.5}
|
||||||
strokeWidth={2}
|
|
||||||
strokeDasharray={2 * (SERVER_W + SERVER_H)}
|
|
||||||
strokeDashoffset={(1 - strokeT) * 2 * (SERVER_W + SERVER_H)}
|
|
||||||
opacity={0.9}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Three internal "tool" rows — appear after perimeter completes */}
|
{/* Outer rectangle stroke */}
|
||||||
|
<rect
|
||||||
|
x={CX - SERVER_W / 2}
|
||||||
|
y={CY - SERVER_H / 2}
|
||||||
|
width={SERVER_W}
|
||||||
|
height={SERVER_H}
|
||||||
|
rx={8}
|
||||||
|
fill="none"
|
||||||
|
stroke={C.accent}
|
||||||
|
strokeWidth={3}
|
||||||
|
strokeDasharray={2 * (SERVER_W + SERVER_H)}
|
||||||
|
strokeDashoffset={(1 - strokeT) * 2 * (SERVER_W + SERVER_H)}
|
||||||
|
opacity={0.95}
|
||||||
|
style={{ filter: `drop-shadow(0 0 8px ${C.accentGlow})` }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Three internal tool rows */}
|
||||||
{[0, 1, 2].map((r) => {
|
{[0, 1, 2].map((r) => {
|
||||||
const rowAlpha = clampLerp(local, 60 + r * 4, 72 + r * 4);
|
const y = CY - 90 + r * 90;
|
||||||
const y = CY - 60 + r * 60;
|
|
||||||
return (
|
return (
|
||||||
<g key={r} opacity={rowAlpha}>
|
<g key={r} opacity={innerT}>
|
||||||
<line
|
<line
|
||||||
x1={CX - SERVER_W / 2 + 24}
|
x1={CX - SERVER_W / 2 + 40}
|
||||||
y1={y}
|
y1={y}
|
||||||
x2={CX + SERVER_W / 2 - 24}
|
x2={CX + SERVER_W / 2 - 40}
|
||||||
y2={y}
|
y2={y}
|
||||||
stroke={C.borderStrong}
|
stroke={C.borderStrong}
|
||||||
strokeWidth={1}
|
strokeWidth={1.5}
|
||||||
|
/>
|
||||||
|
<circle cx={CX - SERVER_W / 2 + 50} cy={y} r={5} fill={C.accent} opacity={0.95}
|
||||||
|
style={{ filter: `drop-shadow(0 0 4px ${C.accentGlow})` }}
|
||||||
/>
|
/>
|
||||||
<circle cx={CX - SERVER_W / 2 + 30} cy={y} r={3} fill={C.accent} opacity={0.9} />
|
|
||||||
</g>
|
</g>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{/* Port dots — 4 each side, pulse once schematic locks in */}
|
{/* Port dots */}
|
||||||
{[-1, 1].map((side) =>
|
{[-1, 1].map((side) =>
|
||||||
[-1, 0, 1].map((off) => (
|
[-1, 0, 1].map((off) => (
|
||||||
<circle
|
<circle
|
||||||
key={`${side}-${off}`}
|
key={`${side}-${off}`}
|
||||||
cx={CX + (SERVER_W / 2) * side}
|
cx={CX + (SERVER_W / 2) * side}
|
||||||
cy={CY + off * 60}
|
cy={CY + off * 90}
|
||||||
r={4}
|
r={6}
|
||||||
fill={C.accent}
|
fill={C.accent}
|
||||||
opacity={portPulse * (0.6 + 0.4 * Math.sin(local * 0.3 + off))}
|
opacity={portPulse * (0.7 + 0.3 * Math.sin(local * 0.3 + off))}
|
||||||
style={{ filter: `drop-shadow(0 0 6px ${C.accentGlow})` }}
|
style={{ filter: `drop-shadow(0 0 8px ${C.accentGlow})` }}
|
||||||
/>
|
/>
|
||||||
)),
|
)),
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Scan-line sweep — diagonal pass after stroke completes */}
|
{/* Scan-line sweep */}
|
||||||
{scanT > 0 && scanT < 1 && (
|
{scanT > 0 && scanT < 1 && (
|
||||||
<rect
|
<rect
|
||||||
x={CX - SERVER_W / 2}
|
x={CX - SERVER_W / 2}
|
||||||
y={CY - SERVER_H / 2}
|
y={CY - SERVER_H / 2}
|
||||||
width={SERVER_W}
|
width={SERVER_W}
|
||||||
height={SERVER_H}
|
height={SERVER_H}
|
||||||
rx={6}
|
rx={8}
|
||||||
fill="url(#scanline)"
|
fill="url(#scanline)"
|
||||||
opacity={0.55}
|
opacity={0.65}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
{/* Corner labels — typographic detail that sells "this is a real server" */}
|
{/* Corner labels — bigger and earlier */}
|
||||||
<CornerLabel x={CX - SERVER_W / 2 - 8} y={CY - SERVER_H / 2 - 28} text="mcp-notion" appearAt={local} delay={62} />
|
<CornerLabel x={CX - SERVER_W / 2} y={CY - SERVER_H / 2 - 36} text="mcp-notion" appearAt={local} delay={42} />
|
||||||
<CornerLabel x={CX + SERVER_W / 2 + 8} y={CY - SERVER_H / 2 - 28} text="OAuth 2.1" appearAt={local} delay={68} align="right" />
|
<CornerLabel x={CX + SERVER_W / 2} y={CY - SERVER_H / 2 - 36} text="OAuth 2.1" appearAt={local} delay={48} align="right" />
|
||||||
<CornerLabel x={CX - SERVER_W / 2 - 8} y={CY + SERVER_H / 2 + 18} text="search_pages" appearAt={local} delay={72} />
|
<CornerLabel x={CX - SERVER_W / 2} y={CY + SERVER_H / 2 + 16} text="search_pages" appearAt={local} delay={54} />
|
||||||
<CornerLabel x={CX + SERVER_W / 2 + 8} y={CY + SERVER_H / 2 + 18} text="get_page_content" appearAt={local} delay={76} align="right" />
|
<CornerLabel x={CX + SERVER_W / 2} y={CY + SERVER_H / 2 + 16} text="get_page_content" appearAt={local} delay={60} align="right" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CornerLabel({
|
function CornerLabel({
|
||||||
x,
|
x, y, text, appearAt, delay, align = 'left',
|
||||||
y,
|
|
||||||
text,
|
|
||||||
appearAt,
|
|
||||||
delay,
|
|
||||||
align = 'left',
|
|
||||||
}: {
|
}: {
|
||||||
x: number;
|
x: number; y: number; text: string; appearAt: number; delay: number; align?: 'left' | 'right';
|
||||||
y: number;
|
|
||||||
text: string;
|
|
||||||
appearAt: number;
|
|
||||||
delay: number;
|
|
||||||
align?: 'left' | 'right';
|
|
||||||
}) {
|
}) {
|
||||||
const t = clampLerp(appearAt, delay, delay + 8);
|
const t = clampLerp(appearAt, delay, delay + 8);
|
||||||
return (
|
return (
|
||||||
@ -241,8 +256,8 @@ function CornerLabel({
|
|||||||
right: align === 'right' ? 1920 - x : undefined,
|
right: align === 'right' ? 1920 - x : undefined,
|
||||||
top: y,
|
top: y,
|
||||||
fontFamily: 'ui-monospace, SF Mono, Menlo, monospace',
|
fontFamily: 'ui-monospace, SF Mono, Menlo, monospace',
|
||||||
fontSize: 13,
|
fontSize: 17,
|
||||||
letterSpacing: '0.06em',
|
letterSpacing: '0.08em',
|
||||||
color: C.fgMuted,
|
color: C.fgMuted,
|
||||||
opacity: t,
|
opacity: t,
|
||||||
transform: `translateY(${(1 - t) * 4}px)`,
|
transform: `translateY(${(1 - t) * 4}px)`,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user