Dithered SVG Particles

Any SVG turned into a field of 10k+ interactive particles. Hover to push them apart, click to fire a shockwave. 2D canvas, no WebGL.

2026-04

An experiment in porting Emil Kowalski's dithered-canvas technique to work with any SVG. The mark loads, rasterizes, and becomes a cloud of responsive dots — no shaders, no libraries, just typed arrays and bucketed 2D canvas draws.

How it works

  1. The SVG loads as an <img> and rasterizes into a 256×256 offscreen canvas.
  2. Every pixel becomes a particle; RGB is quantized to 4-bit bins.
  3. Dots with the same bin collapse into one color bucket (~20–80 per SVG).
  4. Draw pass: one fillStyle per bucket, batched fillRects. 10k+ particles at 60fps.
  5. Hover pushes with cubic falloff; click fires an expanding shockwave; requestAnimationFrame stops at rest.

The craft details

  • Pre-allocated Float32Array / Int32Array for all per-dot state — no per-frame allocation, no GC pressure.
  • Exponential smoothing at 12%/frame (disp += (target - disp) × 0.12) gives spring-adjacent feel for a fraction of the cost of real physics.
  • Epsilon snap (Math.abs(disp) < 0.01 → 0) is what lets the animation loop actually end — without it, dots drift in sub-pixel floats forever and burn CPU indefinitely.
  • Pointer filtering — only pointerType === 'mouse' triggers the hover loop. Touch devices skip the expensive per-frame repulsion but still get click shockwaves.
  • data-ready attribute flips after the first paint, triggering a CSS fade-in. Avoids the flash of blank canvas on mount.

The whole component is ~300 lines. The technique scales: a four-shape brand mark, an illustrated portrait, or an icon — if you can render it as an SVG, the particle field picks up the colors automatically.