COORD: 44.21.90
OFFSET: +12.5°
SYS.READY
BUFFER: 99%
FOCAL_PT
BACK TO DEVLOG
BRUBKR

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

2026-01-23 // RAW LEARNING CAPTURE
PROJECTBRUBKR

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:

  1. Canvas starts at opacity: 0
  2. First frame renders (~100ms delay)
  3. setReady(true) triggers
  4. Tailwind transition: duration-[2000ms] ease-in → 2-second fade to opacity: 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

PathStatusNotes
/Users/joel/Code/brubkr/src/components/blueprint-v2.tsxCreatedWind vectors + static elements
/Users/joel/Code/brubkr/src/app/layout.tsxModifiedSwapped <Backdrop /><BlueprintV2 />
/Users/joel/Code/brubkr/src/components/blueprint.tsxUnchangedOriginal kept for reference
/Users/joel/Code/brubkr/src/components/backdrop.tsxUnusedNetwork graph (no longer in layout)
/Users/joel/Code/brubkr/src/components/backdrop-wind.tsxUnchangedStandalone wind vectors specimen

Performance Characteristics

  • Frame rate: ~20fps (FRAME_INTERVAL = 50ms)
  • Visibility API: Pauses when tab hidden
  • Reduced motion: Respects prefers-reduced-motion (sets ready immediately, 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)

  1. 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
  2. SVG schematic (static):

    • Concentric circles, curved paths, cross-hairs
    • Technical data blocks (COORD, OFFSET, SYS.READY, BUFFER)
  3. 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
  4. Grain texture (static):

    • SVG fractal noise at 0.04 opacity, mix-blend-overlay
  5. 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)
LOG.ENTRY_END
ref:brubkr
RAW