PL. 36 · LATTICES / CIRCLE PACKING / RECURSIVE
Apollonian Gasket
Apollonius of Perga, c. 200 BC · Frederick Soddy, 1936
OPEN THE LIVE PLATE ▸DEFINITION
(k₁ + k₂ + k₃ + k₄)² = 2(k₁² + k₂² + k₃² + k₄²)
NOTES
Repeatedly filling the interstices between mutually tangent circles with smaller tangent circles. Apollonius posed the problem of circle tangency in antiquity; Soddy rediscovered the curvature formula in 1936 as the Kiss Precise poem. The resulting fractal limit set has a Hausdorff dimension of approximately 1.30568.
PROVENANCE
- Origin
- Apollonius of Perga (c. 200 BC), curvature theorem by Soddy (1936)
- Standing
- Public domain — ancient geometry theorem
- Constants
- Depth controls recursion level; spin animates rotation
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. 36 · APOLLONIAN GASKET — Apollonius of Perga, c. 200 BC · Frederick Soddy, 1936
// (k₁ + k₂ + k₃ + k₄)² = 2(k₁² + k₂² + k₃² + k₄²)
// 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=apollonian
float p_depth = 4 + chf('depth_tweak'); // recursion depth · live 2 .. 6
// 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)));
}
// Soddy's Kiss Precise: (k₁+k₂+k₃+k₄)² = 2(k₁²+k₂²+k₃²+k₄²) gives the
// fourth curvature, and its complex form (Lagarias, Mallows, Wilks 2002)
// gives the centres — both used as the plate does, in reflection form: the
// other circle tangent to three of a tangent quadruple is
// k₅ = 2(k₁+k₂+k₃) − k₄ and k₅z₅ = 2(k₁z₁+k₂z₂+k₃z₃) − k₄z₄, no root and
// no sign to choose. The recursion becomes an explicit stack of tangent
// triples, each carrying the fourth circle it leaves out — VEX has no
// recursion. The outer circle is unit radius (curvature −1);
// the pruning floor mirrors the plate's own 1.5 px at its typical frame.
// Each circle is sampled as a closed polyline.
// waived: spin — it paces the plate's rotation, and a cook has no clock
int forma_seg = 48;
float forma_minr = 0.005;
int depth = int(rint(p_depth));
float TAU = 6.28318530718;
// the circle list: centre, radius, curvature, recursion level
float cxs[], cys[], crs[], cks[];
int lvls[];
push(cxs, 0.0); push(cys, 0.0); push(crs, 1.0); push(cks, -1.0); push(lvls, 0);
float rin = 2.0 * sqrt(3.0) - 3.0; // the three equal inscribed tangents
float din = 1.0 - rin;
for (int i = 0; i < 3; i++){
float a = TAU / 4.0 + float(i) * TAU / 3.0;
push(cxs, cos(a) * din); push(cys, sin(a) * din);
push(crs, rin); push(cks, 1.0 / rin); push(lvls, 0);
}
// seed the stack with the four mutually tangent triples, each with the
// circle of the quadruple it leaves out, then fill
int sa[] = {0, 0, 0, 1};
int sb[] = {1, 2, 3, 2};
int sc[] = {2, 3, 1, 3};
int sd[] = {3, 1, 2, 0};
int sl[] = {1, 1, 1, 1};
while (len(sa) > 0){
int i1 = pop(sa), i2 = pop(sb), i3 = pop(sc), i4 = pop(sd), lvl = pop(sl);
if (lvl > depth) continue;
float k1 = cks[i1], k2 = cks[i2], k3 = cks[i3], k4 = cks[i4];
float k5 = 2.0 * (k1 + k2 + k3) - k4;
if (abs(k5) < 1e-9) continue;
float r5 = abs(1.0 / k5);
if (r5 < forma_minr) continue;
float x5 = (2.0 * (cxs[i1] * k1 + cxs[i2] * k2 + cxs[i3] * k3) - cxs[i4] * k4) / k5;
float y5 = (2.0 * (cys[i1] * k1 + cys[i2] * k2 + cys[i3] * k3) - cys[i4] * k4) / k5;
int m = len(cxs);
push(cxs, x5); push(cys, y5); push(crs, r5); push(cks, k5); push(lvls, lvl);
push(sa, i1); push(sb, i2); push(sc, m); push(sd, i3); push(sl, lvl + 1);
push(sa, i2); push(sb, i3); push(sc, m); push(sd, i1); push(sl, lvl + 1);
push(sa, i3); push(sb, i1); push(sc, m); push(sd, i2); push(sl, lvl + 1);
}
for (int c = 0; c < len(cxs); c++){
// the enclosing circle takes the accent itself — ramp(0) is its
// brightest value; the fills mirror the plate's own level colouring,
// held inside the ramp's bright lobe
vector col = cks[c] < 0.0 ? forma_ramp(0.0)
: forma_ramp(0.82 + 0.24 * float(lvls[c]) / float(depth));
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 gasket sits as the plate shows it
int pt = addpoint(0, set(cxs[c] + crs[c] * cos(th),
-(cys[c] + crs[c] * sin(th)), 0.0));
setpointattrib(0, "Cd", pt, col);
addvertex(0, prim, pt);
}
}
AFTER EFFECTS · EXPRESSION
The same published mathematics as a Shape Layer path expression. Paste it onto a Path property; every constant is the published value plus a Slider Control named
// FORMA — PL. 36 · APOLLONIAN GASKET — Apollonius of Perga, c. 200 BC · Frederick Soddy, 1936
// (k₁ + k₂ + k₃ + k₄)² = 2(k₁² + k₂² + k₃² + k₄²)
// After Effects port — paste onto a Shape Layer's Path property
// (Contents › Shape › Path). Written from the published mathematics, not
// adapted from any code. Constants arrive at their published values; add a
// Slider Control (Effect › Expression Controls) named <k>_tweak and that
// constant moves in its own units, starting at 0 — the published figure.
// The plate's comet and its reveal are Trim Paths; the stroke colour is
// FORMA's LATTICES accent, #FFE84D. Animation runs on time.
// https://forma-gen.com/#plate=apollonian
// A missing slider reads 0, so a bare paste already draws the figure.
function forma_tweak(n){ try { return effect(n)("Slider"); } catch (e){ return 0; } }
var p_depth = 4 + forma_tweak("depth_tweak"); // recursion depth · live 2 .. 6
var p_spin = 0.05 + forma_tweak("spin_tweak"); // spin speed · live 0.01 .. 0.2
// The frame: the plate's W × H canvas is this comp, with the origin at the
// layer's anchor; canvas y already runs down, as After Effects' does.
var forma_W = thisComp.width, forma_H = thisComp.height, forma_t = time;
var forma_phase = 0.572913809446618; // this plate's own fixed phase, as the page has it
function forma_pt(x, y){ return [x - forma_W / 2, y - forma_H / 2]; }
// The Apollonian gasket: three equal circles inside a unit one, then every
// interstice filled by the circle tangent to its three walls, to the depth
// on the slider. Soddy's theorem gives each curvature and its complex form
// (Lagarias, Mallows, Wilks 2002) the centre, both in reflection form: the
// other circle tangent to three of a mutually tangent quadruple is
// k₅ = 2(k₁+k₂+k₃) − k₄ and k₅z₅ = 2(k₁z₁+k₂z₂+k₃z₃) − k₄z₄.
//
// One path, because a gasket is one figure: every circle touches the one it
// was born from, so the whole set is a tree of tangencies and a single stroke
// can walk it — enter each circle at the point where it kisses its parent, go
// once round, dropping into each child at its own kiss and coming back, and
// leave by the point of entry. A retraced arc lies exactly on itself, so the
// stroke shows each circle once. Trim Paths then grows the gasket circle by
// circle, depth first. The three inner circles turn at the spin rate from
// this plate's own phase, as on the page.
var cx = forma_W / 2, cy = forma_H / 2, R = Math.min(forma_W, forma_H) * 0.45;
var TAU = 6.283185307179586, ang = forma_t * p_spin + forma_phase * TAU;
var depth = Math.round(p_depth);
var circles = [{ x: cx, y: cy, r: R, k: -1 / R, kids: [] }];
var rIn = R * (2 * Math.sqrt(3) - 3), dIn = R - rIn;
for (var i = 0; i < 3; i++){
var a = ang + i * TAU / 3;
var c = { x: cx + Math.cos(a) * dIn, y: cy + Math.sin(a) * dIn, r: rIn, k: 1 / rIn, kids: [] };
circles.push(c);
circles[0].kids.push(c); // born inside the outer circle
}
function forma_fill(c1, c2, c3, c4, lvl){
if (lvl > depth) return;
var k5 = 2 * (c1.k + c2.k + c3.k) - c4.k;
if (!isFinite(k5) || Math.abs(k5) < 1e-9) return;
var r5 = Math.abs(1 / k5);
if (r5 < 1.5) return; // the page's own floor
var c5 = { x: (2 * (c1.x * c1.k + c2.x * c2.k + c3.x * c3.k) - c4.x * c4.k) / k5,
y: (2 * (c1.y * c1.k + c2.y * c2.k + c3.y * c3.k) - c4.y * c4.k) / k5,
r: r5, k: k5, kids: [] };
circles.push(c5);
c1.kids.push(c5); // the first wall is the parent it kisses
forma_fill(c1, c2, c5, c3, lvl + 1);
forma_fill(c2, c3, c5, c1, lvl + 1);
forma_fill(c3, c1, c5, c2, lvl + 1);
}
forma_fill(circles[0], circles[1], circles[2], circles[3], 1);
forma_fill(circles[0], circles[2], circles[3], circles[1], 1);
forma_fill(circles[0], circles[3], circles[1], circles[2], 1);
forma_fill(circles[1], circles[2], circles[3], circles[0], 1);
var pts = [];
function forma_arc(c, a0, a1){ // from angle a0 to a1 on circle c, a1 excluded
var n = Math.max(2, Math.ceil(Math.abs(a1 - a0) * c.r / 4));
for (var j = 0; j < n; j++){
var th = a0 + (a1 - a0) * j / n;
pts.push(forma_pt(c.x + c.r * Math.cos(th), c.y + c.r * Math.sin(th)));
}
}
// The angle on circle c of its kiss with circle d: towards d when d sits
// outside c (or c is the enclosing circle and d inside it), the same
// direction either way.
function forma_kiss(c, d){ return Math.atan2(d.y - c.y, d.x - c.x); }
function forma_walk(c, entry){
var kids = c.kids.slice();
for (var q = 0; q < kids.length; q++) kids[q].at = ((forma_kiss(c, kids[q]) - entry) % TAU + TAU) % TAU;
kids.sort(function (u, v){ return u.at - v.at; });
var here = entry;
for (var m = 0; m < kids.length; m++){
var kid = kids[m], to = entry + kid.at;
forma_arc(c, here, to);
here = to;
// the child's kiss with c is the same point, seen from its own centre
forma_walk(kid, c.k < 0 ? to : to + Math.PI);
}
forma_arc(c, here, entry + TAU);
pts.push(forma_pt(c.x + c.r * Math.cos(entry), c.y + c.r * Math.sin(entry)));
}
forma_walk(circles[0], 0);
createPath(pts, [], [], false);