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.
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 thebackgroundonce, which is the whole recipe. SeeExamples/Patterns/Cracks. - The wash varies on its own.
gaintakes 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.crackstays the same for the crack's whole life, including its restarts. It therefore maps cleanly onto a palette. maxCrackscontrols 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.segmentsgives 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).