Skip to the plate
FORMA PUBLIC DOMAIN GENERATIVE ATLAS / ED. 0.28
Plate 46, Pythagoras Tree: a still of the recursion / square pair plate as the atlas renders it, in the fractals accent.

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 _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. 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);