Ollin

Crack growth

CrackGrowth subdivides the plane the way glaze crackles. Straight cracks travel across the canvas, and every collision starts a new crack perpendicular to the one it hit. The rules are short. A crack advances until it reaches another crack's line or the edge of the canvas. There it stops, restarts perpendicular to a random point on the existing pattern, and adds one more crack. The result is a street map that keeps getting denser for as long as you let it run. The technique is the crack-subdivision algorithm of Jared Tarbell's Substrate (2003), implemented independently from the published descriptions.

Each crack also lays a translucent wash across the open space on one side of its line. That sand-painter shading is what gives the classic watercolor look. The helper emits geometry only, and you draw the marks yourself. The wash can therefore be inks on paper, glowing additive dust, or nothing at all.

Two panels: left, a vertical crack stopped on a horizontal line at an orange dot marked stops here, with an orange arrow setting out perpendicular from the vertical line; right, a plane subdivided into rectangular city blocks by fine dark cracks with faint colored washes beside them

Contents

CrackGrowth

CrackGrowth(width: Double, height: Double,
            cracks: Int = 3, seedAngles: Int = 16,
            maxCracks: Int = 200, seed: Int = 0)

CrackGrowth is a stateful stepper that you hold across frames. cracks sets how many start moving at once over a grid seeded with seedAngles phantom angles, and the population then grows to maxCracks. The same seed always grows the same pattern.

let field = CrackGrowth(width: 1080, height: 1080, seed: 7)

override func setup() { noClear() }

override func draw() {
    if frameCount == 1 { background(.white) }
    for mark in field.step(4) {
        fill(Color.black.withAlpha(0.33))
        drawPoint(at: mark.point)
    }
}

The canvas never clears, so each frame draws only that frame's marks and the picture builds up over time. field.segments returns every crack line so far as plain (Vector2, Vector2) segments, which are ready for SVG export and the pen plotter.

The marks and the wash

field.step()          // one tick; returns [Mark]
field.step(_ ticks:)  // several ticks, marks collected

CrackGrowth.grains(from: Vector2, to: Vector2,
                   gain: Double, count: Int = 64) -> [Grain]

Each Mark carries the crack's index, the new point on its line, and the washExtent where the open space ends. It also carries a gain in 0...1 that drifts slowly for each crack. The index is a stable palette key. grains lays the classic wash along that span. It returns count translucent grains, crowded toward the crack by a doubled sine ease, with alpha fading outward from 0.1.

for mark in field.step(4) {
    let ink = inks[mark.crack % inks.count]
    for grain in CrackGrowth.grains(from: mark.point, to: mark.washExtent,
                                    gain: mark.gain) {
        fill(ink.withAlpha(grain.alpha))
        drawPoint(grain.position)
    }
}

Practical notes

  • Accumulate, don't redraw. The look comes from thousands of faint marks piling up. Call noClear() and draw the background once, which is the whole recipe. See Examples/Patterns/Cracks.
  • The wash varies on its own. gain takes a random walk for each crack. The shading swells and thins along a line, with no styling work from you.
  • One ink per crack reads best. mark.crack stays the same for the crack's whole life, including its restarts. It therefore maps cleanly onto a palette.
  • maxCracks controls the density. The population only grows. Two hundred cracks fill a canvas in about a minute, and forty keep it sparse and architectural.
  • Plotter output is the segments, not the marks. field.segments gives you the street plan as clean line work. The wash is a raster effect, so leave it out of the vector layer.

Related: Single line and Spanning tree (the other line-work generators), Walks, and Export (SVG and the plotter path).

↑↓ move ↩ open