Last time we grew a whole imaginary continent - coastlines, biomes, rivers finding their way downhill, forests growing where forests belong - and at the very end I dropped a little hook. Everything on that map was grown: land shaped by noise, water shaped by gravity, trees shaped by climate. But people don't only live in wildernesses. They build. And building is a completely diffrent kind of generation, because a building isn't smooth and organic and field-like - it's discrete, it's gridded, it has right angles and repeated parts and rules you can't break or the thing falls down. Allez, today we point our generative thinking at things that are designed instead of grown.
This is the episode I'd call "generative architecture", and it's genuinly one of the most satisfying corners of this whole craft, because you get to feel like an architect and a coder at the same time. We're going to build facades out of grids, lay out floor plans by splitting rooms recursively, treat a whole building as a little grammar (yes, the same grammar idea from episode 149), and finish by generating a whole street of buildings where no two are the same but they all clearly belong to the same town. It's all vanilla JavaScript and the Canvas 2D API, no library, no engine. Let me show you what I figured out :-).
Here's the mental shift, and it's a big one, so let me spell it out. In the noise episodes the world was a continuous field - fbm(x, y) answered for any coordinate, and neighbouring points had neighbouring values, so everything was smooth. Architecture is the opposite. A window is either there or it isn't. A floor is at 3 metres or 6 metres, never 4.37. A wall meets another wall at exactly ninety degrees. Buildings are made of discrete parts on a grid, arranged by rules with intention.
So the tools change. Instead of sampling a smooth field, we're going to snap things to grids, subdivide rectangles, and rewrite symbols with rules. But one old friend comes along for the ride: the deterministic hash. Just like in the world episodes, we want variation that's reproducible - the same building index always gives the same building - so we drive every "random" choice from a hash, never from Math.random. Here's the hash we've leaned on all arc:
// same deterministic hash we used all through the noise arc.
// same inputs -> same output, always. this is how we get REPRODUCIBLE variety.
function hash2(x, y) {
let h = x * 374761393 + y * 668265263;
h = (h ^ (h >> 13)) * 1274126177;
h = h ^ (h >> 16);
return (h >>> 0) / 4294967295; // a stable float in [0, 1)
}
Hold that thought. Every choice today - how many floors, which bays get windows, where a wall splits - will come out of this function, so the whole town regenerates identically every single time. That reproducibility is what separates a real generative system from a pile of Math.random calls.
Let's start with the front of a building, because it's the clearest example of "gridded design". Look at almost any building and you'll see it: floors stacked vertically, and bays (vertical strips) running across. Windows sit at the intersections. So a facade is really just a 2D grid of cells, and the first thing to do is build that grid:
// a facade is a grid: `floors` rows stacked up, `bays` columns across.
// each cell is a rectangle in the facade's own local coordinates.
function makeFacade(floors, bays, floorH, bayW) {
const cells = [];
for (let f = 0; f < floors; f++) {
for (let b = 0; b < bays; b++) {
cells.push({
f, b, // which floor / which bay
x: b * bayW, // local position on the wall
y: f * floorH,
w: bayW, h: floorH,
});
}
}
return cells;
}
Nothing clever yet - it's the same nested loop we've used a hundred times for grids. But notice what each cell knows: which floor and bay it is, plus its rectangle. That f and b are the important bit, because architecture treats floors differently. The ground floor has doors and shopfronts, the top floor might have an attic or a cornice, and the middle floors are just windows repeating. So the rules read the floor number.
This is where it stops being a boring grid and starts being a building. We write a function that looks at a cell's position and decides what belongs there. Ground floor gets a door in one bay and shopfronts in the rest; the top floor gets smaller attic windows; everything in between is a normal window:
// the RULE: given a cell's floor/bay, decide what architectural element goes there.
// this is the whole "design intention" of the building, in one function.
function cellType(cell, floors, bays) {
if (cell.f === 0) {
// ground floor: a door roughly in the middle, shopfronts either side
const doorBay = Math.floor(bays / 2);
return cell.b === doorBay ? "door" : "shopfront";
}
if (cell.f === floors - 1) return "attic"; // top floor: little windows
return "window"; // everything else: a normal window
}
Read that and you can basically see the building already. The door lives in the middle bay of the ground floor, shops flank it, the top floor is attics, and the whole middle is a repeating field of windows. That single function is the architecture. Change the rules and you get a completely diffrent style of building, without touching a single line of drawing code. That seperation - rules in one place, drawing in another - is the thing that makes generative design scale.
Now let's actually see it. We paint the wall, then loop over the cells and draw the right element into each one. Each element is just a few rectangles - a window is a frame with a lighter pane inside, a door is a tall dark rectangle, and so on. I'll keep the drawing deliberately simple so the structure is what shows through:
// draw one architectural element inside its cell rectangle.
function drawElement(ctx, cell, type) {
const pad = cell.w * 0.22; // margin between element and cell edge
const ix = cell.x + pad, iy = cell.y + pad;
const iw = cell.w - pad * 2, ih = cell.h - pad * 2;
if (type === "window" || type === "attic") {
const h = type === "attic" ? ih * 0.6 : ih; // attics are shorter
ctx.fillStyle = "#3b4a5a"; // dark frame
ctx.fillRect(ix, iy, iw, h);
ctx.fillStyle = "#9fc0d9"; // glass
ctx.fillRect(ix + 2, iy + 2, iw - 4, h - 4);
} else if (type === "door") {
ctx.fillStyle = "#5a3b2a";
ctx.fillRect(ix, cell.y + cell.h * 0.2, iw, cell.h * 0.8);
} else if (type === "shopfront") {
ctx.fillStyle = "#c9b58a";
ctx.fillRect(ix, iy, iw, ih);
}
}
And the painter that ties the grid and the rules together - build the wall, walk the cells, ask the rule what goes where, draw it:
// paint a whole building facade at a given origin on the canvas.
function drawBuilding(ctx, ox, oy, floors, bays, floorH = 44, bayW = 40) {
const wallW = bays * bayW, wallH = floors * floorH;
ctx.save();
ctx.translate(ox, oy - wallH); // origin is the building's ground line
ctx.fillStyle = "#d8cdba"; // the wall itself
ctx.fillRect(0, 0, wallW, wallH);
for (const cell of makeFacade(floors, bays, floorH, bayW)) {
drawElement(ctx, cell, cellType(cell, floors, bays));
}
ctx.restore();
}
// drawBuilding(ctx, 40, 300, 5, 4); // a 5-floor, 4-bay building
Run that one line and there's a proper little building: a sandy wall, a row of shopfronts with a door in the middle at street level, four floors of windows marching up in a tidy grid, and a row of squat attic windows across the top. It already reads as a building and not as random rectangles, and the only reason is that the rule function encoded real architectural intention. Makes sense, right?
A town of identical buildings looks fake, but so does a town of totally random ones. Real streets have rhythm - repetition with small variations. So we reach for the hash again to nudge details deterministically. Let me add lit and shuttered windows: some panes glow warm, some are closed up, decided by a hash of the cell's position so it's stable every render:
// vary each window deterministically: some lit, some shuttered.
// driven by hash2 so the SAME window is always the same -> no flickering.
function windowState(buildingSeed, cell) {
const r = hash2(buildingSeed * 100 + cell.b, cell.f);
if (r < 0.15) return "shuttered"; // 15% closed up
if (r < 0.45) return "lit"; // 30% warmly lit
return "plain"; // the rest are plain glass
}
Then we let that state pick the glass colour when we draw. The point isn't the specific numbers - it's that variation flows from a hash keyed to a seed per building, so building number 7 always has its windows lit in exactly the same pattern, forever. That's rhythm you can rely on. When I first built a street like this I used Math.random for the lights and every repaint reshuffled them like a broken Christmas tree - lesson learned, seed everything.
Facades are the outside. Now let's go inside, because interior layout is where generative architecture gets really clever. The classic trick is binary space partitioning - you take the building's footprint as one big rectangle and recursively split it into smaller rectangles until each one is room-sized. Every split is one wall. It's the same recursive-subdivide spirit as the grammar work in episode 149, just applied to space:
// binary space partitioning: recursively cut a rectangle into room-sized pieces.
// each cut is a wall. we split across the LONGER axis so rooms stay reasonable.
function splitRoom(room, depth, seed) {
const MIN = 46; // don't make rooms smaller than this
if (depth === 0 || (room.w < MIN * 2 && room.h < MIN * 2)) {
return [room]; // small enough: it's a leaf room
}
const splitVertical = room.w >= room.h; // cut the longer side
const r = 0.35 + hash2(room.x + seed, room.y) * 0.3; // split at 35%..65%
if (splitVertical) {
const cut = Math.round(room.w * r);
const a = { x: room.x, y: room.y, w: cut, h: room.h };
const b = { x: room.x + cut, y: room.y, w: room.w - cut, h: room.h };
return [...splitRoom(a, depth - 1, seed), ...splitRoom(b, depth - 1, seed)];
} else {
const cut = Math.round(room.h * r);
const a = { x: room.x, y: room.y, w: room.w, h: cut };
const b = { x: room.x, y: room.y + cut, w: room.w, h: room.h - cut };
return [...splitRoom(a, depth - 1, seed), ...splitRoom(b, depth - 1, seed)];
}
}
Trace that in your head. You start with the whole floor, cut it into two along its longer side at some point between 35% and 65%, then recurse into each half, and each half cuts itself again, and so on until a piece is too small to split. The MIN guard is what stops it dicing forever - once a room can't be halved without going below the minimum size, it becomes a leaf and stays whole. The split ratio comes from the hash, so the layout is varied but reproducible. See where this is going? A whole floor plan out of one recursive rule.
Each leaf rectangle is a room, so drawing the plan is just walking the leaves, filling each with a soft colour, and stroking a wall around it. I'll tint each room by its area so bigger rooms read as living spaces and little ones as cupboards and bathrooms:
// draw the floor plan: one filled + outlined rectangle per leaf room.
function drawPlan(ctx, ox, oy, w, h, depth = 4, seed = 1) {
const rooms = splitRoom({ x: 0, y: 0, w, h }, depth, seed);
ctx.save();
ctx.translate(ox, oy);
ctx.lineWidth = 3;
ctx.strokeStyle = "#2a2a2a"; // walls
for (const rm of rooms) {
const area = rm.w * rm.h;
const big = area > w * h * 0.12; // is this a large room?
ctx.fillStyle = big ? "#e7dbc4" : "#cdb89a";
ctx.fillRect(rm.x, rm.y, rm.w, rm.h);
ctx.strokeRect(rm.x, rm.y, rm.w, rm.h); // the wall around the room
}
ctx.restore();
}
// drawPlan(ctx, 30, 30, 300, 220, 4, 7);
Run that and you get a believable apartment layout - a couple of big rooms, some medium ones, a handful of little service spaces, all tiled together with no gaps and no overlaps, walls neatly shared between neighbours. And because the whole thing is seeded, seed = 7 always gives the exact same flat. Change depth and you get anything from a studio to a warren of tiny rooms. That's a genuinly useful little generator, and it's maybe fifteen lines.
Let me connect this back to episode 149 properly, because a building really is a grammar. Back then we grew structure by rewriting symbols with rules: a symbol expands into other symbols. Architecture is the same - "a building" expands into "a base, some floors, a roof", and "a floor" expands into "a row of bays". We can write that as an explicit rule set and let it expand a compact description into a full part list:
// a tiny shape grammar: expand a high-level building spec into concrete parts.
// this echoes the symbol-rewriting from episode 149, but the symbols are architecture.
function expandBuilding(seed) {
const floors = 3 + Math.floor(hash2(seed, 1) * 5); // 3..7 floors
const bays = 2 + Math.floor(hash2(seed, 2) * 4); // 2..5 bays
const style = hash2(seed, 3) < 0.5 ? "brick" : "stone";
const parts = [{ kind: "base", floors: 1 }]; // ground floor
for (let f = 1; f < floors - 1; f++) {
parts.push({ kind: "midFloor", index: f, bays }); // repeating middle
}
parts.push({ kind: "roof", style }); // cap it off
return { floors, bays, style, parts };
}
That function is the grammar's start rule. From a single integer seed it decides a plausible number of floors and bays and a material, then lays out the parts in order: a base, a stack of middle floors, a roof. It's exactly the "symbol expands into an ordered list of sub-symbols" idea, only now the symbols are architectural pieces. The building's whole DNA is one number, and the grammar unfolds it into something buildable.
Time for the payoff, and it's my favourite part: a street. We place buildings side by side, each one seeded by its index so they're all diffrent, and we let ground level vary a touch so it feels like a real block rather than a spreadsheet. Because everything is deterministic, this exact street exists forever and will redraw pixel-for-pixel the same:
// generate a whole row of buildings. each seeded by its index -> all different,
// all reproducible, all clearly from the same town.
function drawStreet(ctx, w, h, count = 8) {
ctx.fillStyle = "#b7c4cc"; // sky
ctx.fillRect(0, 0, w, h);
const ground = h - 20;
let x = 20;
for (let i = 0; i < count; i++) {
const spec = expandBuilding(i + 1); // grammar decides this building
const bayW = 34 + Math.floor(hash2(i, 9) * 12);
drawBuilding(ctx, x, ground, spec.floors, spec.bays, 40, bayW);
x += spec.bays * bayW + 8; // shuffle along, small gap
if (x > w) break; // stop when we run off the canvas
}
ctx.fillStyle = "#5a5148"; // the pavement
ctx.fillRect(0, ground, w, h - ground);
}
const canvas = document.querySelector("canvas");
canvas.width = 640; canvas.height = 360;
const ctx = canvas.getContext("2d");
drawStreet(ctx, 640, 360);
And there it is - a whole streetscape. Tall skinny buildings next to short wide ones, some with five floors, some with seven, doors at street level, shopfronts, rows of windows climbing up, attic floors across the top, all sitting on a shared pavement under a flat sky. No two buildings identical, but every one obeys the same rules, so the street holds together the way a real neighbourhood does. That balance - variety inside order - is the entire art of generative design, and you just built a town that has it.
Math.random - so building number 7's lit windows are the same every render. Reproducible variety is what makes it a real system, not a shuffleSo that's generative architecture, and I hope you can feel how it braids together threads from all over the series - grids from the very early episodes, the deterministic hash from the noise arc, recursive subdivision, and the grammar idea from episode 149 - into buildings you can actually reason about. The single thing I want lodged in your head: variety inside order. Random alone looks like chaos, identical alone looks like a spreadsheet, and the sweet spot is a small set of rules with seeded variation on top. Get one drawBuilding on screen first, then treat yourself to the whole street - watching a coherent little town assemble itself from nothing but integer seeds is a proper thrill :-).
And here's the thread forward. Over this whole arc we've built a big pile of generative pieces - noise, grammars, agents, worlds, and now buildings - each one its own little system with its own rules and its own seeds. Next time we stop learning new techniques for a moment and instead put on the designer's hat: how do you take everything you've collected and deliberately compose it into one generative system that makes something you actually intended, start to finish? That's a diffrent and honestly harder skill than any single algorithm, and it's exactly where we head next. 't Was plezant to play architect with you today - now go build yourself a street and give it a name :-).
Sallukes! Thanks for reading.
X