A single Bézier curve, turned into a field of distances, and drawn by walking along its contours.
The curve you cannot see
Everything in this drawing is derived from one line of SVG path data:
M100,-80 C-120,-80 -120,0 0,0 C120,0 120,80 -100,80
Two cubic Béziers, joined, making a lazy S. It is never drawn. What is drawn is the
distance field around it — the function that maps every point of the plane to how
far it is from the nearest point of that curve — and the strokes are contours of that
function.
The first strokes. Each is a chain of single steps, and each step is a query against
the curve rather than a lookup in a table.
From distance to direction
The field’s gradient points directly away from the curve. Rotate it ninety degrees
and you get a direction that runs along the curve at constant distance:
var dx = (Dist(x, y + Eps) - Dist(x, y - Eps)) / (2 * Eps);
var dy = (Dist(x + Eps, y) - Dist(x - Eps, y)) / (2 * Eps);
// swapped and negated: the perpendicular
So a walk that follows this field traces an offset curve. Every stroke in the piece
is a level set of the distance function, and their spacing is the piece’s only other
subject.
Dist itself is brute force with a shortcut: each Bézier is flattened to segments
at a resolution proportional to its length, and the distance to the path is the
minimum over the segments. There is no analytic solution for the distance from a
point to a cubic Bézier that is worth using here, and there does not need to be.
A fifth of the walk. Strokes near the curve are tight and follow it closely; strokes
far from it are broad and have started to forget its shape.
Why the strokes are evenly spaced
They are not evenly spaced because the field says so. They are evenly spaced because
a Poisson-disc grid refuses any step that lands within Radius of a point already
taken:
bool poissonOk = pGrid.Insert(nx, ny);
if (withinLength && withinBounds && poissonOk) { … } else { restart }
That one test stops a stroke crossing another, stops it crossing itself, and forces
every restart into genuinely empty space. It is also what ends the piece: when a
thousand consecutive random restarts all land in occupied space, the loop breaks and
the drawing is done.
Half. The contour structure is now unmistakable, and so is the thing that makes a
distance field interesting to draw.
Cusps
Look for the places where contours meet at a point rather than running parallel.
Those are on the medial axis — the set of points equidistant from two different
parts of the curve. The distance function is continuous there but its gradient is
not: approach from one side and the nearest point is on one arm of the S, approach
from the other and it jumps to the other arm.
The walk cannot cross that line smoothly, so strokes pile into it and stop. Every
cusp in this drawing marks a place where the curve is, in a precise sense, equally
close in two directions.
Saturated. The pen has run out of places to start.
Five fields
Each variant draws the same field differently — changing what it follows, how far it
travels, or which aspect of the field it shows.
Lime Flow follows the S-curve. Strokes run along its iso-contours at medium
density, making the hidden curve legible through the pattern it leaves behind.
Ember Orbit replaces the S-curve with a circle. The distance field of a closed
curve pulls streamlines into orbits: strokes loop around the ring, stacking into
concentric paths rather than running in parallel. The field is the same kind of
mathematics; the topology produces a different picture.
Chalk takes the S-curve field and shortens the maximum path length from fifty
to twenty-two units. Individual strokes become marks rather than arcs. The curve’s
shape emerges from the grain of the marks and their orientation — impression rather
than outline.
Indigo Curl switches the rendering mode entirely. Instead of walking a flow
field, it samples the curl vector at each point of a regular grid and draws a small
arrow. The S-curve organises the arrows rather than the strokes.
Crimson Sweep returns to streamlines but extends the maximum path length to
one hundred units and widens the Poisson spacing to 1.7. A small number of long,
uninterrupted arcs cross the frame. Each one has enough room to follow the field
far enough that the curvature becomes visible.
Gallery framing
Museum takes Lime Flow and places it on a warm mat — 100 pixels of off-white on
every side, the same colour as cotton rag paper. The artwork itself is unchanged; the
mat is a separate rendering layer that never touches the generative code. It exists
to show the piece as a finished object rather than a frame-filling output.
Determinism
Every restart is seeded from an explicit per-preset value, so each rendering is
fully determined by its parameters. The curve is parsed from a literal string with
InvariantCulture, so the geometry does not depend on locale. The restart generator
is System.Random, which the project’s determinism policy flags as not guaranteed
stable across .NET versions.
Origin
Ported from a Turtletoy sketch by Reinder Nijhoff (2023, MIT License).