Last week I sent you off with a real bit of homework - put one of your sketches online with a licence and a friendly README - and I promised that once your work was out in the open, we'd dive back into some proper generative maths and make things worth opening the door for. Allez, I keep my promises :-). Today we start on one of my absolute favourite corners of this whole craft, the one that made mathematicians and artists stare at the same walls for literally centuries: how shapes fit together. How you can take a flat, empty plane and carve it up, with no gaps and no overlaps, into something that goes on forever. That's tiling. That's tessellation. And it is so much deeper than the bathroom floor it looks like at first glance.
Here's why I love it as a creative coder. A tiling is infinite pattern from finite rules - which, if you've been with me for a while, is basically the beating heart of everything we do. You define one little shape and one rule for repeating it, and suddenly you've got a pattern that fills any canvas you throw at it, at any resolution, forever. No file gets bigger. The rule is the art. So let me show you what I figured out about making shapes lock together, starting from the simplest possible case and working our way up to patterns that never, ever repeat themselves.
Before any code, one idea that unlocks everything else. When shapes meet at a corner - a vertex - the angles around that point have to add up to exactly 360 degrees. A full turn. If they add to less, there's a gap. If they add to more, they overlap and buckle. That single constraint is the entire law of tiling, and it's why so few shapes tile the plane on their own.
Think about the regular polygons. An equilateral triangle has 60-degree corners: six of them meet perfectly (6 x 60 = 360). A square has 90-degree corners: four meet (4 x 90 = 360). A regular hexagon has 120-degree corners: three meet (3 x 120 = 360). And that's it. Those are the only three regular polygons that tile the plane by themselves, and now you know exactly why - no other regular shape's corner angle divides cleanly into a full turn. A pentagon's 108 degrees? Three make 324 (gap), four make 432 (overlap). It just can't. Makes sense, right? Let me put that law in code so it stops being abstract.
// the law of tiling: do N copies of a regular polygon's corner fill 360 degrees?
function interiorAngle(sides) {
// interior angle of a regular polygon, in degrees
return (sides - 2) * 180 / sides;
}
function tilesThePlane(sides) {
const angle = interiorAngle(sides);
// it tiles alone only if 360 divides evenly by the corner angle
return Number.isInteger(360 / angle);
}
for (let s = 3; s <= 8; s++) {
console.log(s, "sides ->", interiorAngle(s), "deg ->", tilesThePlane(s));
}
// 3 -> 60 -> true 4 -> 90 -> true 6 -> 120 -> true
// 5, 7, 8 -> false. only triangles, squares, hexagons. that's the whole club.
I find it genuinly beautiful that a rule this small explains something you can see on any tiled floor in the world. Three shapes. That's the regular tilings sorted. Now let's actually draw them.
We've drawn grids before - way back in episode 5 loops and grids were basically our first taste of pattern - so this'll feel like home. A square tiling is just nested loops stepping by the tile size. But I want to draw it properly, thinking of each cell as a tile, because that framing carries us all the way to the hard stuff later.
// square tiling: the friendliest tessellation there is.
function squareTiling(ctx, size) {
const cols = Math.ceil(ctx.canvas.width / size);
const rows = Math.ceil(ctx.canvas.height / size);
for (let row = 0; row < rows; row++) {
for (let col = 0; col < cols; col++) {
const x = col * size;
const y = row * size;
// colour it by position so we can SEE the grid, not just imagine it
ctx.fillStyle = (col + row) % 2 ? "#2d3142" : "#4f5d75";
ctx.fillRect(x, y, size, size);
ctx.strokeStyle = "#1a1a22";
ctx.strokeRect(x, y, size, size);
}
}
}
Nothing surprising, and that's the point - the square is the tessellation everyone already understands intuitively. The (col + row) % 2 trick giving us a checkerboard is a small joy though; it's the same parity idea we'll lean on again when we want two tile shapes to alternate.
Triangles are the first place it gets interesting, because you can't just stamp the same triangle in a grid - you have to flip every other one so their edges meet. Point up, point down, point up, point down. Two triangles together make a rhombus, and a row of those alternating triangles fills a strip perfectly.
// one triangle, pointing up or down, at a grid position.
function drawTriangle(ctx, x, y, w, h, pointingUp) {
ctx.beginPath();
if (pointingUp) {
ctx.moveTo(x, y + h); // bottom-left
ctx.lineTo(x + w, y + h); // bottom-right
ctx.lineTo(x + w / 2, y); // top apex
} else {
ctx.moveTo(x, y); // top-left
ctx.lineTo(x + w, y); // top-right
ctx.lineTo(x + w / 2, y + h); // bottom apex
}
ctx.closePath();
ctx.fill();
ctx.stroke();
}
Now the tiling. The trick is that within one row the triangles overlap horizontally by half a width - each new triangle starts half a step back - and every other one flips. Watch how the parity of col decides up-versus-down.
// triangular tiling: alternate up/down, and step by HALF a width each time.
function triangleTiling(ctx, w, h) {
const palette = ["#ef476f", "#ffd166"];
const cols = Math.ceil(ctx.canvas.width / (w / 2)) + 1;
const rows = Math.ceil(ctx.canvas.height / h);
for (let row = 0; row < rows; row++) {
for (let col = 0; col < cols; col++) {
const up = col % 2 === 0; // alternate orientation
const x = (col * w) / 2 - w / 2; // half-width horizontal step
ctx.fillStyle = palette[(col + row) % 2];
drawTriangle(ctx, x, row * h, w, h, up);
}
}
}
See where this is going? The moment shapes stop being squares, tiling becomes a little dance of offsets and flips. Get the offset right and the edges kiss with no gap. Get it wrong and you get slivers of background showing through - which, honestly, is how you debug a tiling: those slivers are the plane telling you your angles or offsets don't add to a full turn.
Hexagons are my favourite, and nature's too - honeycomb, basalt columns, the compound eye of a fly. They're efficient: a hexagon packs the most area for the least perimeter of any shape that tiles. But they're also the fiddliest to lay out, because the rows interlock. Every other column is nudged down by half a hex height. Let me build one hexagon first.
// a flat-topped hexagon centred at (cx, cy) with a given radius.
function drawHexagon(ctx, cx, cy, radius) {
ctx.beginPath();
for (let i = 0; i < 6; i++) {
// six points, 60 degrees apart. the -30 offset gives flat top+bottom.
const angle = (Math.PI / 180) * (60 * i - 30);
const px = cx + radius * Math.cos(angle);
const py = cy + radius * Math.sin(angle);
if (i === 0) ctx.moveTo(px, py);
else ctx.lineTo(px, py);
}
ctx.closePath();
ctx.fill();
ctx.stroke();
}
That's just our old friend trigonometry from episode 13 - six points on a circle, sixty degrees apart. The magic is the spacing when we tile them. The horizontal step between hex centres is radius * sqrt(3), and every other column drops by half that. Those aren't numbers I want you to memorise, they're numbers you derive from the geometry, but here's the working version so you can see them do their job.
// hexagonal tiling: interlocking columns, every other one dropped half a step.
function hexTiling(ctx, radius) {
const h = Math.sqrt(3) * radius; // full height of a flat-top hex
const horiz = 1.5 * radius; // horizontal distance between centres
const cols = Math.ceil(ctx.canvas.width / horiz) + 1;
const rows = Math.ceil(ctx.canvas.height / h) + 1;
for (let col = 0; col < cols; col++) {
for (let row = 0; row < rows; row++) {
const cx = col * horiz;
// odd columns get pushed down by half a hex - that's the interlock
const cy = row * h + (col % 2 ? h / 2 : 0);
ctx.fillStyle = `hsl(${(col * 20 + row * 8) % 360}, 55%, 55%)`;
drawHexagon(ctx, cx, cy, radius);
}
}
}
Run that and you get a honeycomb that fills the whole canvas, coloured by a rolling hue so you can watch the columns interlock. The col % 2 ? h/2 : 0 is doing the exact same job the triangle flip did - it's the parity offset that makes neighbouring rows lock instead of collide. Once you see that pattern, you see it everywhere in tiling code.
The three regular tilings use one shape each. But you can also mix shapes, as long as every vertex still obeys the 360 law, and every vertex looks the same. Those are the semi-regular (or Archimedean) tilings, and there are exactly eight of them. A classic one puts octagons and squares together - the pattern you've seen on a thousand kitchen floors. An octagon corner is 135 degrees, so two octagons (270) plus one square (90) makes 360. It fits!
// check a vertex is valid: do these polygons' corners sum to exactly 360?
function validVertex(polygonSides) {
const total = polygonSides.reduce((sum, sides) => sum + interiorAngle(sides), 0);
return Math.abs(total - 360) < 0.001;
}
console.log(validVertex([8, 8, 4])); // octagon+octagon+square -> true
console.log(validVertex([3, 3, 3, 3, 6])); // four triangles + hexagon -> true
console.log(validVertex([5, 5, 5])); // three pentagons -> 324, false (the famous gap)
I love that validVertex function because it turns "which shapes tile together" from a mystery into a two-line experiment. You can sit and hunt for valid combinations, and every time the sum hits 360 you've found a pattern real tilemakers have used for a thousand years. That's the kind of thing that keeps me up too late :-).
Now we get to the part that feels like real magic - the thing that made M.C. Escher's interlocking lizards and birds and fish possible. The secret is almost silly once you know it. Start with a shape that tiles (a square is easiest). Then deform one edge however you like - add a bump, a curve, a spike. The only rule: whatever you add to one edge, you must subtract from the opposite edge. Push out on the left by exactly the amount you push in on the right, and the deformed shape still tiles, because the bump on one tile fits exactly into the dent on its neighbour.
// take a square edge and its opposite, and deform them as a matched pair.
// whatever we push OUT on the right, we push IN on the left by the same amount.
function makeEscherTile(bump) {
// define one tile's outline as a list of points, starting from a plain square
// then perturb the right edge outward and the left edge inward identically.
return {
top: [0, 0, 1, 0],
right: (t) => [1 + bump * Math.sin(t * Math.PI), t], // bulge out
bottom: [1, 1, 0, 1],
left: (t) => [0 + bump * Math.sin(t * Math.PI), 1 - t] // matching dent
};
}
The maths of "add here, subtract there" is really just a promise that the tile's area and its fit survive the deformation. Escher spent years doing this by hand with tracing paper. We get to do it with a function and a loop, and honestly that still feels like cheating in the best way. Here's the idea rendered as an actual repeating outline - deform the edge, then stamp the deformed tile across a grid exactly like we stamped squares.
// stamp a deformed tile across the plane. the bumps and dents interlock.
function escherTiling(ctx, size, bump) {
const cols = Math.ceil(ctx.canvas.width / size) + 1;
const rows = Math.ceil(ctx.canvas.height / size) + 1;
for (let row = 0; row < rows; row++) {
for (let col = 0; col < cols; col++) {
ctx.save();
ctx.translate(col * size, row * size);
ctx.beginPath();
ctx.moveTo(0, 0);
// right edge bulges out with a sine bump...
for (let t = 0; t <= 1; t += 0.05) {
ctx.lineTo(size + bump * Math.sin(t * Math.PI) * size, t * size);
}
// ...and the next tile's left edge will have the exact matching dent
for (let t = 0; t <= 1; t += 0.05) {
ctx.lineTo(size - (1 - t) * size + bump * Math.sin((1 - t) * Math.PI) * size, size);
}
ctx.closePath();
ctx.fillStyle = (col + row) % 2 ? "#06d6a0" : "#118ab2";
ctx.fill();
ctx.restore();
}
}
}
Don't sweat every line of that one - the point is the idea: a deformed edge and its matching opposite. That's the whole Escher toolkit. Bend a square into a bird by pushing and pulling matched pairs of edges, colour alternating tiles differently, and you've got interlocking creatures marching across the plane. Try it. Change bump. Watch the tiles stay locked no matter how weird the edge gets, because the promise always holds.
Here's a completely different way to think about tiling that programmers adore. Forget shapes for a second. Imagine square tiles where each of the four edges has a colour, and the only rule is: two tiles can sit next to each other only if their touching edges share a colour. That's a Wang tile. Suddenly tiling becomes a puzzle - a constraint-satisfaction problem - and you can generate huge patterns by just picking, at each cell, a tile whose left and top edges match the neighbours already placed.
// a Wang tile is four edge colours: [top, right, bottom, left].
// two tiles fit horizontally if left tile's RIGHT == right tile's LEFT.
function fitsRight(leftTile, rightTile) {
return leftTile[1] === rightTile[3];
}
function fitsBelow(topTile, bottomTile) {
return topTile[2] === bottomTile[0];
}
// a small set of tiles using 2 edge colours (0 and 1)
const tileSet = [
[0, 0, 0, 0], [0, 1, 1, 0], [1, 0, 0, 1], [1, 1, 1, 1],
];
To fill a grid, we walk left-to-right, top-to-bottom, and at each cell we keep only the tiles whose left edge matches the tile already to our left and whose top edge matches the tile already above. Then we pick one of the survivors. It's a tiny taste of the constraint solving that powers modern procedural generation.
// fill a grid with Wang tiles, matching edges as we go.
function wangFill(cols, rows, tiles) {
const grid = [];
for (let r = 0; r < rows; r++) {
grid[r] = [];
for (let c = 0; c < cols; c++) {
const left = c > 0 ? grid[r][c - 1] : null;
const above = r > 0 ? grid[r - 1][c] : null;
const options = tiles.filter((t) =>
(!left || fitsRight(left, t)) && (!above || fitsBelow(above, t))
);
// pick a valid tile (or fall back to the first if we painted ourselves in a corner)
grid[r][c] = options.length ? options[(c + r) % options.length] : tiles[0];
}
}
return grid;
}
Why does this matter beyond a neat puzzle? Because with a clever enough set of Wang tiles you can force a pattern that never repeats - it fills the infinite plane and yet you'll never find two identical regions lining up. That's the doorway to the strangest, loveliest tilings of all.
For the longest time, mathematicians assumed any tiling that fills the plane must eventually repeat - shift it far enough and it lines up with itself, like our square grid does. Then Roger Penrose found tilings that never do. You can cover the whole infinite plane with just two shapes - people call them "kites" and "darts", or use two rhombi - following simple matching rules, and the result has a deep five-fold symmetry but never a repeating unit. Ever. It's called aperiodic, and it turns out real matter does this too - quasicrystals, a discovery that won a Nobel Prize.
I won't drop a full Penrose generator on you today - it uses a "subdivision" process that deserves its own slow walk - but I want you to feel the core move, because it's gorgeous. You take a shape, split it into smaller copies of the two base shapes by a fixed rule, and repeat. Each round the tiles get smaller and more numerous, and the pattern that emerges never settles into a repeat. The golden ratio, of all things, sits right at the heart of it.
// the SEED of a Penrose subdivision: the golden ratio drives everything.
const PHI = (1 + Math.sqrt(5)) / 2; // ~1.618, the golden ratio
// each "fat" rhombus subdivides into smaller fat + thin rhombi,
// scaled down by PHI each generation. this recursion never repeats.
function subdivideCount(generations) {
let fat = 1, thin = 0;
for (let g = 0; g < generations; g++) {
// the population grows by the golden ratio - a real fingerprint of aperiodicity
[fat, thin] = [fat + thin, fat];
}
return { fat, thin, ratio: fat / thin };
}
console.log(subdivideCount(10).ratio); // creeps toward PHI, 1.618..., forever
Watch that ratio crawl toward the golden ratio as the generations climb - that's the same Fibonacci growth we've bumped into before, showing up here as the fingerprint of a pattern that refuses to repeat. Order and unpredictability, holding hands. I genuinly think it's one of the most beautiful facts in all of mathematics, and you can render it in a browser. We'll come back and actually draw one properly, because it earns the whole spotlight.
Step back and look at what we've climbed today. We started with the flat rule of 360 degrees, saw why only three regular shapes tile alone, drew squares and triangles and honeycombs, mixed shapes into semi-regular patterns, bent edges into Escher creatures, turned tiling into an edge-matching puzzle with Wang tiles, and finally touched the strange aperiodic world of Penrose where a pattern can fill infinity and never repeat. All of it from the same tiny seed: a shape and a rule for fitting copies together.
And here's the thread I want you holding as we go. Every one of these tilings had a hidden symmetry running through it - the way the square grid slides onto itself, the way the hexagons have a six-fold turn, the way Penrose has its five-fold spin without any repeat. That word, symmetry, has been lurking under everything we did today, and it turns out mathematicians have completely mapped out every possible way a flat pattern can be symmetric - and there are surprisingly few of them. That's a genuinely mind-blowing thing to learn, and it's exactly where we're headed next. So this week, your homework: take the hexagon or Escher tiler above, get it running, and then play - change the bump, change the colours by position, mash two shapes together and see what locks and what leaves a sliver. Feel for yourself where a tiling holds and where it breaks, because understanding that in your fingers is the best possible warm-up for the deep symmetry we're about to unpack. 't Was plezant to build this with you - go make the plane your own :-).
col % 2 ? half : 0) that flips triangles and drops every other hex column is the same idea doing the same jobradius*sqrt(3)validVertex check lets you hunt for valid combinations yourself (octagon+octagon+square = 360, it fits)So that's tiling, from the humble bathroom floor all the way up to Nobel-Prize-winning quasicrystals, and every step of it was just shapes agreeing to fit. The big lovely takeaway, one more time: infinite pattern from finite rules. Define the tile, define the fit, and the plane fills itself forever. Go get the hexagon tiler running and bend a few edges until something surprises you - that surprise is the whole reason I do this. And keep that word symmetry in your back pocket, because next time we pull it apart completely and find out just how few ways a flat pattern can actually be. Merci voor het lezen, en tot de volgende keer :-).
Sallukes! Thanks for reading.
X