Skip to the plate
FORMA PUBLIC DOMAIN GENERATIVE ATLAS / ED. 0.28
Plate 30, Circle Packing: a still of the packing / greedy growth plate as the atlas renders it, in the lattices accent.

PL. 30  ·  LATTICES / PACKING / GREEDY GROWTH

Circle Packing

Classical; the greedy method is folklore

OPEN THE LIVE PLATE ▸

DEFINITION

place a candidate at random;
grow its radius until it meets another or the edge

NOTES

The naive algorithm — propose a centre, expand until something stops it, keep it if it is big enough — is not optimal packing in any mathematical sense, but it is the one that makes the drawings. Density falls off predictably, which is why the plate always ends up with a few large discs and a fine gravel of small ones.

PROVENANCE

Origin
Circle packing is classical; this greedy variant is common practice
Standing
Public domain
Constants
Attempts per frame governs how fine the gravel gets

HOUDINI · VEX

The same published mathematics as a Detail Wrangle body. Paste it into a Wrangle with Run Over set to Detail; every constant is the published value plus a tweak channel, so Create Spare Parameters gives a slider that starts where the paper does.

// FORMA — PL. 30 · CIRCLE PACKING — Classical; the greedy method is folklore
//   place a candidate at random;
//   grow its radius until it meets another or the edge
// Paste into a Detail Wrangle (Run Over: Detail), no inputs needed.
// Written from the published mathematics, not adapted from any code.
// Constants arrive at their published values. Press the node's Create
// Spare Parameters button and every tweak becomes a slider — starting
// at 0, the published figure, and moving in the constant's own units.
// https://forma-gen.com/#plate=packing

float p_tries = 2600 + chf('tries_tweak');    // placement attempts · live 200 .. 6000
float p_rmax  = 0.14 + chf('rmax_tweak');     // maximum radius · live 0.03 .. 0.3
float p_rmin  = 0.005 + chf('rmin_tweak');    // minimum radius · live 0.002 .. 0.03

// The plate's own colour: FORMA's LATTICES accent as a cosine ramp,
// brightest near t = 0 and t = 1, near-black around t = 0.5.
vector forma_ramp(float t){
  return set(
    0.46 + 0.5 * cos(6.28318530718 * (t + 0)),
    0.4185 + 0.4549 * cos(6.28318530718 * (t + 0.05)),
    0.1389 + 0.151 * cos(6.28318530718 * (t + 0.1)));
}

// The greedy packing run to its finish in one cook: propose a centre at
// random, shrink the candidate radius against every circle already kept and
// the four walls, keep it if it still clears the minimum. Density falls off
// exactly as the plate's does because the sequence of proposals is the
// plate's own — seeded(3) is a fixed seed there, so every cook and every
// card show the same gravel. The plate reveals the list over its clock and
// cursors the newest five in HILITE; the cook is the finished packing, so
// neither is ported. Domain is the unit square, where the radius constants
// are already fractions of the frame edge. Circles are closed polylines,
// drawn at the plate's own 0.94 inset.
int forma_seg = 48;

int tries = int(rint(p_tries));
float rmax = p_rmax, rmin = p_rmin;
float TAU = 6.28318530718;

float cx[], cy[], cr[];
int rc = 3;                       // the plate's fixed seed, counted upward
for (int i = 0; i < tries; i++){
    float x = random(rc);  rc++;
    float y = random(rc);  rc++;
    float r = rmax;
    for (int c = 0; c < len(cx); c++){
        float d = sqrt((x - cx[c]) * (x - cx[c]) + (y - cy[c]) * (y - cy[c])) - cr[c];
        if (d < r) r = d;
        if (r < rmin) break;
    }
    r = min(r, min(min(x, y), min(1.0 - x, 1.0 - y)));   // the four walls stop it too
    if (r >= rmin){ push(cx, x);  push(cy, y);  push(cr, r); }
}

for (int c = 0; c < len(cx); c++){
    // big discs bright, gravel dim — the plate's own radius mapping
    vector col = forma_ramp(min(1.0, cr[c] / rmax) * 0.5 + 0.1);
    int prim = addprim(0, "polyline");
    for (int i = 0; i <= forma_seg; i++){
        float th = TAU * float(i) / float(forma_seg);
        // canvas y runs down; negated so the packing sits as the plate shows it
        int pt = addpoint(0, set(cx[c] + cr[c] * 0.94 * cos(th),
                                 -(cy[c] + cr[c] * 0.94 * sin(th)), 0.0));
        setpointattrib(0, "Cd", pt, col);
        addvertex(0, prim, pt);
    }
}

AFTER EFFECTS · DECLINED

Hundreds of circles that never touch: no continuous path visits them, and one circle per copy of the group is not a port anyone would use.