What is a flow field?
A flow field is a grid of directions. Every point on it says which way to go. Give it to a drawing and the randomness stops scattering and starts flowing.
The short definition
Take a patch of space and put a number at every point. Turn each number into an angle. You now have a field of arrows, and anything you drop on it has somewhere to go.
The numbers almost always come from a noise function, usually Perlin or simplex. Noise gives you values that drift smoothly rather than jump about. Neighbouring arrows end up pointing nearly the same way, so the field reads as a current rather than a mess.
What you do with the field is a separate choice. Most generative art releases particles onto it and draws their trails. You can also read the numbers as brightness, as a height, as a nudge on some other decision. The field is just the numbers. The picture is what you make of them.
The rest of this page shows that idea working, using Flield, a generator that runs in the browser. Every panel below is drawn live by the same code the app uses. Where a name is Flield's own rather than the general term, it says so.
Static, and then not
Start with the difference a field makes. Both panels below use the same seed, the same density, and the same colors. The only thing that changes is whether a flow field is steering the result.
No field
With a field
Left, every cell decides on its own and the result is evenly scattered. Right, a field biases those decisions, and the same randomness gathers into bands and thins into valleys.
Six ways to draw one field
In most generative art, a flow field is literally a grid of vectors. You sample noise at each point, turn the value into an angle, and release particles that steer by whatever angle they're standing on. The particles trace the field, and the drawing is the trails they leave.
A field is a set of numbers. Tracing particles through it is one thing you can do with those numbers. Flield does that, and five other things.
The six names below are Flield's, not the field's. No one else calls them this. They are worth a look anyway, because each one is a different answer to the same question: you have the numbers, so what do you draw?
| Texture | What it makes of the field |
|---|---|
| Streaks | Reads the value as a nudge on how likely each cell is to fill. Combed bands. |
| Nebula | Folds the value at zero. The curves the field crosses there turn into bright filaments with dark gaps around them. |
| Marble | Moves where each cell reads from, using a second and coarser field. Then it does what Streaks does. |
| Contours | Cuts the value into levels and draws the lines between them. A contour map of the field. |
| Streamlines | Turns the value into an angle and traces particles along it. The classic construction. |
| Weave | Streamlines twice over, the second set a quarter turn across the first. |
Two of them are worth explaining in full. They are the two furthest apart, and they are below. The rest are variations on one or the other.
Streamlines: the vectors, traced
This is the classic construction. The left panel below is the field itself. One arrow per patch of grid, each pointing where a particle standing there would go.
The right panel is what you get by dropping particles on that field and recording where they went. Turn the curl up and the arrows swing further off the flow direction. The strands bend with them, because both are reading the same numbers.
The vectors
Particles tracing them
At zero curl every arrow points the same way and the particles run in straight lines. The noise is what makes a field a field.
Two details do most of the work here.
The first is that each particle lives for exactly one animation cycle. Then it starts again from its own fixed point. That is what lets an animated flow field close on itself, rather than cut between two frames.
The second is that a young trail wraps onto the far end of its own path. So the amount of strand on screen never dips.
The trails land in the same grid of cells as everything else in the tool. That is how they end up mirrored, kaleidoscoped, masked to a circle and exported as SVG.
Streaks: the field as a bias
Streaks never builds a vector at all, and it is why the tool's default output looks like woven fabric rather than like hair. Here the sampling space itself is rotated and stretched before the noise is read from it. Sample isotropic noise at plain grid coordinates and you get blobs with no preferred direction. Rotate the coordinates toward a heading first, and divide one axis by a stretch factor. The same noise function now returns values that vary slowly along that heading and quickly across it. Blobs become streaks, pointing where you aimed them.
function sampleFlowField(noise2D, x, y, { scale, angleRad, stretch, octaves }) {
const cos = Math.cos(angleRad);
const sin = Math.sin(angleRad);
const rx = x * cos + y * sin;
const ry = -x * sin + y * cos;
const nx = rx / stretch / scale;
const ny = ry / scale;
return fbm(noise2D, nx, ny, octaves); // -1..1
}
Four lines of arithmetic, and they're the whole trick. rx and ry are the point rotated into flow-aligned space. Dividing rx by stretch compresses the coordinate along the flow direction, which means the noise takes longer to change as you travel that way. Everything after is ordinary noise.
Seeing the field itself
The field is invisible in the finished artwork, so here it is directly. The left panel draws the raw value returned by that function at every point, dark for low and light for high. The right panel is the composition those values produce. Drag the sliders and watch them move together.
The field
What it produces
Direction rotates the sampling space. Stretch decides how far the noise travels before it changes, so a low value gives clouds and a high one gives long combed streaks.
Bias, not a gate
There's one more decision in here that matters more than it looks, and it's a single line.
The obvious way to use a field is as a switch: if the value clears a threshold the cell fills, otherwise it doesn't. That works, and it produces hard-edged regions with a visible boundary between filled and empty, because every cell on one side of the threshold agrees with every other.
Instead, the field adjusts the probability that a cell fills, and the cell still rolls its own dice:
let p = density;
if (useField) {
const n = sampleFlowField(noise2D, x, y, { scale, angleRad, stretch, octaves });
p = Math.min(1, Math.max(0, density + field.strength * n));
}
grid[y][x] = rand() < p;
Now a cell in a dense region is merely likely to fill, and one in a sparse region is merely unlikely. Regions blend into each other through a gradient of probability, instead of meeting at an edge. The texture stays grainy everywhere, rather than going solid where the field is strong. That is the difference between the two panels in the first demo, and it is one comparison operator.
Layering the noise
A single noise sample is smooth to the point of being bland. Summing several at doubling frequencies and halving amplitudes, conventionally called fractal Brownian motion, adds fine detail on top of the broad shape without disturbing it. That is the octaves argument, and two or three is usually enough here: the grid is coarse, so detail finer than a cell is wasted work.
The noise underneath is Ken Perlin's gradient noise, using the fade curve from his 2002 improved reference implementation, seeded so the same string always produces the same field.
Try it
Every control on this page is in the app, on each layer's tab, under Flow field. Texture picks which of the six you are drawing. Field Strength, Flow Scale, Flow Direction, Flow Stretch and Turbulence steer all six.
Some of them change name as you switch, because the job changes. On Streamlines and Weave, strength becomes Curl, stretch becomes Trail length and the smoothing passes become Strand weight. On Contours, strength becomes Levels, because what it buys there is a count of lines.
Open Flield and drag them, or read the guide for the rest of the tool.
Two pages take the field further. Looping animated backgrounds sets it moving and closes the loop with no cut. Seamless tiling backgrounds mirrors it into a tile that repeats with no seam.
The implementation is one file, generator.js, with no dependencies and no build step. It knows nothing about the interface, so it runs anywhere a canvas does. That is how the demos on this page work. The same functions, at a smaller size.