Anatomy of Flow Field
How value noise becomes a vector field, how a translucent wipe leaves silk trails, and how observers keep the rAF loop cheap when idle.
↓Scroll to step through it
Noise → angle → step
FlowField paints particles on a canvas that stream along an evolving noise
field. It keeps no SVG path and no trail geometry. Each frame samples
noise for an angle, steps the particle, then fades the whole buffer
slightly so earlier strokes linger as trails.
Per particle, every frame:
noiseScale defaults to 0.0016. Smaller values give broader, smoother
currents. speed scales both the step and the z drift.
Particle count defaults to 900, then clamps so dense viewports don't melt
the main thread:
Math.max(120, Math.min(particleCount, Math.round((w * h) / 1400)))Fade wipe trails
Instead of storing polylines, every frame fills the canvas with a translucent rect in the background color:
Lower fade gives longer, silkier wakes. Higher gives short ticks. Theme
colors re-resolve via a MutationObserver on <html> (class /
data-theme / style) so light and dark both stay correct.
Cheap when offscreen
Three gates stop the loop when work would be wasted:
| Gate | Behavior |
|---|---|
IntersectionObserver | pause when the canvas leaves the viewport |
visibilitychange | pause when document.hidden |
prefers-reduced-motion | run ~240 frozen steps, then stop on a still frame |
ResizeObserver rebuilds the buffer when the container changes size.
The result
Sample → step → wipe. The loop stops drawing frames when nobody is looking.
- Noise
- valueNoise(x·scale, y·scale+z)
- Angle
- noise × π × 4
- Step
- 1.6 × speed px / frame
const angle =valueNoise(p.x * noiseScale, p.y * noiseScale + z) * Math.PI * 4;p.x += Math.cos(angle) * 1.6 * speed;p.y += Math.sin(angle) * 1.6 * speed;z += 0.0008 * speed; // field evolves slowlyAnatomy of Flow Field
How value noise becomes a vector field, how a translucent wipe leaves silk trails, and how observers keep the rAF loop cheap when idle.
- Noise
- valueNoise(x·scale, y·scale+z)
- Angle
- noise × π × 4
- Step
- 1.6 × speed px / frame
const angle =valueNoise(p.x * noiseScale, p.y * noiseScale + z) * Math.PI * 4;p.x += Math.cos(angle) * 1.6 * speed;p.y += Math.sin(angle) * 1.6 * speed;z += 0.0008 * speed; // field evolves slowlyNoise → angle → step
FlowField paints particles on a canvas that stream along an evolving noise
field. It keeps no SVG path and no trail geometry. Each frame samples
noise for an angle, steps the particle, then fades the whole buffer
slightly so earlier strokes linger as trails.
Per particle, every frame:
noiseScale defaults to 0.0016. Smaller values give broader, smoother
currents. speed scales both the step and the z drift.
Particle count defaults to 900, then clamps so dense viewports don't melt
the main thread:
Math.max(120, Math.min(particleCount, Math.round((w * h) / 1400)))ctx.fillStyle = rgba(bg, fade) · default fade 0.06
- Wipe
- full-canvas translucent rect each frame
- Silk
- lower fade → longer trails
ctx.fillStyle = `rgba(${br}, ${bg}, ${bb}, ${fade})`; // default fade = 0.06ctx.fillRect(0, 0, w, h);// then stroke each particle's latest segment in --primaryFade wipe trails
Instead of storing polylines, every frame fills the canvas with a translucent rect in the background color:
Lower fade gives longer, silkier wakes. Higher gives short ticks. Theme
colors re-resolve via a MutationObserver on <html> (class /
data-theme / style) so light and dark both stay correct.
- Intersection
- IO pauses the loop once the canvas leaves the viewport
- Visibility
- document.hidden stops frames while the tab is backgrounded
- Reduced
- ~240 frozen steps, then a still frame with no ongoing rAF
Cheap when offscreen
Three gates stop the loop when work would be wasted:
| Gate | Behavior |
|---|---|
IntersectionObserver | pause when the canvas leaves the viewport |
visibilitychange | pause when document.hidden |
prefers-reduced-motion | run ~240 frozen steps, then stop on a still frame |
ResizeObserver rebuilds the buffer when the container changes size.
The result
Sample → step → wipe. The loop stops drawing frames when nobody is looking.
Motion Score
canvas paint2D canvas particle trails each frame