Flow Field
Ten thousand particles follow a wind you cannot see. The wind’s direction at every point comes from Perlin noise, a smooth random field Ken Perlin published in 1985, here seeded so that each seed is a different wind. Each particle leaves a hairline trail, and in sixty seconds the trails build up a drawing of the whole field: broad currents, and bright seams where the flow converges. Then the drawing holds, and a new seed slowly fades in beneath it.
About this piece
Imagine a wind blowing across the frame that you cannot see: at every point it has a direction, and the direction changes smoothly from place to place. Drop ten thousand particles into it, let each one drift a short way along the wind and leave a hairline behind, and the trails draw a map of the wind. That is a flow field. Here every particle takes 10 steps a second, each step 0.18% of the frame’s short side long (about 1.2 px on this page’s stage). A particle lives for 30 to 120 steps, then is reborn somewhere random. After 60 seconds, 600 steps, about 5.9 million segments have been laid down. The drawing holds for 6 seconds, then the next seed starts drawing and the finished picture fades out under it over another 6. The very first drawing opens twelve seconds in (those 120 steps are taken at once when the page loads), so the shape of the wind is on screen straight away; every later drawing starts from an empty frame. Every visitor sees the same drawings in the same order: drawing 0 first, then 1, 2 and so on. With reduced motion switched on you get one still, the first drawing, and nothing moves.
Where the wind comes from: Perlin noise
The wind’s direction is gradient noise, the kind Ken Perlin introduced in his SIGGRAPH 1985 paper “An Image Synthesizer” (Computer Graphics 19(3), pages 287–296). Space is cut into a grid of unit squares. Each grid corner gets a gradient, a short arrow picked by hashing the corner’s coordinates through a shuffled table of the numbers 0 to 255. At a point inside a square, each of the four corners contributes the dot product of its arrow with the offset from that corner to the point, and the four numbers are blended. Because every corner’s ramp is zero at the corner itself, the noise is exactly 0 on every grid point and rises and falls smoothly between them.
This plate follows the two changes Perlin made in “Improving Noise” (SIGGRAPH 2002), as written in his own Java reference implementation (read 12 October 2026). First, the blend uses the fade curve 6t⁵ − 15t⁴ + 10t³ instead of the older 3t² − 2t³, so that the second derivative is also continuous across grid lines. Second, the arrow is chosen from a short fixed list by the low bits of the hash, not from a table of random vectors. Perlin’s reference is three-dimensional, with twelve arrows. This is the two-dimensional version, with eight: the four diagonals (±1, ±1) and the four axes. One deliberate difference from the reference: Perlin uses one fixed permutation table, and this plate shuffles the table from a seed, so each seed is a different wind and the same seed is always the same wind.
noise(x, y) in [-1, 1], exactly 0 at whole-number x, y
angle(x, y) = base + noise(2.2·x + ox, 2.2·y + oy) · 2.4π
step = 0.0018 · (cos angle, sin angle)
(x, y and step in short sides of the frame)
The factor 2.2 sets the scale: about 2.2 noise cells across the frame’s short side, which is why you see three or four large swirls rather than dozens of small ones. The factor 2.4π sets how hard the wind turns: when the noise moves from 0 to 0.5 the wind swings through 216°. The seed also picks base, a starting direction, and an offset (ox, oy) into the noise. Without base, the wind would blow due right at every grid point, where the noise is zero, and every drawing would lean the same way.
How this differs from Contour Drift
Contour Drift also starts from a noise field, so the two plates are easy to confuse. They differ in three ways. The noise: Contour Drift uses value noise, a random height at each grid corner blended with 3t² − 2t³, summed over three octaves. This plate uses one octave of Perlin’s gradient noise, which has no peaks sitting on the grid and so less of a square bias. What the number means: there the noise is a height, and marching squares draws the lines where the height equals twelve fixed levels. Here the noise is an angle, and nothing is ever drawn where a value is reached; lines are drawn where particles go. Time: Contour Drift’s field moves along a third axis, and each frame is a complete, fresh map. This field stands still, and the picture is an accumulation of trails: a single frame here is only the latest step of a drawing that takes a minute. The home page hero is a flow field too, but a simpler one: about 440 to 1,500 particles on summed value noise that drifts slowly, with trails that fade continuously instead of building up to a finished drawing.
Reading the plate: what to watch for
- The bright ridges are where the wind converges. Nothing in this field forces the flow to spread evenly. Where neighbouring arrows point toward one line, particles from a wide area are funnelled onto it and their trails pile up into a bright seam; where they point apart, the frame stays thin and dark. Most of the structure you see is that piling up, not the noise itself.
- One in four lines is coral. Particle i takes the palette’s ink slot i mod 4, and three of the four slots are cobalt. That is the site’s own cobalt-to-coral weighting. In the dense seams the coral blends into the cobalt; it shows best in the thin regions between them.
- A new drawing starts out looking like fur. For its first few seconds each trail is a short dash and you can see individual particles. As particles die and are reborn, new strands fill the gaps, and by about 30 seconds the dashes have merged into continuous currents.
- The same wind is drawn twice per cycle, once fading. During the 6-second cross-fade both drawings are on screen. Their ridges sit in completely different places, because a new seed reshuffles the whole permutation table.
How it is computed
Each drawing has a clock in wall-clock seconds, and the number of steps taken is that time multiplied by 10, rounded down. The frame rate only decides how many steps fall between two pictures, never which steps are taken, so the drawing at a given second is the same at 30 frames a second and at 144. The test suite checks this on the plate’s own code: run for 10 seconds and for 70 seconds (past a change of seed) at both frame rates, every particle’s position is identical. All the randomness (the shuffle, the offset, every birth point and lifetime) comes from one seeded generator (mulberry32), drawn in particle order.
Each step’s segments are stroked into an off-screen layer that is never cleared, in four paths, one per ink slot, with the same low opacity for every segment. That opacity is worked out from the frame size and the particle count, so that a whole drawing lays about the same amount of ink per pixel on any screen. On this page’s stage it is 0.04 on an ordinary screen and about 0.08 on a 2× (Retina-class) one, where the frame has four times the pixels. The visible frame is the layer drawn over the background; during a cross-fade it is two layers at complementary opacities. The gallery tile runs the same code with the same ten thousand particles, so the thumbnail and this stage are one picture at two sizes. The reduced-motion still is drawing 0 after 240 steps, with each segment 2.5 times as opaque, so it carries the ink of a finished drawing for less than half the work.
Honest limits
It is not a fluid. Real air cannot pile up in one place, but this wind can: the field is just an angle at each point, with no rule that what flows in must flow out. That is exactly why the bright seams form. A divergence-free field, such as curl noise, would spread the trails evenly and look like smoke instead; it is a different picture, and not this one.
No line avoids another. Tyler Hobbs’s widely read essay “Flow Fields” describes keeping a minimum distance between curves so that each line stays visible. Nothing here does that. Ten thousand particles cross and overlap freely, and the image is a density map of where they went, not a set of separate, clean curves.
Thin lines at low opacity hit the limits of 8-bit colour. Each segment adds only a few units out of 255 to a pixel. On a phone, where the same ten thousand particles share far fewer pixels, the opacity is held at a floor of 0.04 so the strokes still register, and the drawing ends up denser than on a laptop. In the densest seams, rounding can leave faint dark specks.
A resize or a theme change starts the drawing again. Strokes already laid down cannot be recoloured or moved, so the current seed is redrawn from its first step, and after a theme change the old-coloured picture fades out beneath it.
One octave, eight arrows. A single octave of noise gives broad, smooth swirls and no fine detail. Eight fixed gradients can leave faint preferred directions along the diagonals and axes. Adding octaves, or using Perlin’s later simplex noise, would change both.
A 30-line version
The snippet below draws one complete flow-field drawing in 30 lines of HTML and plain JavaScript, with no library. It keeps the plate’s noise and its constants: the same seeded shuffle of Perlin’s table, the same 2002 fade and eight gradients, 10,000 particles, 600 steps at 10 a second, 2.2 cells per short side, a 2.4π swing, steps of 0.0018 short sides and lives of 30 to 120 steps. For seed 7, the test suite runs the snippet’s noise function against the plate’s on 10,000 points and requires the same value at every one. What it leaves out is the gallery machinery: the cross-fade to new seeds, the off-screen layer, sharp drawing on high-density screens (it draws in CSS pixels) and the reaction to theme changes.
- Noise. p is Perlin’s permutation table: the numbers 0 to 255, shuffled from the seed, then repeated so p[X + 1] never runs off the end. grad turns a hash into one of the eight arrows and takes its dot product with the offset from the corner. noise blends the four corners with fade.
- Wind. Positions are measured in short sides, so the swirls have the same size relative to the window on any screen. base and the offset ox, oy come from the seed.
- Particles. Each step moves a particle one STEP along the wind and draws that segment. A particle whose life runs out, or that leaves the window, is reborn at a random point on its next turn. Particle i draws in ink slot i % 4, so each step is four strokes.
- Clock. Like the plate, it counts steps from the time since it started, not from frames, so a 120 Hz screen draws at the same speed as a 60 Hz one. Under reduced motion it draws all 600 steps at once, which takes a second or two, and then never redraws.
Things worth changing: TURN = 0.5 gives a gentle, almost straight drift; TURN = 6 gives tight curls and small vortices. FREQ = 8 shrinks the swirls to a fine grain. Change SEED for a different wind. Replace fade with (t) => t and the grid shows: currents bend sharply where they cross a cell edge, which is the artefact the fade curve exists to remove.
<canvas id="flow" style="position:fixed; inset:0"></canvas>
<script>
const dark = matchMedia('(prefers-color-scheme: dark)').matches, still = matchMedia('(prefers-reduced-motion: reduce)').matches;
const [ground, inks] = dark ? ['#0B0D12', ['150,180,255', '150,180,255', '120,158,255', '255,120,84']]
: ['#E7E2D5', ['40,72,205', '40,72,205', '52,88,214', '190,68,28']]; // the site's palette: 3 cobalt slots, 1 coral
const N = 10000, STEPS = 600, RATE = 10, FREQ = 2.2, TURN = 2.4, STEP = 0.0018, SEED = 7; // the plate's constants
const rng = (a) => () => { a = (a + 0x6D2B79F5) | 0; let t = Math.imul(a ^ (a >>> 15), 1 | a); t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; };
const rand = rng(SEED), p = new Uint8Array(512).map((_, i) => i & 255);
for (let i = 255; i > 0; i--) { const j = Math.floor(rand() * (i + 1)); [p[i], p[j]] = [p[j], p[i]]; } // shuffle Perlin's permutation from the seed
p.copyWithin(256, 0, 256);
const fade = (t) => t * t * t * (t * (t * 6 - 15) + 10), lerp = (t, a, b) => a + t * (b - a); // Perlin 2002: 6t^5 - 15t^4 + 10t^3
const G = [[1, 1], [-1, 1], [1, -1], [-1, -1], [1, 0], [-1, 0], [0, 1], [0, -1]], grad = (h, x, y) => G[h & 7][0] * x + G[h & 7][1] * y;
function noise(x, y) { const X = Math.floor(x) & 255, Y = Math.floor(y) & 255; x -= Math.floor(x); y -= Math.floor(y);
const u = fade(x), v = fade(y), A = p[X] + Y, B = p[X + 1] + Y; // hash the four corners, blend their gradient ramps
return lerp(v, lerp(u, grad(p[A], x, y), grad(p[B], x - 1, y)), lerp(u, grad(p[A + 1], x, y - 1), grad(p[B + 1], x - 1, y - 1))); }
const cv = document.getElementById('flow'), ctx = cv.getContext('2d'), W = cv.width = innerWidth, H = cv.height = innerHeight, S = Math.min(W, H);
const ox = rand() * 256, oy = rand() * 256, base = rand() * Math.PI * 2, alpha = Math.max(0.04, Math.min(0.5, 0.35 * W * H / (N * STEPS * STEP * S)));
const spawn = (q) => { q.x = rand() * W / S; q.y = rand() * H / S; q.life = 30 + Math.floor(rand() * 91); return q; };
const pts = Array.from({ length: N }, () => spawn({}));
ctx.fillStyle = ground; ctx.fillRect(0, 0, W, H); ctx.lineWidth = 1;
function step() { for (let j = 0; j < 4; j++) { ctx.strokeStyle = `rgba(${inks[j]},${alpha})`; ctx.beginPath();
for (let i = j; i < N; i += 4) { const q = pts[i]; if (q.life-- <= 0) { spawn(q); continue; }
const a = base + noise(q.x * FREQ + ox, q.y * FREQ + oy) * TURN * Math.PI; // the wind's angle at this point
ctx.moveTo(q.x * S, q.y * S); q.x += Math.cos(a) * STEP; q.y += Math.sin(a) * STEP; ctx.lineTo(q.x * S, q.y * S);
if (q.x < 0 || q.y < 0 || q.x > W / S || q.y > H / S) q.life = 0; }
ctx.stroke(); } }
let done = 0; const t0 = performance.now();
function frame(now) { const due = Math.min(STEPS, Math.floor((now - t0) / 1000 * RATE)); while (done < due) { step(); done++; } if (done < STEPS) requestAnimationFrame(frame); }
if (still) { while (done < STEPS) { step(); done++; } } else requestAnimationFrame(frame); // reduced motion: the whole drawing at once
</script>
Curious how the loop and canvas fit together? Read how it works →
More from the gallery
- Plate 37 Dragon Curve A paper strip folded in half twelve times and opened: four Heighway dragons that tile.
- Plate 01 Quantum Fibonacci Golden-angle phyllotaxis blooming from the seed outward.
- Plate 02 Bitcoin Matrix A quiet rain of ₿ and hex sliding down the glass.
- Plate 03 Cosmic Circles Soft orbs drifting through slow orbital rounds.
- Plate 04 Spiral Waves Archimedean arms turning, so the bands seem to stream inward.
- Plate 05 Particle Burst A continuous bloom of particles from the core.