Skip to main content

Flow Field

Open the flow field. Drag the circular vortex handle or use arrow keys to navigate the disturbance center. Adjust vortex strength, toggle pause/play, or regenerate random streamline offsets.

Vector Field Formulation

The particle motion is governed by an analytical 2D velocity field V(x,y)=(vx,vy)\mathbf{V}(x, y) = (v_x, v_y). The field combines a baseline horizontal laminar flow, a sinusoidal wave component, and an interactive vortex perturbation centered at source point (xs,ys)(x_s, y_s):

  1. Squared distance with smoothing kernel: r2=(xxs)2+(yys)2+0.025,r^2 = (x - x_s)^2 + (y - y_s)^2 + 0.025, where ϵ=0.025\epsilon = 0.025 prevents singularities when a particle passes through the exact vortex core.
  2. Horizontal and vertical velocities: vx=0.80.22k(yys)r2,v_x = 0.8 - \frac{0.22 k (y - y_s)}{r^2}, vy=0.13sin(9x+seedmod19)+0.22k(xxs)r2,v_y = 0.13 \sin(9x + \text{seed} \bmod 19) + \frac{0.22 k (x - x_s)}{r^2}, where kk represents vortex strength and seed\text{seed} controls vertical wave phase.
  3. Normalized directional step: The velocity vector is normalized and scaled to a uniform integration step size of 0.0090.009: Vstep=0.009(vx,vy)(vx,vy).\mathbf{V}_{\text{step}} = 0.009 \cdot \frac{(v_x, v_y)}{\|(v_x, v_y)\|}.
const field = (x, y) => {
const {source: s, strength: k, seed: currentSeed} = settings.current;
const dx = x - s.x;
const dy = y - s.y;
const radius = dx * dx + dy * dy + 0.025;
const vx = 0.8 - dy * 0.22 * k / radius;
const vy = 0.13 * Math.sin(x * 9 + currentSeed % 19) + dx * 0.22 * k / radius;
const length = Math.hypot(vx, vy) || 1;
return {x: vx / length * 0.009, y: vy / length * 0.009};
};

Two-Stage Rendering with Offscreen Buffers

Integrating 62 streamlines for 250 steps each on every animation frame would require 62×250=15,50062 \times 250 = 15{,}500 vector field evaluations per frame, consuming excessive CPU time.

To maintain 60 FPS animation, the renderer separates static streamline curves from dynamic particle heads:

  1. Static streamline precomputation (p5.Graphics): When parameters change or the canvas resizes, 62 streamlines are integrated once and drawn into an offscreen graphics buffer (paper):
    • Row colors alternate between the primary instrument red and neutral metal tone.
    • Points along each streamline are retained in an in-memory trajectory array.
  2. Live particle animation: On each frame, the canvas draws the cached offscreen image (p.image(paper, 0, 0)) and renders small particle circles advancing along the precomputed point arrays using modular indexing:
const trace = () => {
const css = getComputedStyle(node);
paper.background(css.getPropertyValue('--instrument-panel').trim());
ink = css.getPropertyValue('--instrument-red').trim();
const metal = css.getPropertyValue('--instrument-metal').trim();
paper.noFill();
paper.strokeWeight(0.8);
p.randomSeed(settings.current.seed);
paths = [];

for (let row = 0; row < 62; row++) {
let x = -0.1 + p.random(-0.025, 0.025);
let y = row / 51 - 0.1;
const points = [];
const color = p.color(row % 6 === 0 ? ink : metal);
color.setAlpha(row % 6 === 0 ? 145 : 78);
paper.stroke(color);
paper.beginShape();

for (let step = 0; step < 250; step++) {
points.push({x: x * p.width, y: y * p.height});
paper.vertex(x * p.width, y * p.height);
const v = field(x, y);
x += v.x; y += v.y;
if (x > 1.1 || y < -0.3 || y > 1.3) break;
}
paper.endShape();
paths.push(points);
}
dirty = false;
};

p.draw = () => {
if (!paper || disposed) return;
if (dirty) trace();
p.image(paper, 0, 0); // Blit cached streamlines in one call

p.noStroke(); p.fill(ink);
paths.forEach((points, index) => {
const point = points[(frame + index * 17) % points.length];
p.circle(point.x, point.y, index % 6 === 0 ? 3 : 1.6);
});
if (settings.current.playing) frame++;
};

Interaction and Accessibility

  • Pointer capture: The vortex handle captures pointer events (setPointerCapture), allowing uninterrupted manipulation even when dragging beyond container bounds.
  • Keyboard navigation: The handle accepts arrow keys (ArrowLeft, ArrowRight, ArrowUp, ArrowDown) with step size 0.0250.025, making the flow field fully accessible without a pointer device.
  • Autoplay: The homepage preview and full experiment page start animating even when the system requests reduced motion. The preview has no playback button; the full page provides pause and play controls. Animation pauses while offscreen or while the browser tab is hidden.
  • Cleanup: On unmount, event listeners, observers, offscreen graphics buffers (paper.remove()), and the p5 instance (sketch.remove()) are thoroughly disposed.
Explore connectionsOpen network