PL. 46 · FRACTALS / RECURSION / SQUARE PAIR
Pythagoras Tree
Albert E. Bosman, 1942
OPEN THE LIVE PLATE ▸DEFINITION
on each square, a right triangle of angle θ two squares on its legs, scaled cos θ and sin θ area per level: cos²θ + sin²θ = 1
NOTES
Bosman, a Dutch engineering teacher, drew it by hand with compasses during the war and published it in a book about the wonder of plane geometry. Each square carries a right triangle on its shoulders and two smaller squares on the legs; the theorem itself guarantees every level adds exactly the same total area, while the silhouette turns from scaffolding into a wind-blown tree as the angle leaves 45°.
PROVENANCE
- Origin
- A. E. Bosman, drawn 1942; published in "Het wondere onderzoekingsveld der vlakke meetkunde", 1957
- Standing
- Public domain — a geometric construction
- Constants
- Depth doubles the square count per level; the angle sets the lean
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. 46 · PYTHAGORAS TREE — Albert E. Bosman, 1942
// on each square, a right triangle of angle θ
// two squares on its legs, scaled cos θ and sin θ
// area per level: cos²θ + sin²θ = 1
// 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=pythagoras
float p_ang = 45 + chf('ang_tweak'); // θ — branch angle · live 25 .. 65
float p_depth = 9 + chf('depth_tweak'); // levels · live 5 .. 11
// The plate's own colour: FORMA's FRACTALS 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.1389 + 0.151 * cos(6.28318530718 * (t + 0.05)),
0.1912 + 0.2078 * cos(6.28318530718 * (t + 0.1)));
}
// On each square, a right triangle of angle θ; on its legs, two squares
// scaled cos θ and sin θ — the theorem itself keeps every level's total
// area constant. Bosman's recursion is already breadth-first in the plate
// — a worklist, not a call stack — and VEX walks the same growing list.
// Each square is a closed polyline; the root square's edge is the unit.
int dmax = int(rint(p_depth));
float th = p_ang * 3.14159265359 / 180.0;
float co = cos(th), si = sin(th);
// the worklist: each entry a baseline a → b and its level
float nax[] = {0.0}, nay[] = {0.0}, nbx[] = {1.0}, nby[] = {0.0};
int nlvl[] = {0};
for (int i = 0; i < len(nax); i++){
float ax = nax[i], ay = nay[i], bx = nbx[i], by = nby[i];
int lvl = nlvl[i];
float ux = bx - ax, uy = by - ay;
// the square hangs one edge above its baseline (canvas y runs up-negative)
float ddx = ax + uy, ddy = ay - ux;
float ccx = bx + uy, ccy = by - ux;
// colour climbs the bright lobe of the ramp a storey at a time,
// exactly the plate's own level colouring
vector col = forma_ramp(0.84 + 0.22 * float(lvl) / float(dmax));
int prim = addprim(0, "polyline");
// array(), not a brace literal — VEX braces take constants only
float qx[] = array(ax, bx, ccx, ddx, ax);
float qy[] = array(ay, by, ccy, ddy, ay);
for (int v = 0; v < 5; v++){
// canvas y runs down; negated so the tree grows upward
int pt = addpoint(0, set(qx[v], -qy[v], 0.0));
setpointattrib(0, "Cd", pt, col);
setpointattrib(0, "Alpha", pt, 0.8);
addvertex(0, prim, pt);
}
if (lvl < dmax){
// apex of the right triangle sitting on the top edge d → c
float tx = ccx - ddx, ty = ccy - ddy;
float ex = ddx + co * (tx * co + ty * si);
float ey = ddy + co * (ty * co - tx * si);
push(nax, ddx); push(nay, ddy); push(nbx, ex); push(nby, ey); push(nlvl, lvl + 1);
push(nax, ex); push(nay, ey); push(nbx, ccx); push(nby, ccy); push(nlvl, lvl + 1);
}
}
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. 46 · PYTHAGORAS TREE — Albert E. Bosman, 1942
// on each square, a right triangle of angle θ
// two squares on its legs, scaled cos θ and sin θ
// area per level: cos²θ + sin²θ = 1
// 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 FRACTALS accent, #FF4D6A. Animation runs on time.
// https://forma-gen.com/#plate=pythagoras
// 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_ang = 45 + forma_tweak("ang_tweak"); // θ — branch angle · live 25 .. 65
var p_depth = 9 + forma_tweak("depth_tweak"); // levels · live 5 .. 11
// 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.6314572622068226; // 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 Pythagoras tree: on each square a right triangle with the slider's
// angle at its left corner, and a square on each leg — 2^(depth+1) − 1
// squares, the page's own construction and fit (0.9 of the frame, measured
// for this angle, since the lean changes completely with it). A tree of
// squares is one path: every child square shares a corner with its parent
// (the triangle's base is the parent's top edge), so the walk goes round each
// square from the corner it shares with its parent, dropping into the left
// child at the top-left corner and the right child at the top-right, and
// coming back the way it went. A retraced edge lies on itself, so each square
// is stroked once and Trim Paths grows the tree branch by branch.
var th = p_ang * Math.PI / 180, co = Math.cos(th), si = Math.sin(th);
var depth = Math.round(p_depth);
var minx = 1e9, maxx = -1e9, miny = 1e9, maxy = -1e9;
function forma_grow(a, b, lvl){
// a → b is the base, left to right; the square stands on the far side of it
var ux = b[0] - a[0], uy = b[1] - a[1];
var d = [a[0] + uy, a[1] - ux], c = [b[0] + uy, b[1] - ux];
var sq = { p: [a, b, c, d], kids: [] };
for (var v = 0; v < 4; v++){
if (sq.p[v][0] < minx) minx = sq.p[v][0]; if (sq.p[v][0] > maxx) maxx = sq.p[v][0];
if (sq.p[v][1] < miny) miny = sq.p[v][1]; if (sq.p[v][1] > maxy) maxy = sq.p[v][1];
}
if (lvl < depth){
var tx = c[0] - d[0], ty = c[1] - d[1];
var e = [d[0] + co * (tx * co + ty * si), d[1] + co * (ty * co - tx * si)];
sq.kids.push(forma_grow(d, e, lvl + 1), forma_grow(e, c, lvl + 1));
}
return sq;
}
var root = forma_grow([0, 0], [1, 0], 0);
var s = Math.min(forma_W * 0.9 / (maxx - minx), forma_H * 0.9 / (maxy - miny));
var ox = (forma_W - (maxx - minx) * s) / 2 - minx * s;
var oy = (forma_H - (maxy - miny) * s) / 2 - miny * s;
var pts = [];
function forma_put(q){ pts.push(forma_pt(ox + q[0] * s, oy + q[1] * s)); }
// Walk a square from corner `from` (0 = a, 1 = b, 2 = c, 3 = d) the long way
// round, visiting the children at d (left) and c (right), back to `from`.
function forma_walk(sq, from){
var p = sq.p;
for (var i = 1; i <= 4; i++){
var v = (from + i) % 4;
forma_put(p[v]);
if (sq.kids.length){
if (v === 3) forma_walk(sq.kids[0], 0); // the left child stands on d → e
if (v === 2) forma_walk(sq.kids[1], 1); // the right child on e → c
}
}
}
forma_put(root.p[0]);
forma_walk(root, 0);
createPath(pts, [], [], false);