Learn Creative Coding (#149) - Grammar-Based Generation

Last time I left you with a proper cliffhanger. We'd just given our L-system turtle a full 3D body, grown trees and coral in space, and right at the end I asked the question that's been rattling around my head for weeks: what if a grammar could lay out a building? A dungeon? A whole street? Today we answer it. We take the one idea that made L-systems tick - a tiny set of rules that replace a thing with more things - and we point it at architecture instead of plants. Allez, let me show you what I figured out, because this one genuinly changed how I think about generative work.
Here's the shift in one sentence. L-systems rewrote strings: a symbol became a longer string of symbols, and the turtle read the result. A shape grammar rewrites space: a rectangle becomes several smaller rectangles, each with its own job, and you keep going until every rectangle is small and specific enough to just draw. Same machinery, different medium. Once that clicked for me I started seeing facades everywhere as split-up boxes, and I couldn't unsee it.
From strings to shapes
Let me anchor this against what we already know. The plant grammars had rules like "replace A with AB". Pure text. The whole cleverness was that a text rewrite, read by a turtle, drew a plant. A shape grammar keeps the replace-a-thing-with-parts idea but drops the text layer entirely - the thing being replaced is a literal shape on the canvas:
// L-systems rewrote STRINGS: a symbol becomes a longer string of symbols.
const rules = { A: "AB", B: "A" };
// shape grammars rewrite SPACE: a rectangle becomes several smaller rectangles.
// same idea (replace-a-thing-with-parts), different medium.
Why bother switching from strings to shapes? Because buildings, posters, UI layouts and city blocks are fundamentally about dividing space, not about growing a path. A turtle is brilliant for organic branching things. It's terrible for "put a row of five identical windows across this wall". Splitting a box is exactly the right tool for that, and it turns out a shocking amount of designed-looking structure is just boxes inside boxes inside boxes.
A shape is just a labelled box
We need a way to hold a shape as data. For architecture we can get astonishingly far with nothing but axis-aligned rectangles, so that's all I'll use. The one twist versus a plain rectangle is a symbol - a little label that says what this box currently represents. "Building", "Floor", "Window". The symbol is what decides which rule fires next, exactly like the letters in our old grammar:
// a shape is just a labelled axis-aligned rectangle.
// x,y is the top-left corner; w,h its size; symbol says what it "is".
function rect(x, y, w, h, symbol) {
return { x, y, w, h, symbol };
}
That's the whole data model. A shape grammar is then a bunch of rules, and each rule takes one labelled box and returns a list of smaller labelled boxes. The symbols on the children decide what happens to them next. See where this is going? It's a tree of boxes, and we grow it downward until the leaves are things we can paint.
The two workhorses: split into rows and columns
Almost every architectural rule is a split. You take a box and cut it into strips, either vertically (columns) or horizontally (rows). The nice version doesn't use fixed pixel sizes - it uses relative weights, so a rule reads like "left wall, big middle, right wall" as weights [1, 3, 1] and scales to any box size. Here's the column splitter:
// split a rectangle into vertical strips by a list of relative weights.
// weights [1,2,1] -> three columns sized 25%, 50%, 25% of the width.
function splitCols(r, weights, symbols) {
const total = weights.reduce((a, b) => a + b, 0);
const out = [];
let x = r.x;
for (let i = 0; i < weights.length; i++) {
const w = r.w * weights[i] / total;
out.push(rect(x, r.y, w, r.h, symbols[i]));
x += w;
}
return out;
}
Rows are the exact same idea turned ninety degrees - walk down instead of across, carve height instead of width:
// split a rectangle into horizontal strips by relative weights.
function splitRows(r, weights, symbols) {
const total = weights.reduce((a, b) => a + b, 0);
const out = [];
let y = r.y;
for (let i = 0; i < weights.length; i++) {
const h = r.h * weights[i] / total;
out.push(rect(r.x, y, r.w, h, symbols[i]));
y += h;
}
return out;
}
Two functions. Honestly, that's the load-bearing wall of this entire episode (sorry, I couldn't resist). Everything else is decoration on top of splitting boxes by weights.
Repeat: when you don't know the count
Splitting by weights is great when you know how many parts you want. But a facade has "as many windows as fit", and you don't know that number up front - it depends on how wide the building is. That's the repeat operator: pick a rough target size, work out how many fit, then divide evenly so nothing is left over. It's the difference between a layout that only works at one size and one that adapts:
// repeat: fill a rectangle with as many strips of ~target width as fit,
// then divide evenly so there are no gaps. great for "a row of windows".
function repeatCols(r, target, symbol) {
const n = Math.max(1, Math.round(r.w / target));
const w = r.w / n;
const out = [];
for (let i = 0; i < n; i++) out.push(rect(r.x + i * w, r.y, w, r.h, symbol));
return out;
}
// rows are the same idea along the vertical axis - stack floors, say.
function repeatRows(r, target, symbol) {
const n = Math.max(1, Math.round(r.h / target));
const h = r.h / n;
const out = [];
for (let i = 0; i < n; i++) out.push(rect(r.x, r.y + i * h, r.w, h, symbol));
return out;
}
Notice I Math.round and then re-divide by the actual count. If I'd just chopped fixed-width strips off the left, the last one would be a weird sliver. Rounding to a whole number and sharing the width evenly keeps every window the same size, which is exactly what real buildings do.
Writing an actual grammar
Now the fun part - we describe a building as a dictionary of rules. Each key is a symbol, each value is a function that replaces one box with its parts. A symbol with no rule is a terminal: it's small and specific, so we just draw it. Read this top to bottom and you can basically hear it describing a building:
// a shape grammar: each symbol maps to a function that replaces one shape
// with a list of smaller shapes. symbols with no rule are terminals (we draw them).
const facade = {
Building: r => splitRows(r, [1, 5], ["Roof", "Floors"]),
Floors: r => repeatRows(r, 60, "Floor"),
Floor: r => splitCols(r, [1, 4, 1], ["Wall", "WindowBand", "Wall"]),
WindowBand: r => repeatCols(r, 48, "WindowCell"),
WindowCell: r => splitCols(r, [1, 2, 1], ["Wall", "Window", "Wall"]),
};
Trace it: a Building is a thin Roof on top of a tall Floors block. Floors repeats into a stack of Floor strips. Each Floor is wall-band-wall across. The band repeats into WindowCells, and each cell is wall-window-wall. The terminals that fall out the bottom are Roof, Wall and Window - and those are the boxes that actually get colour. Everything above them is just structure that dissolves once it's been split.
The derivation engine
Now we need the thing that runs the grammar. It's beautifully simple: keep a worklist of boxes still to process. Pop one, look up its rule. Got a rule? Replace the box with its children and throw them back on the list. No rule? It's a terminal, set it aside to be drawn. Repeat until the worklist is empty:
// run the grammar: keep replacing non-terminal shapes until only terminals remain.
// terminals (symbols with no rule) are the shapes we actually draw.
function derive(axiom, grammar, maxSteps = 5000) {
const work = [axiom];
const done = [];
let steps = 0;
while (work.length && steps++ < maxSteps) {
const shape = work.pop();
const rule = grammar[shape.symbol];
if (!rule) { done.push(shape); continue; } // terminal -> keep it
for (const child of rule(shape)) work.push(child);
}
return done;
}
That maxSteps guard is not optional, and I learned that the embarrassing way. If a rule ever produces a child with the same symbol at the same size, you've built an infinite loop that fills memory and hangs the tab. The step counter is a cheap seatbelt - it just stops the runaway before it takes the browser down with it. Always leave it in while you're experimenting.
Drawing the terminals
The grammar produces a flat list of terminal boxes. Rendering them is the easy bit: give each symbol a colour and paint the rectangle. I like a thin stroke too, so you can actually see the individual windows instead of one blue smear:
// draw the terminal shapes. each symbol gets a colour so structure is visible.
const palette = { Wall: "#d8d2c4", Window: "#3a5a80", Roof: "#6b4a2b" };
function draw(ctx, shapes) {
for (const s of shapes) {
ctx.fillStyle = palette[s.symbol] || "#111";
ctx.fillRect(s.x, s.y, s.w, s.h);
ctx.strokeStyle = "rgba(0,0,0,0.25)";
ctx.strokeRect(s.x, s.y, s.w, s.h);
}
}
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
const shapes = derive(rect(40, 20, 240, 360, "Building"), facade);
draw(ctx, shapes);
Run that and a little apartment block appears - cream walls, blue windows in tidy rows, a brown cap on top. From five rules. The first time it drew for me I actually laughed, because there was no drawing code that knew anything about "buildings" - it's all just boxes cut from boxes, and yet your eye instantly reads it as architecture. That gap between how simple the rules are and how structured the output looks is the whole magic of grammars.
Making every building different
One grammar, one building. Boring. The trick we used for plants works here too: let a rule pick from several options at random, and suddenly every derivation is unique. Same species, different individual. Here's a floor rule that sometimes centres its windows, sometimes shoves them to one side, sometimes fills the whole width:
// pick one option at random so every floor (and every building) differs.
function choice(options) {
return options[Math.floor(Math.random() * options.length)];
}
const stochasticFloor = r => choice([
() => splitCols(r, [1, 4, 1], ["Wall", "WindowBand", "Wall"]),
() => splitCols(r, [2, 3], ["WindowBand", "Wall"]), // asymmetric
() => repeatCols(r, 44, "WindowCell"), // full band across
])();
Swap Floor: stochasticFloor into the grammar and derive it a dozen times side by side. No two facades match, but they're all clearly the same style of building. That controlled variety - varied but coherent - is the sweet spot every generative system is chasing, and grammars hand it to you almost for free. We saw the same principle with stochastic plants; it's the exact same idea wearing a hard hat.
Conditions: stop before it shatters
There's a failure mode you'll hit fast. If a rule keeps splitting no matter what, tiny boxes get split into even tinier boxes and your windows turn into unreadable slivers. The fix is a conditional rule - check the box's size and decide whether it's worth splitting further. This is what separates a grammar that produces junk from one that produces buildings:
// conditional rule: only add a balcony if the cell is tall AND wide enough.
// below the threshold we just emit a plain wall. this stops endless slivering.
const windowCell = r => {
if (r.w < 22 || r.h < 30) return [rect(r.x, r.y, r.w, r.h, "Wall")];
return splitRows(r, [3, 1], ["GlassPart", "Balcony"]);
};
That single if is doing a lot of quiet work. It's the grammar equivalent of knowing when to stop talking. Real procedural systems are absolutely full of these little guards - "only place a door if the wall is wider than a metre", "only spawn a tower if the lot is deep enough". The rules describe what could happen; the conditions decide whether it should here, given this actual box.
From one building to a street
Here's where it gets silly-fun. A grammar doesn't care what it's dividing. If splitting a box into floors gives a building, then splitting the ground into lots and growing a facade in each lot gives a street. We just wrap our building derivation in one more layer of repeat:
// one grammar, many buildings: split the ground into lots, grow a facade in each.
function cityRow(ctx, x, y, w, h, grammar) {
const lots = repeatCols(rect(x, y, w, h, "Lot"), 90, "Lot");
for (const lot of lots) {
// small gap between buildings, and a random height per lot for a skyline
const gap = 6;
const top = y + Math.random() * 60;
const b = rect(lot.x + gap, top, lot.w - gap * 2, (y + h) - top, "Building");
draw(ctx, derive(b, grammar));
}
}
Give the lots random top edges and you get a skyline - short buildings next to tall ones, all sharing the same window logic. That is a genuinely tiny amount of code for a whole block of a procedural city, and it's the same five-ish rules we started with, just invoked once per lot. This is the moment the approach stops feeling like a toy and starts feeling like a superpower. It's also, not coincidentally, exactly the muscle you need for generating whole worlds - which is where this arc is quietly heading.
Why grammars are worth having in your kit
Let me step back, because I don't want this to read as "here's a neat building trick". Grammars are a way of thinking about generation, and it's a way that scales. When you frame a problem as "a start symbol plus rules that replace symbols with parts", you get a few things for free. You get structure - the output is never random mush, it's always a valid derivation. You get variety - sprinkle in stochastic rules and every run differs. You get control - the conditions and weights are knobs a designer can tune without touching the engine. And you get reuse - the same derive-until-terminal machine draws buildings, UI layouts, dungeon maps, generative posters, even sentences, just by swapping the rule set.
That last point is the one I want you to hold onto. We wrote one engine today, derive, and it has zero knowledge of architecture. All the "building-ness" lives in the facade dictionary. Want a different domain? Write a different dictionary. The grammar is data, the engine is fixed. That seperation - a small fixed interpreter plus swappable rule data - is one of the most powerful patterns in all of generative art, and you'll meet it again and again.
Where this is heading
So here's your homework, and it's a proper sandbox this time. Get the facade grammar drawing, then start meddling. Change the weights - make [1, 4, 1] into [1, 8, 1] for a glassy tower, or [3, 2] for something lopsided and modernist. Add a Door symbol to the ground floor only (hint: give Floors a special first row). Wire in stochasticFloor and render a row of ten buildings so you can admire the family resemblance. Then, if you're feeling brave, invent a totally different grammar - a chessboard, a stained-glass window, a subway map - using nothing but splitCols, splitRows, repeatCols and a fresh dictionary. Send me a screenshot, I genuinely want to see what you grow.
And carry the big idea with you, because it's about to matter a lot. We just built a machine where a compact set of rules generates far more structure than we typed - buildings, streets, skylines - all from splitting boxes and knowing when to stop. That exact pattern, rules-grow-structure, is the seed for the really ambitious stuff coming up: laying out terrain, placing settlements, wiring whole generated environments together. Today was buildings on a flat canvas. Soon we take this thinking somewhere much larger, and grammars are going to be right there in the toolkit when we do. 't Was plezant to build a little city with you out of nothing but labelled rectangles :-).
't Komt erop neer...
- A shape grammar rewrites space, not strings - same replace-a-thing-with-parts idea as L-systems, but the thing is a box and the parts are smaller boxes. Keep splitting until every box is a terminal you can draw
- A shape is a labelled rectangle - x, y, w, h plus a symbol that says what it is. The symbol decides which rule fires next
- Split and repeat are the whole toolkit -
splitColsandsplitRowscut a box by relative weights;repeatCols/repeatRowsfill it with as many even strips as fit when you don't know the count - The engine is tiny and domain-blind -
derivejust replaces non-terminals until only terminals remain. All the building-ness lives in the rule dictionary, not the engine - Stochastic rules give variety - let a rule pick an option at random and every derivation is a unique-but-coherent building, exactly like our stochastic plants
- Conditions stop the shattering - check a box's size before splitting further, or your windows dissolve into slivers. Rules say what could happen; conditions say whether it should, here
- Wrap it once more for a city - split the ground into lots, grow a facade in each, randomise the heights, and five rules become a whole skyline
So that's grammar-based generation, from a single labelled box all the way to a procedural street - and every bit of it came from splitting rectangles and knowing when to quit. The one takeaway, said plainly: frame generation as rules-that-replace-things-with-parts and you get structure, variety, control and reuse almost for free. Go build a weird little city, then invent a grammar for something that isn't a building at all. Merci voor het lezen, en tot de volgende keer :-).
Sallukes! Thanks for reading.
X