PL. 67 · LATTICES / TILING / APERIODIC
Penrose Tiling
Roger Penrose, 1974 · after Robinson and de Bruijn
OPEN THE LIVE PLATE ▸DEFINITION
deflate each Robinson triangle by φ = (1+√5)/2 acute → one acute + one obtuse obtuse → two obtuse + one acute
NOTES
Two rhombs that tile the plane and can never tile it periodically. Penrose found the set in 1974; the proof that no arrangement of them ever repeats is what made it famous, and Shechtman’s quasicrystals turned it from recreation into physics a decade later. This plate builds it by deflation — start with ten Robinson triangles around a point and subdivide each in the golden ratio, forever. Every vertex configuration you see is forced.
PROVENANCE
- Origin
- R. Penrose, "The Rôle of Aesthetics in Pure and Applied Mathematical Research", Bull. Inst. Math. Appl. 10, 1974; de Bruijn gave the pentagrid construction in 1981
- Patent
- US4133152, granted 1979 to Penrose, covering the tiles as a puzzle — long expired
- Standing
- Free to use. Built here by triangle deflation, from the published rule.
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. 67 · PENROSE TILING — Roger Penrose, 1974 · after Robinson and de Bruijn
// deflate each Robinson triangle by φ = (1+√5)/2
// acute → one acute + one obtuse
// obtuse → two obtuse + one acute
// 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=penrose
float p_depth = 5 + chf('depth_tweak'); // deflations · live 3 .. 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)));
}
// Penrose's rhombs by triangle deflation, the plate's own construction: ten
// Robinson triangles around a point — the sun patch, alternating handedness
// so neighbours meet edge to edge — each subdivided in the golden ratio,
// depth times over, with the recursion carried as flat arrays since VEX has
// none. Each triangle is half a rhomb mirrored across its odd edge, so only
// the two matching sides are drawn and the internal bisectors are omitted;
// which edge is odd is found by length, as the plate finds it, so it cannot
// be got wrong. Acute and obtuse halves sit on the plate's two steps of the
// bright lobe. The reveal's underlay and HILITE head are the animation's.
// Unit circumradius; deterministic by construction — nothing is random.
float PHI = (1.0 + sqrt(5.0)) / 2.0;
int depth = int(rint(p_depth));
// the sun patch: type, then A, B, C per triangle, kept as flat arrays
int ty[];
float axs[], ays[], bxs[], bys[], cxs[], cys[];
for (int i = 0; i < 10; i++){
float a0 = float(2 * i - 1) * M_PI / 10.0, a1 = float(2 * i + 1) * M_PI / 10.0;
float Bx = cos(a0), By = sin(a0), Cx = cos(a1), Cy = sin(a1);
if (i % 2 == 1){
float t;
t = Bx; Bx = Cx; Cx = t;
t = By; By = Cy; Cy = t;
}
push(ty, 0);
push(axs, 0.0); push(ays, 0.0);
push(bxs, Bx); push(bys, By);
push(cxs, Cx); push(cys, Cy);
}
// deflation: the subdivision point sits one golden section along an edge —
// the only place it can go if the children are to be Robinson triangles
for (int it = 0; it < depth; it++){
int nty[];
float nax[], nay[], nbx[], nby[], ncx[], ncy[];
for (int i = 0; i < len(ty); i++){
float Ax = axs[i], Ay = ays[i], Bx = bxs[i], By = bys[i], Cx = cxs[i], Cy = cys[i];
if (ty[i] == 0){
float Px = Ax + (Bx - Ax) / PHI, Py = Ay + (By - Ay) / PHI;
push(nty, 0); push(nax, Cx); push(nay, Cy); push(nbx, Px); push(nby, Py); push(ncx, Bx); push(ncy, By);
push(nty, 1); push(nax, Px); push(nay, Py); push(nbx, Cx); push(nby, Cy); push(ncx, Ax); push(ncy, Ay);
} else {
float Qx = Bx + (Ax - Bx) / PHI, Qy = By + (Ay - By) / PHI;
float Rx = Bx + (Cx - Bx) / PHI, Ry = By + (Cy - By) / PHI;
push(nty, 1); push(nax, Rx); push(nay, Ry); push(nbx, Cx); push(nby, Cy); push(ncx, Ax); push(ncy, Ay);
push(nty, 1); push(nax, Qx); push(nay, Qy); push(nbx, Rx); push(nby, Ry); push(ncx, Bx); push(ncy, By);
push(nty, 0); push(nax, Rx); push(nay, Ry); push(nbx, Qx); push(nby, Qy); push(ncx, Ax); push(ncy, Ay);
}
}
ty = nty;
axs = nax; ays = nay; bxs = nbx; bys = nby; cxs = ncx; cys = ncy;
}
// the two matching sides of each half-rhomb, the odd edge found by length
for (int i = 0; i < len(ty); i++){
float Ax = axs[i], Ay = ays[i], Bx = bxs[i], By = bys[i], Cx = cxs[i], Cy = cys[i];
float ab = sqrt((Ax - Bx) * (Ax - Bx) + (Ay - By) * (Ay - By));
float bc = sqrt((Bx - Cx) * (Bx - Cx) + (By - Cy) * (By - Cy));
float ca = sqrt((Cx - Ax) * (Cx - Ax) + (Cy - Ay) * (Cy - Ay));
float e0x, e0y, e1x, e1y, f0x, f0y, f1x, f1y;
if (abs(ab - bc) < 1e-6){ e0x = Cx; e0y = Cy; e1x = Ax; e1y = Ay; f0x = Ax; f0y = Ay; f1x = Bx; f1y = By; }
else if (abs(bc - ca) < 1e-6){ e0x = Ax; e0y = Ay; e1x = Bx; e1y = By; f0x = Bx; f0y = By; f1x = Cx; f1y = Cy; }
else { e0x = Bx; e0y = By; e1x = Cx; e1y = Cy; f0x = Cx; f0y = Cy; f1x = Ax; f1y = Ay; }
vector col = forma_ramp(ty[i] ? 1.04 : 0.86);
for (int e = 0; e < 2; e++){
float x0 = e ? f0x : e0x, y0 = e ? f0y : e0y;
float x1 = e ? f1x : e1x, y1 = e ? f1y : e1y;
// canvas y runs down; negated so the patch turns as the plate shows it
int q0 = addpoint(0, set(x0, -y0, 0.0));
int q1 = addpoint(0, set(x1, -y1, 0.0));
setpointattrib(0, "Cd", q0, col); setpointattrib(0, "Cd", q1, col);
setpointattrib(0, "Alpha", q0, 0.85); setpointattrib(0, "Alpha", q1, 0.85);
addprim(0, "polyline", q0, q1);
}
}
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. 67 · PENROSE TILING — Roger Penrose, 1974 · after Robinson and de Bruijn
// deflate each Robinson triangle by φ = (1+√5)/2
// acute → one acute + one obtuse
// obtuse → two obtuse + one acute
// 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=penrose
// 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 = 5 + forma_tweak("depth_tweak"); // deflations · live 3 .. 6
// 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.47748878179118037; // 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]; }
// One path through a connected set of segments (each [x0, y0, x1, y1]):
// vertices within eps are one vertex, a vertex on the inside of a segment
// splits it, duplicate segments are one segment, and a depth-first walk takes
// every segment out and back, so each is drawn on itself and the pen never
// leaves the figure.
function forma_graphPath(segs, eps){
var cell = eps * 4, verts = [], grid = {}, edges = [], ekey = {};
function vid(x, y){
var gx = Math.floor(x / cell), gy = Math.floor(y / cell);
for (var i = -1; i <= 1; i++) for (var j = -1; j <= 1; j++){
var list = grid[(gx + i) + "," + (gy + j)];
if (!list) continue;
for (var k = 0; k < list.length; k++){
var v = verts[list[k]];
if (Math.abs(v[0] - x) <= eps && Math.abs(v[1] - y) <= eps) return list[k];
}
}
var id = verts.length;
verts.push([x, y, []]);
(grid[gx + "," + gy] = grid[gx + "," + gy] || []).push(id);
return id;
}
for (var e = 0; e < segs.length; e++) edges.push([vid(segs[e][0], segs[e][1]), vid(segs[e][2], segs[e][3])]);
// T-junctions: a vertex on the inside of an edge splits it there
var split = [];
for (var g = 0; g < edges.length; g++){
var A = verts[edges[g][0]], B = verts[edges[g][1]], dx = B[0] - A[0], dy = B[1] - A[1], L2 = dx * dx + dy * dy;
var on = [];
if (L2 > eps * eps){
var lo0 = Math.min(A[0], B[0]) - eps, hi0 = Math.max(A[0], B[0]) + eps;
var lo1 = Math.min(A[1], B[1]) - eps, hi1 = Math.max(A[1], B[1]) + eps;
for (var w = 0; w < verts.length; w++){
var V = verts[w];
if (w === edges[g][0] || w === edges[g][1] || V[0] < lo0 || V[0] > hi0 || V[1] < lo1 || V[1] > hi1) continue;
var u = ((V[0] - A[0]) * dx + (V[1] - A[1]) * dy) / L2;
if (u <= 0 || u >= 1) continue;
var px = A[0] + u * dx - V[0], py = A[1] + u * dy - V[1];
if (px * px + py * py <= eps * eps) on.push([u, w]);
}
}
on.sort(function (p, q){ return p[0] - q[0]; });
var prev = edges[g][0];
for (var o = 0; o < on.length; o++){ split.push([prev, on[o][1]]); prev = on[o][1]; }
split.push([prev, edges[g][1]]);
}
var used = [];
for (var s = 0; s < split.length; s++){
var a = split[s][0], b = split[s][1];
if (a === b) continue;
var key = a < b ? a + ":" + b : b + ":" + a;
if (ekey[key]) continue;
ekey[key] = 1;
verts[a][2].push([b, used.length]); verts[b][2].push([a, used.length]);
used.push(0);
}
var out = [];
for (var st = 0; st < verts.length; st++){
var fresh = false;
for (var q = 0; q < verts[st][2].length; q++) if (!used[verts[st][2][q][1]]) fresh = true;
if (!fresh) continue;
out.push([verts[st][0], verts[st][1]]);
var stack = [[st, 0]];
while (stack.length){
var top = stack[stack.length - 1], adj = verts[top[0]][2], found = -1;
while (top[1] < adj.length){
var ed = adj[top[1]++];
if (!used[ed[1]]){ used[ed[1]] = 1; found = ed[0]; break; }
}
if (found >= 0){ out.push([verts[found][0], verts[found][1]]); stack.push([found, 0]); }
else { stack.pop(); if (stack.length){ var bk = verts[stack[stack.length - 1][0]]; out.push([bk[0], bk[1]]); } }
}
}
return out;
}
// Penrose's P2 rhombs by Robinson-triangle deflation, as the page builds
// them: ten acute triangles round a point, alternating hands so neighbours
// meet edge to edge, and at every level each triangle cut at the golden
// section of its edges into Robinson triangles again. Each triangle is half a
// rhomb, so only the two matching sides are drawn and the rhomb outlines
// are what remain. A tiling is one connected figure, and so one path: the
// edges are walked depth first from a vertex, each walked out and back, so
// every edge is stroked (twice, on itself) and the pen never leaves the
// tiling — no connector is ever drawn. Trim Paths then lays the tiling down
// edge by edge. The page's two rhomb tones and its reveal are the stroke's.
var PHI = (1 + Math.sqrt(5)) / 2, R = Math.min(forma_W, forma_H) * 0.52;
var cx = forma_W / 2, cy = forma_H / 2, depth = Math.round(p_depth);
var tris = [];
for (var i = 0; i < 10; i++){
var a0 = (2 * i - 1) * Math.PI / 10, a1 = (2 * i + 1) * Math.PI / 10;
var B = [cx + R * Math.cos(a0), cy + R * Math.sin(a0)], C = [cx + R * Math.cos(a1), cy + R * Math.sin(a1)];
if (i % 2){ var sw = B; B = C; C = sw; }
tris.push([0, [cx, cy], B, C]);
}
function forma_cut(P, Q){ return [P[0] + (Q[0] - P[0]) / PHI, P[1] + (Q[1] - P[1]) / PHI]; }
for (var it = 0; it < depth; it++){
var next = [];
for (var n = 0; n < tris.length; n++){
var tr = tris[n], A = tr[1], Bv = tr[2], Cv = tr[3];
if (tr[0] === 0){
var P = forma_cut(A, Bv);
next.push([0, Cv, P, Bv], [1, P, Cv, A]);
} else {
var Q = forma_cut(Bv, A), Rr = forma_cut(Bv, Cv);
next.push([1, Rr, Cv, A], [1, Q, Rr, Bv], [0, Rr, Q, A]);
}
}
tris = next;
}
// The rhomb edges: each triangle's two equal sides, found by length.
var segs = [];
for (var m = 0; m < tris.length; m++){
var t3 = tris[m], A2 = t3[1], B2 = t3[2], C2 = t3[3];
var ab = Math.sqrt((A2[0] - B2[0]) * (A2[0] - B2[0]) + (A2[1] - B2[1]) * (A2[1] - B2[1]));
var bc = Math.sqrt((B2[0] - C2[0]) * (B2[0] - C2[0]) + (B2[1] - C2[1]) * (B2[1] - C2[1]));
var ca = Math.sqrt((C2[0] - A2[0]) * (C2[0] - A2[0]) + (C2[1] - A2[1]) * (C2[1] - A2[1]));
if (Math.abs(ab - bc) < 1e-6){ segs.push([C2[0], C2[1], A2[0], A2[1]], [A2[0], A2[1], B2[0], B2[1]]); }
else if (Math.abs(bc - ca) < 1e-6){ segs.push([A2[0], A2[1], B2[0], B2[1]], [B2[0], B2[1], C2[0], C2[1]]); }
else { segs.push([B2[0], B2[1], C2[0], C2[1]], [C2[0], C2[1], A2[0], A2[1]]); }
}
var walk = forma_graphPath(segs, 0.05), pts = [];
for (var w = 0; w < walk.length; w++) pts.push(forma_pt(walk[w][0], walk[w][1]));
createPath(pts, [], [], false);