PL. 136 · LATTICES / TILING / SUBSTITUTION
Pinwheel Tiling
John H. Conway · proved by Charles Radin, 1994
OPEN THE LIVE PLATE ▸DEFINITION
A=(0,0) B=(2,0) C=(0,1) — legs 2 and 1, hypotenuse √5 D=(1,0) P=(1/5,2/5) X=(2/5,4/5) Q=(6/5,2/5) σ(ABC) = XAC ∪ PDA ∪ QBD ∪ PDX ∪ QXD — five copies of ABC/√5 (first letter is the right angle) turn per level = arctan(1/2) = 26.5651…°, irrational in degrees, so σⁿ never repeats a direction
NOTES
Penrose and Ammann–Beenker, the two aperiodic tilings already in this order, set every tile at a multiple of one fixed angle — 36 degrees for the first, 45 for the second — so each has a short list of orientations and every tile it will ever contain is on that list. This one has no such list: one shape appears in more and more directions the longer the substitution runs, and nothing else in the atlas repeats a single shape that way. Conway noticed that the right triangle with legs 1 and 2 and hypotenuse √5 cuts into five copies of itself at a fifth of the area, and that the copies come out turned by arctan(1/2), which is 26.5651 degrees and not a rational part of a turn. Substitute again and the turns compound; substitute forever and the tiles stand in infinitely many directions, dense in the circle. Radin published the construction in 1994 with the proof that the tilings it generates are non-periodic and that no finite list of orientations describes them — the first tilings ever shown to have that property. The plate paints each triangle by its own bearing, so one colour is one orientation exactly, and the number of colours on the plate is the number of orientations the substitution has reached: 4 after one level, then 8, 12, 16, 20 — four more every level, and nothing stops it. One bearing at a time is lit, which is what shows how far apart tiles of a single direction sit. The heavier web is the supertile boundary a chosen number of levels up — the same triangle again, larger by √5 per level and turned by the same angle each time, which is the reason the count never stops climbing.
PROVENANCE
- Origin
- The five-piece subdivision is due to John H. Conway; the tilings, their non-periodicity and the infinitude of orientations are C. Radin, "The pinwheel tilings of the plane", Annals of Mathematics 139(3), 1994, 661–702, which credits Conway for the construction. Radin wrote up the decade of work with Conway afterwards in "Conway and aperiodic tilings", arXiv:2008.09085 (2020)
- Standing
- Public domain — a subdivision rule and a theorem about it, neither patentable. Penrose patented his own tiles as a puzzle (US4133152, long expired); nothing of the kind was ever filed here
- Derived, then checked
- The subdivision on this plate was not copied from a published figure. It was forced out of the boundary: the short leg admits exactly one covering by child edges, the long leg exactly one, the hypotenuse only legs, and the right-angled corner has to be filled by exactly two children — and everything surviving those constraints was then enumerated in exact rational arithmetic. Exactly two decompositions of the triangle into five copies at 1/√5 exist. The plate draws the one whose orientation set grows without bound; the other one mirrors all five children, so its turn cancels every second level and it never exceeds five orientations at any depth however far it is iterated. It tiles the triangle perfectly and it is not the pinwheel, which is the whole reason a subdivision that looks right is not evidence that it is right
- Exact, not approximate
- Checked in exact BigInt rationals with no tolerance anywhere in the argument: every child has 25·|long leg|² = 20, 25·|short leg|² = 5, 25·|hypotenuse|² = 25 and legs meeting at a zero dot product, against 4 : 1 : 5 for the parent; the five absolute areas sum to exactly the area of the parent; every child vertex sits inside the parent by exact half-plane tests; and all 10 pairs, then all 300 pairs one level down, clip to an intersection area of exactly zero by exact rational polygon clipping. Areas that sum, with nothing overlapping and nothing outside, is a tiling rather than a picture of one. In doubles, at the depths the plate actually draws and two beyond them, the worst side-length error over 78,125 tiles is 4.5e-16
- Both hands
- Three of the five children are mirror images of their parent and two are direct, so handedness is not preserved and the tiling carries both hands of the triangle — measured at 50.0 per cent direct from the second level onward. That mixture is exactly what makes the turn accumulate instead of cancelling, and it is the one structural difference between this decomposition and the other one
- Why the angle never comes back
- Doubling arctan(1/2) gives the angle of the 3-4-5 triangle, so the turn across two levels is the argument of (3+4i)/5, a unit complex number that is not a root of unity — its powers never return to 1. Checked numerically to 400,000 powers, where the closest approach to 1 is 8.0e-6 and still falling
- The seed
- The plate starts from the 1×2 rectangle cut along its diagonal into two triangles. That pair is not an arbitrary frame: the substitution puts exactly that pair, at 1/√5 scale, into the middle of every triangle it divides — children four and five share their hypotenuse and their union is a rectangle — so the plate starts inside the tiling rather than beside it, and the window can be panned anywhere in the rectangle without ever running off tiled ground
- One colour, one direction
- Bearings are read off the finished tiling and collected exactly rather than banded, so the plate paints as many colours as the tiling has orientations, and the pass count is one per orientation instead of one per tile. The colour runs across the ramp from 0.8 at due east to 1.2 at a full turn, a whole circle inside the bright lobe and never through the trough at 0.5 where the ramp is black. Any map from a circle onto a segment has a seam; this one is at due east, where the substitution leaves a gap at every depth — 36.87 degrees at three levels, 16.26 at four, 20.61 at five — so the two ends of the band never land on two tiles that nearly agree in direction
- Constants
- depth is locked: each level multiplies the tile count by five, measured in a browser at card size and at the widest point of the drift at 0.03, 0.14 and 1.51 ms a paint for 3, 4 and 5. A sixth level was measured too, at 26.6 ms at card size and 41.6 in the drawer, and is not in the range — it is past what the drawer should offer, let alone the grid. zoom and travel move a window over a tiling that is the same everywhere, so no position and no magnification can be dead. super chooses which substitution level the supertile web is drawn at; all three values are live, at 250, 50 and 10 supertiles at the default depth
- Source
- doi:10.2307/2118575
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. 136 · PINWHEEL TILING — John H. Conway · proved by Charles Radin, 1994
// A=(0,0) B=(2,0) C=(0,1) — legs 2 and 1, hypotenuse √5
// D=(1,0) P=(1/5,2/5) X=(2/5,4/5) Q=(6/5,2/5)
// σ(ABC) = XAC ∪ PDA ∪ QBD ∪ PDX ∪ QXD — five copies of ABC/√5
// (first letter is the right angle) turn per level = arctan(1/2)
// = 26.5651…°, irrational in degrees, so σⁿ never repeats a direction
// 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=pinwheel
float p_depth = 4 + chf('depth_tweak'); // substitution levels · live 3 .. 5
float p_super = 2 + chf('super_tweak'); // supertile level · live 1 .. 3
// 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)));
}
// waived: zoom, travel — both aim a canvas viewport at a fixed tiling; the port emits the whole tiling as geometry, so there is nothing left to look through
//
// Conway's cut: the right triangle with legs 1 and 2 and hypotenuse sqrt(5)
// divides into five copies of itself at 1/sqrt(5), and three of the five come
// out turned by arctan(1/2) = 26.5651 degrees, which is not a rational part of
// a turn. Substitute repeatedly and the turns compound, so the tiles stand in
// ever more distinct directions — 6, 12, 20, 28, 36 of them at one level to
// five, from the two-triangle seed this port uses. That is the whole exhibit,
// and it is why each tile is coloured by the direction of its own long leg.
//
// VEX has no recursion, so the substitution is unrolled onto flat arrays as a
// worklist, exactly the way the plate unrolls it onto one array. A tile is
// carried as three points — R the right angle, L the far end of the long leg,
// S the far end of the short leg — because a child needs its parent's frame,
// and both handednesses then fall out for free: a mirrored parent simply hands
// over a left-handed basis. Nothing here is random; two cooks give identical
// geometry.
//
// Canvas y runs down and Houdini's runs up, so every y is negated on the way
// out. The plate tints each tile at alpha 0.08 and outlines it at 0.25; the
// prims carry the outline alpha, since the outline is what the plate reads by.
// The one thing left behind is the pulse that lights a single bearing at a
// time — that is animation, not geometry, the same call penrose's port makes
// about its own reveal.
int depth = int(rint(p_depth));
int sup = min(int(rint(p_super)), depth);
float TAU = 6.28318530718;
// The five children in the parent's own frame: right angle at (0,0), long leg
// to (2,0), short leg to (0,1). These twenty-four fifths are forced rather
// than chosen — the parent's short leg admits only one child hypotenuse, the
// long leg only two (so it is cut at its midpoint), the parent's hypotenuse
// only legs, and the right angle only a 63.4349 and a 26.5651 corner. Exactly
// two arrangements survive all of that; the other one mirrors all five
// children, so its turn cancels every second level and it never shows more
// than five orientations. This is the one whose orientations never close up.
// array(), not a brace literal — VEX braces take constants only.
float cRx[] = array(0.4, 0.2, 1.2, 0.2, 1.2);
float cRy[] = array(0.8, 0.4, 0.4, 0.4, 0.4);
float cLx[] = array(0.0, 1.0, 2.0, 1.0, 0.4);
float cLy[] = array(0.0, 0.0, 0.0, 0.0, 0.8);
float cSx[] = array(0.0, 0.0, 1.0, 0.4, 1.0);
float cSy[] = array(1.0, 0.0, 0.0, 0.8, 0.0);
// The seed: the 1 x 2 rectangle cut along its diagonal into two triangles.
// Not an arbitrary frame — the substitution puts exactly that pair, at
// 1/sqrt(5) scale, into the middle of every triangle it divides.
float rx[] = array(0.0, 2.0), ry[] = array(0.0, 1.0);
float lx[] = array(2.0, 0.0), ly[] = array(0.0, 1.0);
float sx[] = array(0.0, 2.0), sy[] = array(1.0, 0.0);
// the supertile level, kept aside as the worklist passes through it
float kRx[], kRy[], kLx[], kLy[], kSx[], kSy[];
if (sup >= depth){
kRx = rx; kRy = ry; kLx = lx; kLy = ly; kSx = sx; kSy = sy;
}
for (int lev = 0; lev < depth; lev++){
float nrx[], nry[], nlx[], nly[], nsx[], nsy[];
for (int i = 0; i < len(rx); i++){
// half the long leg and the whole short leg are the parent's basis:
// a child vertex written (u, v) above lands at R + u*(L-R)/2 + v*(S-R)
float e1x = (lx[i] - rx[i]) * 0.5, e1y = (ly[i] - ry[i]) * 0.5;
float e2x = sx[i] - rx[i], e2y = sy[i] - ry[i];
for (int c = 0; c < 5; c++){
push(nrx, rx[i] + cRx[c] * e1x + cRy[c] * e2x);
push(nry, ry[i] + cRx[c] * e1y + cRy[c] * e2y);
push(nlx, rx[i] + cLx[c] * e1x + cLy[c] * e2x);
push(nly, ry[i] + cLx[c] * e1y + cLy[c] * e2y);
push(nsx, rx[i] + cSx[c] * e1x + cSy[c] * e2x);
push(nsy, ry[i] + cSx[c] * e1y + cSy[c] * e2y);
}
}
rx = nrx; ry = nry; lx = nlx; ly = nly; sx = nsx; sy = nsy;
if (lev + 1 == depth - sup){
kRx = rx; kRy = ry; kLx = lx; kLy = ly; kSx = sx; kSy = sy;
}
}
// The tiles. Colour is the bearing of the long leg, laid across the ramp from
// 0.8 at due east through 1.2 at a full turn — a whole circle of directions
// inside the bright lobe, never crossing the trough at 0.5 where the ramp goes
// black. The seam sits at due east because the substitution never produces a
// bearing in the 16.26 degrees below a full turn, the widest hole in the table.
for (int i = 0; i < len(rx); i++){
float a = atan2(ly[i] - ry[i], lx[i] - rx[i]);
if (a < 0.0) a += TAU;
// a long leg that should be exactly horizontal arrives a hair under a full
// turn, and without this it would be painted as if it pointed the other way
if (a > TAU - 1e-5) a = 0.0;
vector col = forma_ramp(0.8 + 0.4 * (a / TAU));
int p0 = addpoint(0, set(rx[i], -ry[i], 0.0));
int p1 = addpoint(0, set(lx[i], -ly[i], 0.0));
int p2 = addpoint(0, set(sx[i], -sy[i], 0.0));
setpointattrib(0, "Cd", p0, col);
setpointattrib(0, "Cd", p1, col);
setpointattrib(0, "Cd", p2, col);
setpointattrib(0, "Alpha", p0, 0.25);
setpointattrib(0, "Alpha", p1, 0.25);
setpointattrib(0, "Alpha", p2, 0.25);
// the variadic form closes the polygon
addprim(0, "poly", p0, p1, p2);
}
// The supertile web: the same tiling sup levels up, which is the same triangle
// again at sqrt(5)^sup the size and turned by the same angle — the reason the
// orientation count never stops climbing. Drawn as closed polylines in white,
// which is the plate's HILITE exactly, at the weight the plate gives it.
for (int i = 0; i < len(kRx); i++){
int q0 = addpoint(0, set(kRx[i], -kRy[i], 0.0));
int q1 = addpoint(0, set(kLx[i], -kLy[i], 0.0));
int q2 = addpoint(0, set(kSx[i], -kSy[i], 0.0));
int prim = addprim(0, "polyline");
int ring[] = array(q0, q1, q2, q0);
for (int v = 0; v < 4; v++){
setpointattrib(0, "Cd", ring[v], set(1.0, 1.0, 1.0));
setpointattrib(0, "Alpha", ring[v], 0.15);
addvertex(0, prim, ring[v]);
}
}
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. 136 · PINWHEEL TILING — John H. Conway · proved by Charles Radin, 1994
// A=(0,0) B=(2,0) C=(0,1) — legs 2 and 1, hypotenuse √5
// D=(1,0) P=(1/5,2/5) X=(2/5,4/5) Q=(6/5,2/5)
// σ(ABC) = XAC ∪ PDA ∪ QBD ∪ PDX ∪ QXD — five copies of ABC/√5
// (first letter is the right angle) turn per level = arctan(1/2)
// = 26.5651…°, irrational in degrees, so σⁿ never repeats a direction
// 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.
// This plate draws 2 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=pinwheel
// 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 = 4 + forma_tweak("depth_tweak"); // substitution levels · live 3 .. 5
var p_zoom = 2.2 + forma_tweak("zoom_tweak"); // closest zoom · live 1.4 .. 3.4
var p_travel = 0.85 + forma_tweak("travel_tweak"); // pan travel · live 0 .. 1
var p_super = 2 + forma_tweak("super_tweak"); // supertile level · live 1 .. 3
// 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.5632173358462751; // 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();
// 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;
}
// The Conway–Radin pinwheel: a 1 : 2 : √5 right triangle cut into five copies
// of itself at 1/√5, by the one arrangement of the two that survive whose
// orientations never close up — the twenty-four fifths of CH, the page's
// own table, read in the parent's basis R + u·(L−R)/2 + v·(S−R). Two
// triangles tile a 1 × 2 rectangle, subdivided `depth` times; the window
// breathes between the whole rectangle and the slider's zoom and drifts
// across it at `travel`, from this plate's phase, as on the page. Part 0 is
// the tiling inside the window as one path — the edges walked depth first,
// each out and back, so the pen never leaves the fabric; part 1 is the
// coarse tiling `super` levels up, the page's heavy underlay. What the
// stroke cannot carry is the page's exhibit: each tile coloured by its
// bearing, and the pulse that lights one orientation at a time.
// parts: 2
var TAU = 6.283185307179586, depth = Math.round(p_depth);
var sup = Math.min(Math.round(p_super), depth);
var CH = [0.4, 0.8, 0.0, 0.0, 0.0, 1.0,
0.2, 0.4, 1.0, 0.0, 0.0, 0.0,
1.2, 0.4, 2.0, 0.0, 1.0, 0.0,
0.2, 0.4, 1.0, 0.0, 0.4, 0.8,
1.2, 0.4, 0.4, 0.8, 1.0, 0.0];
// a tile is [rx, ry, lx, ly, sx, sy]: right-angle vertex, long-leg end, short-leg end
var tiles = [[0, 0, 2, 0, 0, 1], [2, 1, 0, 1, 2, 0]], coarse = null;
if (sup >= depth) coarse = tiles;
for (var lvl = 1; lvl <= depth; lvl++){
var next = [];
for (var n = 0; n < tiles.length; n++){
var T = tiles[n], rx = T[0], ry = T[1];
var e1x = (T[2] - rx) / 2, e1y = (T[3] - ry) / 2, e2x = T[4] - rx, e2y = T[5] - ry;
for (var c = 0; c < 5; c++){
var q = c * 6;
next.push([rx + CH[q] * e1x + CH[q + 1] * e2x, ry + CH[q] * e1y + CH[q + 1] * e2y,
rx + CH[q + 2] * e1x + CH[q + 3] * e2x, ry + CH[q + 2] * e1y + CH[q + 3] * e2y,
rx + CH[q + 4] * e1x + CH[q + 5] * e2x, ry + CH[q + 4] * e1y + CH[q + 5] * e2y]);
}
}
tiles = next;
if (lvl === depth - sup) coarse = tiles;
}
var zoom = 1 + (p_zoom - 1) * 0.5 * (1 - Math.cos(TAU * (((forma_t / 71) + forma_phase) % 1)));
var winH = 1 / zoom, winW = winH * forma_W / forma_H;
if (winW > 2){ winW = 2; winH = 2 * forma_H / forma_W; }
var ux = 0.5 + 0.5 * p_travel * Math.sin(TAU * (((forma_t / 53) + forma_phase) % 1));
var uy = 0.5 + 0.5 * p_travel * Math.sin(TAU * (((forma_t / 37) + forma_phase) % 1));
var wcx = winW / 2 + (2 - winW) * ux, wcy = winH / 2 + (1 - winH) * uy;
var kk = forma_W / winW, ox = forma_W / 2 - wcx * kk, oy = forma_H / 2 + wcy * kk; // canvas y runs down
var list = forma_part === 1 ? coarse : tiles, segs = [];
for (var i = 0; i < list.length; i++){
var t2 = list[i];
var x0 = ox + t2[0] * kk, x1 = ox + t2[2] * kk, x2 = ox + t2[4] * kk;
if ((x0 < -2 && x1 < -2 && x2 < -2) || (x0 > forma_W + 2 && x1 > forma_W + 2 && x2 > forma_W + 2)) continue;
var y0 = oy - t2[1] * kk, y1 = oy - t2[3] * kk, y2 = oy - t2[5] * kk;
if ((y0 < -2 && y1 < -2 && y2 < -2) || (y0 > forma_H + 2 && y1 > forma_H + 2 && y2 > forma_H + 2)) continue;
segs.push([x0, y0, x1, y1], [x1, y1, x2, y2], [x2, y2, x0, y0]);
}
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);