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.
What a flow field is made of
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.
Two choices make the field what it is.
- Where the numbers come from
- Almost always a noise function, usually Perlin or simplex. Noise gives 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 them
- A separate decision. Most generative art releases particles onto the field and draws their trails. You can also read the numbers as brightness, as a height, or as a nudge on some other choice.
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 seamless background 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.
The same composition, with and without a field
Both panels below use the same seed, the same density and the same colors. The only difference is whether a 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 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 are standing on. The particles trace the field, and the drawing is the trails they leave.
That is one thing to do with a set of numbers. Flield does it, and five others.
| 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. |
The two furthest apart are explained in full below. The rest are variations on one or the other.
Streamlines: particles traced along the field
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.
- Each particle lives 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.
- 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: noise stretched into bands
Streaks never builds a vector at all. That is why the tool's default output looks like woven fabric rather than like hair. What gets rotated and stretched here is the sampling space itself, before any noise is read from it.
Sample isotropic noise at plain grid coordinates and you get blobs with no preferred direction. So do two things to the coordinates first:
- Rotate them toward a heading.
- 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 are 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, so the noise takes longer to change as you travel that way. Everything after that is ordinary noise.
The field drawn beside what it produces
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.
Why the field nudges instead of switching
One more decision matters more than it looks, and it is a single line.
The obvious way to use a field is as a switch. If the value clears a threshold the cell fills, and otherwise it does not. That works, and it produces hard-edged regions with a visible boundary between filled and empty. 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 rather than meeting at an edge. The texture stays grainy everywhere instead of going solid where the field is strong.
That is the difference between the two panels in the first demo. It is one comparison operator.
Octaves: layering the noise
A single noise sample is smooth to the point of being bland. Sum several of them at doubling frequencies and halving amplitudes and you get fine detail on top of the broad shape, without disturbing the shape. The convention is called fractal Brownian motion.
That is the octaves argument. Two or three is usually enough here, because the grid is coarse and 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
- Field strength becomes Curl, which is how far the noise swings a strand off the flow direction. Flow stretch becomes Trail length. Smoothing passes becomes Strand weight.
- On Contours
- Field strength becomes Levels, because what it buys there is a count of lines.
Open Flield, free with no signup, and drag them. The guide covers the rest of the tool.
Read next
- Looping animated backgrounds sets the field 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.