Skip to the plate
FORMA PUBLIC DOMAIN GENERATIVE ATLAS / ED. 0.28
Plate 36, Apollonian Gasket: a still of the circle packing / recursive plate as the atlas renders it, in the lattices accent.

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 _tweak, so a bare paste already draws the figure and each slider moves one constant in its own units. Trim Paths is the comet.

// 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);