- 01.Context
- 02.1. Component Creation: Merging Static + Dynamic
- 03.Architecture
- 04.WindCanvas Inner Component
- 05.2. Layout Integration
- 06.3. Removing Duplicate Grid
- 07.4. Fixing Pixel Centering
- 08.5. Particle Label Evolution
- 09.Iteration 1: Agent Process Names (from backdrop-wind.tsx)
- 10.Iteration 2: Protocol/System Words
- 11.Iteration 3: Memory Dump + AI Model Agents (FINAL)
- 12.6. Accent Color Particles
- 13.Final State
- 14.Files
- 15.Performance Characteristics
- 16.Visual Layers (render order)
- 17.Technical Decisions
- 18.Why 100ms delay before fade-in?
- 19.Why keep original blueprint.tsx?
- 20.Why not merge backdrop-wind.tsx into blueprint-v2.tsx?
- 21.Why center-offset fillRect instead of drawing at (x, y)?
- 22.Next Steps (potential)
- 23.Code References
- 24.Particle Interface
- 25.Lifecycle Timing
- 26.Double Ripple Rings
- 27.Commit Message
blueprint-v2: Integrating Wind Vector Canvas with Static Blueprint Elements
Previous session established the final wind vectors pattern: 1px pixel dots with opacity-only variation driven by simplex noise, particles with agent labels, double ripple rings, and slow fade-out lif
Session: Continued from previous wind vector exploration
Context
Previous session established the final wind vectors pattern: 1px pixel dots with opacity-only variation driven by simplex noise, particles with agent labels, double ripple rings, and slow fade-out lifecycle. This session integrated that technique into the existing blueprint backdrop to create blueprint-v2.tsx.
1. Component Creation: Merging Static + Dynamic
Goal: Combine blueprint's static elements (CSS grids, SVG schematic, edge furniture, grain texture, radial mask) with the wind vectors canvas, where canvas fades in after static elements render.
Source files read:
/Users/joel/Code/brubkr/src/components/blueprint.tsx— static elements/Users/joel/Code/brubkr/src/components/blueprint-canvas.tsx— broken packet animation (not reused)/Users/joel/Code/brubkr/src/components/backdrop-wind.tsx— reference for wind vectors
Created: /Users/joel/Code/brubkr/src/components/blueprint-v2.tsx
Architecture
export function BlueprintV2() {
return (
<div className="fixed inset-0 overflow-hidden pointer-events-none select-none z-0"
style={{
WebkitMaskImage: 'radial-gradient(ellipse 60% 70% at 50% 45%, ...)',
maskImage: 'radial-gradient(ellipse 60% 70% at 50% 45%, ...)'
}}
>
{/* Wind Vectors Canvas (fades in) */}
<WindCanvas />
{/* Static SVG Schematic */}
<div className="relative w-full max-w-3xl m-auto h-[700px] mt-40 opacity-60">
{/* Circles, paths, technical data blocks */}
</div>
{/* Masked Structural UI (edge furniture) */}
<div style={{ maskImage: 'radial-gradient(...)' }}>
{/* LEFT: spine with tick marks */}
{/* RIGHT: memory hex map, rotary scanner, buffer indicator */}
{/* CORNERS: stats, encryption labels */}
</div>
{/* Grain Texture Overlay */}
<div className="absolute inset-0 opacity-[0.04] mix-blend-overlay"
style={{ backgroundImage: "url(\"data:image/svg+xml,...\")" }}
/>
</div>
);
}WindCanvas Inner Component
Embedded client component handling canvas animation:
function WindCanvas() {
const canvasRef = useRef<HTMLCanvasElement>(null);
const rafRef = useRef<number>(0);
const [ready, setReady] = useState(false);
useEffect(() => {
// ... setup noise, particles, resize
// Fade in after first frame renders
const fadeTimer = setTimeout(() => setReady(true), 100);
function animate(ts: number) {
// Grid lines at 0.03 alpha
// 1px noise pixels centered on grid nodes
// Particles with labels/ripples/fade-out
}
rafRef.current = requestAnimationFrame(animate);
return () => {
clearTimeout(fadeTimer);
cancelAnimationFrame(rafRef.current);
// ... cleanup
};
}, []);
return (
<canvas
ref={canvasRef}
className="absolute inset-0 transition-opacity duration-[2000ms] ease-in"
style={{ opacity: ready ? 1 : 0 }}
aria-hidden="true"
/>
);
}Fade-in mechanism:
- Canvas starts at
opacity: 0 - First frame renders (~100ms delay)
setReady(true)triggers- Tailwind transition:
duration-[2000ms] ease-in→ 2-second fade toopacity: 1
2. Layout Integration
File: /Users/joel/Code/brubkr/src/app/layout.tsx
- import { Backdrop } from '@/components/backdrop'
+ import { BlueprintV2 } from '@/components/blueprint-v2'
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
- <Backdrop />
+ <BlueprintV2 />
<div className="relative z-10 min-h-screen">
{/* ... */}
</div>
</body>
</html>
);
}The original <Backdrop /> network graph is now unused in layout but kept as /Users/joel/Code/brubkr/src/components/backdrop.tsx. The standalone wind vectors are in /Users/joel/Code/brubkr/src/components/backdrop-wind.tsx.
3. Removing Duplicate Grid
Problem: Blueprint.tsx had CSS background grids (32px major, 8px minor) at lines 14-32. WindCanvas also draws grid lines. Result: two overlapping grids.
Solution: Removed CSS background grid divs entirely. Canvas grid is sufficient.
Original code (removed):
{/* Background Grid */}
<div
className="absolute inset-0 opacity-[0.02]"
style={{
backgroundImage:
'linear-gradient(#fff 1px, transparent 1px), linear-gradient(90deg, #fff 1px, transparent 1px)',
backgroundSize: '32px 32px',
}}
/>
{/* Secondary Grid */}
<div
className="absolute inset-0 opacity-[0.01]"
style={{
backgroundImage:
'linear-gradient(#fff 0.5px, transparent 0.5px), linear-gradient(90deg, #fff 0.5px, transparent 0.5px)',
backgroundSize: '8px 8px',
}}
/>Canvas grid lines render at GRID_STEP = 30 with alpha: 0.03.
4. Fixing Pixel Centering
Problem: Wind vector pixels were offset down-right by 1px relative to grid intersections.
Root cause: ctx.fillRect(x, y, 1, 1) draws from (x,y) to (x+1,y+1). The "position" is the top-left corner, not the center.
Fix: Offset by 0.5px to center the 1×1 rect on grid intersections:
// Vector pixels
for (let x = GRID_STEP / 2; x < width; x += GRID_STEP) {
for (let y = GRID_STEP / 2; y < height; y += GRID_STEP) {
const speed = (noise.get(x * NOISE_SCALE + 100, y * NOISE_SCALE + 100 + time) + 1) / 2;
const alpha = 0.03 + speed * 0.14;
ctx!.fillStyle = `rgba(255, 255, 255, ${alpha})`;
- ctx!.fillRect(x, y, 1, 1);
+ ctx!.fillRect(x - 0.5, y - 0.5, 1, 1);
}
}Result: 1px pixel dots now perfectly centered on grid nodes at (x, y).
5. Particle Label Evolution
Particles needed labels that felt like "spying on memory" — not generic agent names, not bare protocol words.
Iteration 1: Agent Process Names (from backdrop-wind.tsx)
const PARTICLE_LABELS = [
'scout', 'relay', 'parse', 'index', 'embed', 'fetch',
'route', 'sync', 'scan', 'audit', 'shard', 'merge',
'cache', 'rank', 'infer'
];Problem: Too generic, no technical depth.
Iteration 2: Protocol/System Words
const PARTICLE_LABELS = [
'ACK', 'SYN', 'EOF', 'RST', 'FIN', 'PSH',
// ...
];Feedback: User said "don't love these" — too standalone, not immersive.
Iteration 3: Memory Dump + AI Model Agents (FINAL)
const PARTICLE_LABELS = [
// Memory dump / pointer style
'0xA6F2', '0xFF04', '0x4B09', '0xC2E1', '0x7D3A', '0xFE91',
'ptr->0x4C', 'buf[0x12]', '&0xE7A0', '*0x09FF', 'mem:0x3B',
// AI model agents
'claude-4', 'opus-4.5', 'sonnet-4', 'haiku-3.5',
'gpt-4o', 'o3-mini', 'codex-r1',
'gemini-2.5', 'gemini-pro',
];Why this works:
- Memory pointers:
0xA6F2,ptr->0x4C,&0xE7A0— looks like watching raw memory access - Buffer notation:
buf[0x12],mem:0x3B— C-style array indexing - AI model names: Grounds the aesthetic in the actual tools being used (Claude, GPT, Gemini)
Renders at 7px monospace, 50% opacity of particle alpha:
ctx!.font = '7px monospace';
ctx!.fillStyle = `rgba(${rgb}, ${lifeAlpha * 0.5})`;
ctx!.fillText(p.label, p.x + 5, p.y + 3);6. Accent Color Particles
Goal: ~10% of particles render in emerald green instead of white for visual variety.
Implementation:
interface Particle {
x: number;
y: number;
age: number;
maxAge: number;
ripplePhase: number;
rippleInterval: number;
hasRipple: boolean;
label: string;
isAccent: boolean; // NEW
}
function spawnParticle(): Particle {
return {
// ...
isAccent: rand() < 0.1, // 10% chance
};
}
// In render loop:
const rgb = p.isAccent ? '16, 185, 129' : '255, 255, 255';
// Applied to:
// 1. Core dot
ctx!.fillStyle = `rgba(${rgb}, ${lifeAlpha})`;
// 2. Label text
ctx!.fillStyle = `rgba(${rgb}, ${lifeAlpha * 0.5})`;
// 3. Ripple rings (if hasRipple)
ctx!.strokeStyle = `rgba(${rgb}, ${r1Alpha})`;
ctx!.strokeStyle = `rgba(${rgb}, ${r2Alpha})`;Color: rgb(16, 185, 129) — Tailwind emerald-500
Result: Occasional green particles breaking up the white field, all elements (dot/label/ripples) share the same color.
Final State
Files
| Path | Status | Notes |
|---|---|---|
/Users/joel/Code/brubkr/src/components/blueprint-v2.tsx | Created | Wind vectors + static elements |
/Users/joel/Code/brubkr/src/app/layout.tsx | Modified | Swapped <Backdrop /> → <BlueprintV2 /> |
/Users/joel/Code/brubkr/src/components/blueprint.tsx | Unchanged | Original kept for reference |
/Users/joel/Code/brubkr/src/components/backdrop.tsx | Unused | Network graph (no longer in layout) |
/Users/joel/Code/brubkr/src/components/backdrop-wind.tsx | Unchanged | Standalone wind vectors specimen |
Performance Characteristics
- Frame rate: ~20fps (
FRAME_INTERVAL = 50ms) - Visibility API: Pauses when tab hidden
- Reduced motion: Respects
prefers-reduced-motion(setsreadyimmediately, skips animation) - Particles:
Math.floor((width * height) / 60000)— scales with viewport size - Ripples: 40% of particles have double ripple rings
- Accent: 10% of particles render in emerald green
Visual Layers (render order)
-
Canvas (fades in over 2s):
- Grid lines at 0.03 alpha
- 1px noise pixels (0.03-0.17 alpha, centered on grid nodes)
- Particles with labels/ripples/fade-out
-
SVG schematic (static):
- Concentric circles, curved paths, cross-hairs
- Technical data blocks (COORD, OFFSET, SYS.READY, BUFFER)
-
Edge furniture (radial mask, static):
- LEFT: Spine with tick marks, vertical AXIS_Y label
- RIGHT: Memory hex map, rotary scanner, buffer indicator
- CORNERS: TX/RX stats, encryption/location labels
-
Grain texture (static):
- SVG fractal noise at 0.04 opacity, mix-blend-overlay
-
Radial vignette mask (outermost):
- Fades entire blueprint toward center (12% at center → 100% at edges)
Technical Decisions
Why 100ms delay before fade-in?
First frame needs time to render. Without delay, canvas would start fading in before anything is painted, causing user to see black canvas → painted canvas. With delay: black → (instant) painted canvas → (smooth) faded-in canvas.
Why keep original blueprint.tsx?
Serves as reference for the static-only version. If blueprint-v2 performance is poor, can revert to static blueprint.
Why not merge backdrop-wind.tsx into blueprint-v2.tsx?
Backdrop-wind.tsx is a standalone specimen in /lab. Keeping it separate allows comparing wind vectors in isolation vs. integrated with blueprint furniture.
Why center-offset fillRect instead of drawing at (x, y)?
Canvas fillRect(x, y, w, h) treats (x,y) as top-left corner. For a 1×1 rect, that means the pixel occupies [x, x+1) × [y, y+1). Grid intersections are at exact (x, y) coordinates, so drawing from (x-0.5, y-0.5) centers the 1×1 pixel on the intersection.
Next Steps (potential)
- Performance tuning: If ~20fps feels sluggish, reduce particle count or grid resolution
- Interaction: Mouse influence on noise field (requires tracking pointer, updating noise offset)
- Color variation: Noise-driven hue shift for accent particles (emerald → cyan → blue)
- Label rotation: More model names as new releases drop (claude-5, gpt-5, gemini-3.0)
- Mobile optimization: Reduce particle count on small viewports (already scales with area, but could add explicit breakpoint)
Code References
Particle Interface
interface Particle {
x: number;
y: number;
age: number;
maxAge: number;
ripplePhase: number;
rippleInterval: number;
hasRipple: boolean;
label: string;
isAccent: boolean;
}Lifecycle Timing
const PARTICLE_ALPHA_PEAK = 0.28;
const FADE_OUT_RATIO = 0.4; // 40% of life spent fading out
// In render:
const lifeRatio = p.age / p.maxAge;
let lifeAlpha: number;
if (lifeRatio < 0.05) {
// 5% fade-in
lifeAlpha = (lifeRatio / 0.05) * PARTICLE_ALPHA_PEAK;
} else if (lifeRatio < (1 - FADE_OUT_RATIO)) {
// 55% sustain
lifeAlpha = PARTICLE_ALPHA_PEAK;
} else {
// 40% fade-out
lifeAlpha = ((1 - lifeRatio) / FADE_OUT_RATIO) * PARTICLE_ALPHA_PEAK;
}Double Ripple Rings
if (p.hasRipple) {
p.ripplePhase += 1 / p.rippleInterval;
if (p.ripplePhase >= 1) p.ripplePhase = 0;
// Ring 1
const r1Scale = p.ripplePhase;
const r1Alpha = (1 - r1Scale) * lifeAlpha * 0.45;
const r1Radius = 3 + r1Scale * 10;
// draw arc...
// Ring 2 (offset by 0.5 phase)
const r2Phase = (p.ripplePhase + 0.5) % 1;
const r2Scale = r2Phase;
const r2Alpha = (1 - r2Scale) * lifeAlpha * 0.3;
const r2Radius = 3 + r2Scale * 10;
// draw arc...
}Commit Message
feat: Create blueprint-v2 with wind vector canvas integration
- Merge blueprint static elements with wind vectors animation
- Canvas fades in over 2s after first frame renders
- Remove duplicate CSS background grids (canvas grid sufficient)
- Fix pixel centering: fillRect(x-0.5, y-0.5, 1, 1) centers on grid nodes
- Update particle labels: memory pointers + AI model names
- Add accent color: 10% particles render in emerald green
- Swap layout.tsx: <Backdrop /> → <BlueprintV2 />
- Performance: ~20fps, visibility API pause, reduced motion respect
- Keep original blueprint.tsx as reference
Files:
- src/components/blueprint-v2.tsx (created)
- src/app/layout.tsx (modified)