Three mutually tangent circles determine a fourth, and doing that forever fills the gaps.
Descartes’ theorem
If four circles are mutually tangent, their curvatures — the reciprocals of their
radii — satisfy
(k₁ + k₂ + k₃ + k₄)² = 2(k₁² + k₂² + k₃² + k₄²)
Solve that for k₄ and it is a quadratic, so there are two answers. Given three
mutually tangent circles and one circle already sitting in the gap, the other root
is the circle sitting in the opposite gap:
k₄ = 2(k₁ + k₂ + k₃) − k'₄
No square root survives. Finding the next circle costs one addition and one
subtraction — which is why a gasket with tens of thousands of circles in it takes
no time at all to compute.
The classic seed configuration: curvatures −1, 2, 2, 3. The negative curvature
belongs to the enclosing circle, which contains the others rather than touching them
from outside. Every gasket in this piece grows from exactly this.
The centres come along for free
Curvature alone is not enough to draw anything — the centres are needed too, and
there is a complex version of the same theorem that supplies them. Track the
product k·z, curvature times centre as a complex number, and it obeys the identical
recurrence:
k₄z₄ = 2(k₁z₁ + k₂z₂ + k₃z₃) − k'₄z'₄
So each disk is stored as the pair (k, kz) and the radius and centre are recovered
by division. That is the whole data structure:
private readonly record struct Disk(double K, Complex KZ)
{
public double Radius => 1.0 / Math.Abs(K);
public double CX => KZ.Real / K;
public double CY => KZ.Imaginary / K;
}
One generation, and three circles rather than four. The seed configuration offers
four triples, but the fourth — the two k = 2 circles and the enclosing one — has
the k = 3 circle already sitting in it, so Descartes’ other root reproduces a
circle that is already there. The degeneracy test catches it:
if (Math.Abs(k_new - known.K) < 1e-6) return;
The three that do land have curvatures 15, 6 and 6 — integers, which is not a
coincidence.
Three gaps for every circle
Placing a circle into a gap does not close the gap; it divides it. A new disk
tangent to a, b and c creates three fresh triples — (new, b, c),
(a, new, c) and (a, b, new) — and each of those is another gap wanting a circle.
Generate(disk, b, c, a, result, minK, depth + 1, maxDepth);
Generate(a, disk, c, b, result, minK, depth + 1, maxDepth);
Generate(a, b, disk, c, result, minK, depth + 1, maxDepth);
The count triples each generation, and the radii shrink fast enough that the total
area still converges. The gasket’s limit set has Hausdorff dimension about 1.3057 —
more than a curve, less than a region, and irrational in a way nobody has been able
to write down in closed form.
Two generations. Nine more circles, three from each of the three, and the
characteristic branching into every corner is established.
Four. The eye now reads the whole gasket from a fraction of its circles, because the
arrangement is self-similar and the missing detail is all at scales the frame cannot
resolve anyway.
Where it stops
Nothing stops it mathematically — the recursion is infinite. What stops it here is
the canvas:
var minK = scale / MinRadius; // curvature at which a circle is a pixel wide
if (k_new > minK) return; // curvature above the cutoff = circle below MinRadius
MinRadius is stated in pixels, converted to a curvature, and compared directly
against the curvature Descartes’ theorem just produced. A circle that would draw
smaller than that is not drawn, and — more importantly — is not recursed into, which
prunes a whole infinite subtree in one comparison.
The floor reached. Every circle down to about a pixel across, and the tangency
structure carried all the way down.
Integers all the way down
There is a curiosity in this particular seed. Start at (−1, 2, 2, 3) and every
curvature the recursion produces is an integer. The reason is the recurrence
itself — 2(k₁ + k₂ + k₃) − k₄ cannot leave the integers if it starts in them — and
gaskets with this property are called integral. The circle radii in this drawing
are therefore exactly 1/1, 1/2, 1/3, 1/6, 1/7, 1/15, …, and the tiny ones near the
tangency points have curvatures in the thousands.
Six renderings
| Rendering | Min radius | Style | Treatment |
|---|
| Classic | 1.0px | stroke 0.7px | #0F1729 on white |
| Vellum | 2.0px | stroke 1.4px | #2E1A0E on #F2E8D5 |
| Draft | 1.0px | stroke 0.5px | #D4EBF8 on #0E1E2E |
| Spectrum | 1.0px | stroke 0.6px | hue 180°→320° by curvature, on #050510 |
| Ember | 1.5px | filled | hue 25°→260° by curvature, on #080508 |
| Museum | 1.0px | stroke 0.7px | Classic, gallery-framed with mat and shadow |
Spectrum and Ember both colour circles by the logarithm of curvature: the largest
circles take the starting hue and the pixel-sized ones take the ending hue. In
Spectrum, circles are stroked as usual. In Ember they are filled — sorted largest
to smallest before drawing so each new circle appears on top — creating a layered,
jewel-like surface rather than an open lattice.
Vellum recurses only to a 2-pixel floor, halving the depth and leaving the
large-scale structure legible at a glance. The warm ground and heavy stroke give
it the feel of a printed plate or etching.
Draft matches Classic in depth but inverts the value: precise pale strokes on
a dark ground, evoking a technical drawing or architectural plan.
Museum presents the classic drawing as a finished exhibit using the gallery
framing from SkiaSharp.Extensions: the artwork sits in a mat inside a dark
frame, lifted off the wall by a soft drop shadow. The circular form floats in a
square field, which is the traditional relationship between a round subject and
its mount.
Determinism
Nothing is seeded and nothing is random. The four starting curvatures are literals,
the recursion is depth-first in a fixed order, and circles are added to the list in
insertion order. In stroke mode they are drawn in that order; in fill mode they are
sorted by curvature ascending so the largest are drawn first. Both orderings are
deterministic. The only place the arithmetic could drift is the degeneracy test
Math.Abs(k_new - known.K) < 1e-6, which rejects the root that reproduces the
circle already in the gap.