Ollin

Space colonization

Space colonization grows a branching structure toward a scattered set of attraction points. On every step, each remaining attractor pulls on the single closest branch node within reach. A pulled node then grows one step toward the average of the pulls on it, and an attractor is consumed once a branch reaches it. That is the classic venation-and-branching growth model, so veins, roots, lightning, and trees all come out of the same loop. What changes is where you scatter the attractors and where you plant the roots.

Four panels of the same growth at step 6, 18, 40, and finished: ink veins spread from a bottom root into a field of orange dots, and the dots vanish as branches reach them

SpaceColonization keeps its own state, so you hold on to it and call step() each frame, or run it to the end with grow(). The algorithm draws no random numbers, which means the same attractors and roots always grow the same structure. Scatter the attractors with a seeded generator and the whole sketch is reproducible.

seed(7)
let growth = SpaceColonization(attractors: poissonDisk(radius: 26),
                               roots: [Vector2(width / 2, height - 70)])

override func draw() {
    growth.step()
    background(.black)
    stroke(.white)
    for (a, b) in growth.segments { drawLine(a, b) }
}

Contents

Building a growth

SpaceColonization(attractors: [Vector2], roots: [Vector2],
                  influenceRadius: Double = 120, killRadius: Double = 24,
                  stepLength: Double = 12, maxNodes: Int = 6000)

The three distances have to sit in a particular relationship. Keep stepLength smaller than killRadius, because otherwise a branch can step right past the attractor it is chasing. Keep killRadius well below influenceRadius. A smaller stepLength gives smoother, denser curves, and a larger influenceRadius lets branches reach across open space toward far attractors.

poissonDisk is the usual source of attractors, because it covers the area evenly and leaves no clumps. Any point set works. Sample a Shape's interior for growth that fills a silhouette, or a circle's rim for a radial burst.

Stepping and finishing

step() advances one growth round, step(_:) advances several, and grow() runs to the end. isFinished turns true once every reachable attractor is consumed and nothing can grow further. Attractors that sit outside every branch's reach are left standing, and the growth still stops. nodes and attractors hold the live state, so a sketch can draw the attractors nothing has reached yet as waiting seeds.

Segments and thickness

Each Node carries its position and the index of its parent, so the structure is a tree. segments flattens that tree into (parent, child) line pairs for drawLine, hatching, or SVG export. To give the branches organic weight, thicknesses(tipWidth:exponent:) runs the pipe model. Every tip gets tipWidth, and a parent's width combines its children's widths, raised to exponent and then re-rooted. Trunks come out thick and twigs come out hairline:

let widths = growth.thicknesses(tipWidth: 1.4, exponent: 2.2)
for (i, node) in growth.nodes.enumerated() {
    guard let parent = node.parent else { continue }
    strokeWeight(widths[i])
    drawLine(growth.nodes[parent].position, node.position)
}

Shaping the result

  • Leaf venation: attractors from poissonDisk over the canvas, and one root at the bottom edge (the Patterns/Venation example).
  • A tree: attractors scattered in a crown-shaped region near the top, and the root at the ground. Keep influenceRadius small so branches do not shortcut across the trunk.
  • Lightning or roots: attractors in a tall band below the root, and a raised stepLength for jagged lines.
  • Several plants: more than one point in roots. Each attractor feeds whichever structure reaches it first, so the plants divide the space between them.

Related: Blue noise (the attractor scatter), Diffusion-limited aggregation (dendrites from random walkers, not pulls), L-systems (branching from a grammar, not from space), Differential growth (the other stateful grower).

↑↓ move ↩ open