One walk through the curl of a noise field, restarting wherever it runs out of room.
Following a field
There are no paths stored anywhere in this piece. There is one moving point, and at
every step it asks the field which way to go:
var curl = CurlNoise(p.X, p.Y, noise);
var dest = new SKPoint(p.X + curl[0], p.Y + curl[1]);
If the step is legal it is drawn and the point moves there. If it is not — the path
has run too long, or left the frame, or reached somewhere already occupied — the
point is thrown somewhere else and starts again. Ten thousand steps of that, and the
result is a field of streamlines.
The first few hundred steps. Short strokes, each one a chain of single steps rather
than a curve that was planned.
Why curl and not gradient
The direction comes from the curl of a scalar noise field, which in two
dimensions is the gradient rotated by ninety degrees:
var dx = (Fbm(x, y + eps) - Fbm(x, y - eps)) / (2 * eps);
var dy = (Fbm(x + eps, y) - Fbm(x - eps, y)) / (2 * eps);
return new[] { dx / l, -dy / l };
Note which derivative goes where: the y-derivative becomes the x-component, and the
x-derivative becomes the negated y-component. That swap is the rotation, and it is
the whole trick.
A field built this way is divergence-free. Following the gradient of noise sends
every path downhill into the same basins, and the drawing collapses into blobs;
following the curl sends every path around the hills, along the contours, and
nothing ever converges. It is the difference between water running off a landscape
and water circulating in it.
The noise itself is three octaves of simplex noise summed with halving amplitude —
fractional Brownian motion — so the flow has structure at three scales at once.
A sixth of the walk. Paths run parallel where the underlying noise is flat and fan
apart where it turns.
The spacing is enforced, not emergent
Even spacing between streamlines does not come from the field. It comes from a
Poisson-disc grid that refuses to accept a point too close to one already taken:
if (traveled < MaxPathLength && Math.Abs(dest.X) < 110 && … && grid.Insert(dest))
grid.Insert returns false if the destination is within Radius of an existing
point, and that single test does three jobs at once. It stops a path from crossing
another, it stops a path from crossing itself, and — when a path is restarted — it
forces the new seed point into genuinely empty space.
So the density of the finished drawing is set by Radius alone, and is uniform
everywhere regardless of what the field is doing.
A third. Restarts now have to search: the loop tries up to a thousand random points
before giving up, and most of the frame is already claimed.
Two thirds. The strokes have become dense enough to read as a single field rather
than as separate marks.
Where it stops
The walk ends one of two ways: ten thousand steps, or a restart that fails a
thousand times in a row. The second is the interesting one — it means the frame is
saturated to the resolution the Poisson radius allows, and no further stroke could
be placed anywhere.
Path length is capped too, at MaxPathLength units of travel. Without it a single
streamline in a smooth region would wander the whole frame and claim it, and the
drawing would be a few long snakes instead of a texture.
Five renderings
| Rendering | Freq | Stroke | Seed | Radius | Path cap | Ink |
|---|
| Neon Teal | 2.0 | 1.0 px | 69 | 1.3 | 50 | #00CED1 on #000000 |
| Ember | 1.6 | 1.5 px | 42 | 1.6 | 85 | #FF5500 on #0C0401 |
| Phantom Purple | 3.2 | 0.65 px | 17 | 0.95 | 50 | #CC00FF on #0A0015 |
| Drift | 1.2 | 0.9 px | 31 | 1.4 | 110 | #7EC8E3 on #020810 |
| Chalk | 2.2 | 0.9 px | 77 | 1.15 | 50 | #DDD8C4 on #131313 |
Phantom Purple also raises MaxIterations to 12 000; every other rendering uses
the default 10 000.
The renderings differ across seed, frequency, Poisson radius, path-length cap, and
stroke weight — so each one draws a field with a distinct density, path character,
and line weight, not only a different ink.
Frequency scales the noise coordinates and sets how tightly the field turns: Drift
at 1.2 produces long, gentle sweeps; Phantom Purple at 3.2 produces short, tight
curls. Radius sets the spacing between paths and the step length; Phantom Purple
at 0.95 is the densest, Ember at 1.6 the sparsest. The path-length cap limits how
far a single streamline runs before restarting: Drift at 110 produces a few long
arcs spanning the frame; Ember at 85 produces medium arcs; everything else at 50.
Gallery presentation
One plate in the gallery is the piece hung. MuseumFrame — the presentation
layer from SkiaSharp.Extensions — hangs the lead rendering the way a gallery
would: a charcoal wall, a near-black mat with an opening, the artwork fitted
into it, and a black frame, with every band sized as a fraction of the shorter
canvas side. The frame knows nothing about curl noise and the field knows
nothing about being framed: the inner piece is generated identically to
neon-teal and fitted into the opening, so the framing never touches the
generative geometry and could present any of the presets above without change.
Determinism
Both the noise and the restart generator are seeded from the preset’s single Seed,
and every step depends only on the state before it. The restart generator is
System.Random, which the project’s determinism policy flags as not guaranteed
stable across .NET versions; the simplex noise is the project’s own and is stable.