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.