GodUIGodUI
85

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.

FlowField paints particles on a canvas that stream along an evolving noise field. There is no SVG path and no retained trail geometry — each frame samples noise for an angle, steps the particle, then fades the whole buffer a hair so yesterday's stroke lingers as silk.

Noise → angle → step

Per particle, every frame:

tsx
 
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 slowly

noiseScale defaults to 0.0016 — smaller means broader, smoother currents. speed scales both the step and the z drift.

Anatomynoise → angle → step
Noise
valueNoise(x·scale, y·scale+z)
Angle
noise × π × 4
Step
1.6 × speed px / frame

Particle count defaults to 900, then clamps so dense viewports don't melt the main thread:

tsx
 
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:

tsx
 
ctx.fillStyle = `rgba(${br}, ${bg}, ${bb}, ${fade})`; // default fade = 0.06
ctx.fillRect(0, 0, w, h);
// then stroke each particle's latest segment in --primary

Lower fade → longer, silkier wakes. Higher → crisp, short ticks. Theme colors re-resolve via a MutationObserver on <html> (class / data-theme / style) so light and dark both stay correct.

Trailsfull-frame fade wipe

ctx.fillStyle = rgba(bg, fade) · default fade 0.06

Wipe
full-canvas translucent rect each frame
Silk
lower fade → longer trails

Cheap when offscreen

Three gates stop the loop when work would be wasted:

GateBehavior
IntersectionObserverpause when the canvas leaves the viewport
visibilitychangepause when document.hidden
prefers-reduced-motionrun ~240 frozen steps, then stop on a still frame

ResizeObserver rebuilds the buffer when the container changes size.

LifecycleIO · visibility · reduced
offscreen → paused
tab hidden → raf = 0
240 steps → freeze
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 — no ongoing rAF

The result

Resultthe real component — calm field

Sample → step → wipe. A living field that refuses to burn frames when nobody is looking.

On this page

Built with GodUI

Beautifully crafted motion components for modern interfaces.

Star on GitHub