Flow fields
A flow field gives a direction at every point of the plane. You can trace streamlines through it, which are curves that follow the flow, or advect particles and strokes along it. The direction comes from an angle function, so you can build a field from noise, from a formula, or from anything else you write. Streamlines are ordinary point lists, so they feed stroking, the shape booleans, hatching, and SVG export.
Contents
Building a field
The flowField and curlField sketch sugar build a field from the seeded Perlin noise. Both are reproducible under seed.
flowField(scale: Double = 0.002, turns: Double = 1, z: Double = 0) -> FlowField
curlField(scale: Double = 0.003, z: Double = 0) -> FlowField
flowFieldmaps the noise to an angle. A smallerscalegives a smoother field, andturnswidens the range of directions.curlFieldpoints along the divergence-free curl of the noise, so its streamlines are smooth, sourceless swirls.zis the noise slice. Animate it for a field that drifts over time.
You can also build a field from your own angle function:
let field = FlowField { p in atan2(p.y - 540, p.x - 540) + .pi / 2 } // a vortex around the center
flowField and curlField capture the sketch's noise, so trace streamlines from the field and hold those rather than storing the field itself.
Streamlines
field.streamline(from: Vector2, stepLength: Double = 4, steps: Int = 200, bounds: Rectangle? = nil) -> [Vector2]
field.streamlines(from seeds: [Vector2], stepLength: Double = 4, steps: Int = 200,
bounds: Rectangle? = nil, separation: Double? = nil) -> [[Vector2]]
streamline steps through a seed point both forward and backward, so the seed sits in the middle of the curve. It stops after steps in each direction, or earlier if the curve leaves bounds. streamlines traces one curve from each seed.
Pass a separation and the lines are traced evenly spaced. A line stops when it comes within separation of one already traced, and a seed too close to an existing line is skipped. The curves then fan out without crossing, which is the flow-field look. Seed the field densely, with a blue-noise set for example, and let the separation thin it out.
seed(3)
let field = flowField(scale: 0.0016)
let seeds = poissonDisk(radius: 10)
noFill(); strokeWeight(3); strokeCap(.round)
for line in field.streamlines(from: seeds, stepLength: 4, steps: 220, bounds: bounds, separation: 14) {
stroke(.white); drawPolyline(line)
}
Trace the lines once and hold them. You can then animate their color or width to get motion without tracing again. See the Streamlines example.
Advecting particles
field.advected(_ points: [Vector2], stepLength: Double = 2) -> [Vector2]
advected steps a set of points one move along the field. Call it each frame for particles or strokes drifting through the flow. Pair it with accumulation (noClear()) and the trails paint the field over time:
override func setup() { noClear() }
override func draw() {
let field = curlField(scale: 0.004)
let next = field.advected(particles, stepLength: 2)
for (a, b) in zip(particles, next) { stroke(.white); drawLine(a, b) }
particles = next.map { bounds.contains($0) ? $0 : Vector2(random(0, width), random(0, height)) }
}
Related: Noise (the Perlin and curl noise the fields are built from), Blue noise (the even seed set the streamlines fan out from), Geometry (the shape booleans and stroking the lines feed).