PL. 113 · CURVES / ARBELOS / CIRCLE INVERSION
Pappus Chain
Pappus of Alexandria, c. 320
OPEN THE LIVE PLATE ▸DEFINITION
arbelos: three semicircles, pairwise tangent, on one line chain circle Cₙ tangent to Cₙ₋₁ and to both bounding arcs height of centre(Cₙ) above the line = 2n · radius(Cₙ)
NOTES
Split a diameter at one point and raise three semicircles on the three spans it makes: the whole span, and its two pieces. That figure is the arbelos, the shoemaker knife of Greek geometry. Into one of its two lune-shaped bays an unending chain of circles packs exactly: a first circle tangent to all three arcs, then a sequence where each new circle is tangent to the one before it, to the big arc, and to one small arc. Pappus of Alexandria recorded the chain and its governing measurement in Book IV of the Collection, written around the year 320: the height of the center of the n-th circle above the baseline is always 2n times its own radius, whatever point the diameter was split at. The proof by Pappus was synthetic, built from geometry available in late antiquity. The construction here instead argues by circle inversion, a tool from nineteenth-century inversive geometry generally credited to Jakob Steiner, whose 1824 treatise on circle transformations established it: invert the whole figure through the point where the big arc and the hugged small arc touch, and that tangent pair becomes two parallel lines. Between two parallel lines the only tangent chain is trivial — a row of equal circles, each touching both lines and its neighbours, spaced one diameter apart. Undo the inversion and that row becomes the real chain. Doing this once per bay shows the two chains share their first circle outright: whichever bay it was built to seed, it ends up tangent to all three arcs, so the figure really has one root and two arms growing from it.
PROVENANCE
- Origin
- Pappus of Alexandria, Synagoge (Collection), Book IV, circa 320 — the chain of circles and its height theorem (2n times the radius) are his own result
- Standing
- Public domain — antiquity, predates any assignable DOI; transmitted through the Byzantine manuscript tradition of the Collection
- Proof method
- The synthetic proof in the Collection is not reproduced here. This plate proves the chain by circle inversion instead, generally credited to Jakob Steiner (1824 treatise on circle transformations) as the technique that made inversive geometry a subject — applying it to the arbelos chain is a standard later exercise, not a claim about what Pappus himself used
- Constants
- length is a resolution dial — more circles only resolve further down the same convergent sequence, never a different or a dead one — so it is locked from regenerate on apollonian recursion-depth and ford maximum-denominator precedent. ratio is jitterable across its whole declared box; measured live at every value, see the header above
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. 113 · PAPPUS CHAIN — Pappus of Alexandria, c. 320
// arbelos: three semicircles, pairwise tangent, on one line
// chain circle Cₙ tangent to Cₙ₋₁ and to both bounding arcs
// height of centre(Cₙ) above the line = 2n · radius(Cₙ)
// 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=pappus
float p_ratio = 0.38 + chf('ratio_tweak'); // arbelos split · live 0.08 .. 0.92
float p_length = 10 + chf('length_tweak'); // circles per arm · live 5 .. 14
// The plate's own colour: FORMA's CURVES 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.11 + 0.1196 * cos(6.28318530718 * (t + 0)),
0.46 + 0.5 * cos(6.28318530718 * (t + 0.05)),
0.2453 + 0.2667 * cos(6.28318530718 * (t + 0.1)));
}
// The Pappus chain, built the same way pappus.js builds it: circle
// inversion through the point where the big arc and the hugged small arc
// touch turns that tangent pair into two parallel lines, the only tangent
// chain between two parallel lines is a trivial row of equal circles one
// diameter apart, and inverting the row back gives the real chain. Run
// once per bay, pivoting at the opposite ends of the diameter (1, 0) and
// (-1, 0); the two arms come out sharing their first circle, which is kept
// once as the root. The plate's faint whole-chain underlay and its HILITE
// reveal head are the animation's legibility devices, not the finished
// figure, and are not ported — the same call ford.vex makes about its own
// underlay. The geometry already runs y upward from the baseline, so
// nothing is negated here, on ford.vex's own precedent.
// Circle-to-circle inversion: a circle centred (cx,cy), radius r, inverted
// through a centre (ox,oy) with inversion radius k, has image centre
// (ox,oy) + k^2(C-O)/s and radius |k^2 r / s|, where s is the power of the
// inversion centre with respect to the circle. Packed as [cx, cy, r].
function float[] forma_invert(float cx, cy, r, ox, oy, k){
float dx = cx - ox;
float dy = cy - oy;
float s = dx * dx + dy * dy - r * r;
float f = (k * k) / s;
return array(ox + dx * f, oy + dy * f, abs(k * k * r / s));
}
// One arm: hugs the arc of radius farR's partner (the OTHER small circle
// stays implicit — only its role as the far arc, tangent to both the big
// arc and the hugged one, matters) and spirals toward (pivotX, 0). Returns
// n circles flattened as [x0,y0,r0, x1,y1,r1, ...].
function float[] forma_arm(float farCx, farR, pivotX; int n){
float ox = pivotX, oy = 0.0, k = 1.0;
float far[] = forma_invert(farCx, 0.0, farR, ox, oy, k);
float xmid = far[0], rowR = far[2], step = 2.0 * rowR;
float probe[] = forma_invert(xmid, step, rowR, ox, oy, k);
float sign = probe[1] > 0.0 ? 1.0 : -1.0;
float res[];
for (int i = 1; i <= n; i++){
float c[] = forma_invert(xmid, sign * float(i) * step, rowR, ox, oy, k);
push(res, c[0]); push(res, c[1]); push(res, c[2]);
}
return res;
}
int forma_seg = 48;
float ratio = p_ratio;
int N = int(rint(p_length));
// the arbelos: big semicircle radius 1 at the origin, cut at one point
// into two smaller semicircles of radius `ratio` and `1 - ratio`
float leftR = ratio, leftCx = -1.0 + ratio;
float rightR = 1.0 - ratio, rightCx = ratio;
// the faint arbelos itself, upper semicircles only (theta 0..pi, y upward).
// VEX arrays are one-dimensional, so the three arcs are parallel arrays
// rather than a table, the same idiom apollonian.vex uses for its circles.
vector ctxCol = forma_ramp(0.0);
float ctxCx[], ctxCy[], ctxR[];
push(ctxCx, 0.0); push(ctxCy, 0.0); push(ctxR, 1.0);
push(ctxCx, leftCx); push(ctxCy, 0.0); push(ctxR, leftR);
push(ctxCx, rightCx); push(ctxCy, 0.0); push(ctxR, rightR);
for (int a = 0; a < 3; a++){
float cx = ctxCx[a], cy = ctxCy[a], cr = ctxR[a];
int prim = addprim(0, "polyline");
for (int i = 0; i <= forma_seg; i++){
float th = M_PI * float(i) / float(forma_seg);
int pt = addpoint(0, set(cx + cr * cos(th), cy + cr * sin(th), 0.0));
setpointattrib(0, "Cd", pt, ctxCol);
setpointattrib(0, "Alpha", pt, 0.28);
addvertex(0, prim, pt);
}
}
// the two arms, pivoting at the opposite ends of the diameter the hugged
// arc and the big arc are tangent at
float right[] = forma_arm(leftCx, leftR, 1.0, N);
float left[] = forma_arm(rightCx, rightR, -1.0, N);
// right and left share their first circle (n = 1) by construction — see
// pappus.js — so it is emitted once, coloured as the root
// seg arrives as a parameter: a VEX function cannot read a snippet-scope
// variable (no closures), which the cook reports as "variable is not
// accessible in this function" — the constant lives one literal deep here
// for the same reason.
function void forma_circle(float cx, cy, r; vector col; int seg){
int prim = addprim(0, "polyline");
for (int i = 0; i <= seg; i++){
float th = 6.28318530718 * float(i) / float(seg);
int pt = addpoint(0, set(cx + r * cos(th), cy + r * sin(th), 0.0));
setpointattrib(0, "Cd", pt, col);
setpointattrib(0, "Alpha", pt, 0.82);
addvertex(0, prim, pt);
}
}
forma_circle(right[0], right[1], right[2], forma_ramp(0.0), forma_seg);
for (int i = 1; i < N; i++){
// n = i + 1, so (n-1)/(N-1) = i/(N-1) — the plate's own bright-lobe map
float u = 0.82 + 0.2 * float(i) / float(N - 1);
forma_circle(right[i * 3], right[i * 3 + 1], right[i * 3 + 2], forma_ramp(u), forma_seg);
forma_circle(left[i * 3], left[i * 3 + 1], left[i * 3 + 2], forma_ramp(u), forma_seg);
}
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. 113 · PAPPUS CHAIN — Pappus of Alexandria, c. 320
// arbelos: three semicircles, pairwise tangent, on one line
// chain circle Cₙ tangent to Cₙ₋₁ and to both bounding arcs
// height of centre(Cₙ) above the line = 2n · radius(Cₙ)
// 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 CURVES accent, #3DFF88. Animation runs on time.
// This plate draws 3 separate paths at its published constants:
// duplicate the group (Contents › Group) that many times and each copy draws
// its own part, read from its position in the layer. A Slider Control named
// "part" on the layer pins one instead.
// https://forma-gen.com/#plate=pappus
// 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_ratio = 0.38 + forma_tweak("ratio_tweak"); // arbelos split · live 0.08 .. 0.92
var p_length = 10 + forma_tweak("length_tweak"); // circles per arm · live 5 .. 14
// 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.673477218253538; // 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]; }
function forma_partIndex(){
try { return Math.round(effect("part")("Slider")); } catch (e){}
try { return thisProperty.propertyGroup(3).propertyIndex - 1; } catch (e){ return 0; }
}
var forma_part = forma_partIndex();
// The Pappus chain in an arbelos: a unit semicircle cut at `ratio` into two
// smaller ones, and in each of the two crescents the chain of circles each
// tangent to the big arc, one small arc and the circle before it. Built as
// Pappus could not and Steiner could: invert about the point where the two
// hugged arcs touch, which sends both to parallel lines, so the chain there is
// a row of equal circles one diameter apart; invert each back. Every circle
// is tangent to the next, so a chain is one path — part 0 walks it from the
// outermost circle of the left arm, through the shared root, to the end of
// the right arm, going once round each circle at its kiss with the previous
// one (the retraced arc between kisses lies on itself). Part 1 is the arbelos
// itself, baseline and three arcs, one continuous stroke; part 2 is the
// theorem's mark — each centre stands 2n radii above the baseline — as a
// walk along the baseline with a spike up to every centre.
// parts: 3
var ratio = p_ratio, N = Math.round(p_length), TAU = 6.283185307179586;
function forma_invert(cx, cy, r, ox, oy, k){
var dx = cx - ox, dy = cy - oy, s = dx * dx + dy * dy - r * r, f = (k * k) / s;
return [ox + dx * f, oy + dy * f, Math.abs(k * k * r / s)];
}
var leftR = ratio, leftCx = -1 + ratio, rightR = 1 - ratio, rightCx = ratio;
function forma_arm(farCx, farR, pivotX){
var far = forma_invert(farCx, 0, farR, pivotX, 0, 1);
var xmid = far[0], rowR = far[2], step = 2 * rowR;
var probe = forma_invert(xmid, step, rowR, pivotX, 0, 1);
var sign = probe[1] > 0 ? 1 : -1, out = [];
for (var n = 1; n <= N; n++){
var c = forma_invert(xmid, sign * n * step, rowR, pivotX, 0, 1);
out.push({ n: n, x: c[0], y: c[1], r: c[2] });
}
return out;
}
var right = forma_arm(leftCx, leftR, 1), left = forma_arm(rightCx, rightR, -1);
var span = Math.min(forma_W, forma_H) * 0.84, scale = span / 2;
var baseX = forma_W / 2, baseY = forma_H / 2 + scale * 0.5;
function forma_canvas(c){ return { cx: baseX + c.x * scale, cy: baseY - c.y * scale, r: c.r * scale }; }
var chain = [];
for (var i = N - 1; i >= 1; i--) chain.push(forma_canvas(left[i]));
chain.push(forma_canvas(right[0]));
for (var j = 1; j < N; j++) chain.push(forma_canvas(right[j]));
var pts = [];
function forma_arc(c, a0, a1){
var n = Math.max(2, Math.ceil(Math.abs(a1 - a0) * c.r / 3));
for (var q = 0; q <= n; q++){
var th = a0 + (a1 - a0) * q / n;
pts.push(forma_pt(c.cx + c.r * Math.cos(th), c.cy + c.r * Math.sin(th)));
}
}
if (forma_part === 1){
pts.push(forma_pt(baseX - scale, baseY), forma_pt(baseX + scale, baseY));
forma_arc({ cx: baseX, cy: baseY, r: scale }, TAU, Math.PI);
forma_arc({ cx: baseX + leftCx * scale, cy: baseY, r: leftR * scale }, Math.PI, TAU);
forma_arc({ cx: baseX + rightCx * scale, cy: baseY, r: rightR * scale }, Math.PI, TAU);
} else if (forma_part === 2){
var byX = chain.slice().sort(function (u, v){ return u.cx - v.cx; });
for (var m = 0; m < byX.length; m++){
pts.push(forma_pt(byX[m].cx, baseY), forma_pt(byX[m].cx, byX[m].cy), forma_pt(byX[m].cx, baseY));
}
} else {
var entry = Math.PI / 2;
for (var k = 0; k < chain.length; k++){
var c = chain[k];
forma_arc(c, entry, entry + TAU); // once round from the kiss
if (k + 1 < chain.length){
var d = chain[k + 1], exit = Math.atan2(d.cy - c.cy, d.cx - c.cx);
var turn = ((exit - entry) % TAU + TAU) % TAU;
forma_arc(c, entry, entry + turn); // on to the next kiss, retraced
entry = exit + Math.PI; // the same point from the next centre
}
}
}
createPath(pts, [], [], false);