Last time I left you standing on a street you'd grown from nothing but integer seeds, and I promised that this time we'd stop learning new techniques and put on the designer's hat instead. So here we are. This is a mini-project episode, but it's a diffrent kind of mini-project than the posters and galaxies and ecosystems we've built before, because the thing we're making today isn't really the artwork. The thing we're making today is a decision process. We're going to design a brand new generative system - not a variant of boids, not another reaction-diffusion, not an L-system with the numbers tweaked - something that didn't exist before we sat down. And then we're going to actually build it, live, start to finish.
I've been dreading and looking forward to writing this one in equal measure, because it's the episode where I have to admit that the hard part of generative art was never the algorithms. You've got a whole toolbox now - noise, agents, grammars, flow fields, palettes, the deterministic hash. That's the easy bit, honestly. The hard bit is knowing what to do with them, on a blank canvas, when nobody handed you a spec. So let me show you how I actually do it at my desk when there's nothing to copy. Allez, we're going to invent something.
Here's a thing that happened to me, and maybe it's happened to you too. After I'd learned twenty algorithms I found myself opening a blank file, typing const canvas = ..., and then just... sitting there. I knew how to do Perlin noise. I knew how to do Voronoi (episode 146). I knew flocking. And I had absolutely no idea what to make, because knowing a hundred techniques is not the same as having one idea. I'd become a technique-collector, and a pile of techniques is not a system.
The fix, when I finally found it, was embarrassingly simple: stop starting from the technique. A technique answers "how". Art starts from "what" and "why". So the first rule of designing a generative system is that you don't open with perlin() or boids() - you open with a sentence about a feeling, and the algorithms are just the ingredients you reach for to get there. See the flip? The technique is the paint, not the painting.
So today I'm not going to say "let's use flow fields". I'm going to say: I want to make something that feels like colonies of colour growing into a shared space and negotiating their borders - like lichen on a rock, or spilled inks settling into each other, or countries drawing themselves onto an empty map. That's the intent. Everything else - which algorithms, which numbers - falls out of serving that sentence.
Before we write a line, let me give you the little checklist I run through in my head. It's nothing fancy, but having it written down is what got me un-stuck, so here it is:
Math.random. Reproducible or it isn't a system, it's a slot machine.That's it. Feeling, mechanic, feedback, palette, seed, tune. Let me walk our lichen idea through all six with real code. And a quick honest heads-up so you're not surprised: it's all vanilla JavaScript and the Canvas 2D API, no library.
I'm doing rule 5 first because every other piece is going to lean on it. We learned back in the seed-based episode (24) that a real generative system needs its own private, reproducible randomness - the same seed always gives the same world. Math.random can't do that, so here's a tiny seeded generator called mulberry32. Thirty-two bits of state in, a clean stream of numbers out, identical every run:
// mulberry32: a tiny seeded PRNG. same seed -> the same stream of numbers, forever.
// this is the backbone of a reproducible system: no Math.random anywhere.
function makeRng(seed) {
let s = seed >>> 0;
return function () {
s = (s + 0x6D2B79F5) | 0;
let t = Math.imul(s ^ (s >>> 15), 1 | s);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296; // a float in [0, 1)
};
}
Call makeRng(1234) and you get a function that hands out the same sequence of numbers in the same order every single time. That's the whole trick. When I want a diffrent world I change the seed number, not the code.
My colonies need to grow somehow, and I don't want them to grow in boring straight lines or perfect circles. I want them to follow a grain, like wood or wind, so the territories come out with character. That's exactly what a flow field gives us, and we've built these before in the noise arc. Let me bring back the deterministic hash and a smooth value-noise built on top of it:
// deterministic hash: same inputs -> same output, always. (our noise-arc workhorse.)
function hash2(x, y) {
let h = x * 374761393 + y * 668265263;
h = (h ^ (h >> 13)) * 1274126177;
h = h ^ (h >> 16);
return (h >>> 0) / 4294967295;
}
// smooth value noise: interpolate the hash on a grid with a smoothstep blend.
function noise2(x, y) {
const xi = Math.floor(x), yi = Math.floor(y);
const xf = x - xi, yf = y - yi;
const u = xf * xf * (3 - 2 * xf); // smoothstep on x
const v = yf * yf * (3 - 2 * yf); // smoothstep on y
const a = hash2(xi, yi), b = hash2(xi + 1, yi);
const c = hash2(xi, yi + 1), d = hash2(xi + 1, yi + 1);
return a + (b - a) * u + (c - a) * v + (a - b - c + d) * u * v;
}
And then a flow direction is just that noise read as an angle. Every point on the canvas gets an arrow, and the arrows curl smoothly because the noise is smooth:
// turn the noise field into a direction. every (x, y) gets a smoothly-varying angle.
function flowAngle(x, y, scale) {
return noise2(x * scale, y * scale) * Math.PI * 4; // several full turns across the field
}
Nothing here is new - it's the same flow field from earlier in the series. That's the whole point of composing: you don't reinvent the ingredients, you combine known ones in a new way. The novelty is going to come from what the colonies do when they meet, not from the noise.
Before I write the mechanic I'm going to do something that past-me never did and always regretted: put every number that shapes the feeling into one config object. When it's time to tune (rule 6), I do not want to be hunting for a magic 1.5 buried three functions deep. One control panel, everything on it:
// every knob that shapes the FEELING lives here. this is the control panel.
const CONFIG = {
seed: 1234,
colonies: 5, // how many colours are competing for space
agentsPerColony: 40, // how aggressively each colony grows
step: 1.6, // how far a growth-agent moves per tick
cell: 4, // territory resolution (smaller = finer borders)
flowScale: 0.006, // zoom of the flow field (smaller = broader sweeps)
hueSpread: 44, // palette width in degrees (smaller = more harmonious)
};
I promise you this one habit is worth more than any clever algorithm. The difference between a system that makes one nice picture and a system you can actually play is whether the knobs are all in reach.
Now the world the colonies fight over. It's just a low-resolution grid where each cell remembers who owns it: -1 means empty, and any number 0 and up is a colony id. This grid is the shared memory that makes the whole thing tick - it's what one colony reads to discover another colony was here first. I'll back it with a typed array for speed and give it three little helpers:
// the shared world: a grid of cells, each remembering which colony owns it.
// -1 = empty, 0.. = a colony id. this memory is what lets colonies "see" each other.
function makeTerritory(cols, rows) {
const cells = new Int16Array(cols * rows).fill(-1);
return { cols, rows, cells };
}
function owner(terr, cx, cy) {
if (cx < 0 || cy < 0 || cx >= terr.cols || cy >= terr.rows) return -2; // off-world
return terr.cells[cy * terr.cols + cx];
}
function claim(terr, cx, cy, id) {
terr.cells[cy * terr.cols + cx] = id;
}
Notice owner returns -2 for anything off the edge of the world. That little detail matters in a second, because an agent that walks off the map should just quietly stop, and a single sentinel value lets me treat "the void" exactly like "someone else's land".
Here's the heart of it, and it's genuinly the only original idea in the whole episode - everything else was borrowed. A growth-agent walks along the flow field. Each step it looks at the cell it's standing on and follows three rules:
That third rule is the core mechanic and the feedback loop at the same time, which is why the system feels alive. Each agent's fate depends on what other agents did in the past - on the trail of claimed cells they left behind. Colony A stops growing exactly where colony B already reached, and vice versa, so the border between them isn't drawn by me, it's negotiated by the history of who got there first. That's the feedback loop: the system reacting to its own past. Here it is:
// one growth-agent takes a step. this tiny function is the entire soul of the system.
function stepAgent(a, terr, cfg) {
if (!a.alive) return;
const ang = flowAngle(a.x, a.y, cfg.flowScale);
a.x += Math.cos(ang) * cfg.step; // drift along the grain
a.y += Math.sin(ang) * cfg.step;
const cx = Math.floor(a.x / cfg.cell);
const cy = Math.floor(a.y / cfg.cell);
const who = owner(terr, cx, cy);
if (who === -1) { claim(terr, cx, cy, a.colony); return; } // empty -> grow into it
if (who === a.colony) return; // my own land -> pass through
a.alive = false; // foreign land or void -> stop
}
Read that and you can feel the whole artwork living inside four lines of logic. I want to really underline this: the mechanic is small. Almost every good generative system I've ever loved has a mechanic you could explain to a friend in one breath. If your core rule needs a paragraph, it's usually two ideas wearing a trenchcoat, and you should split them.
Random colour is the fastest way to make a generative piece look cheap - we talked about this all the way back in the palette episode (28). So the colonies don't get random hues. They get a harmony: I pick one anchor hue at random, then place all the colonies in a tight neighbourhood around it, so the whole picture reads as one deliberate colour story instead of a clown's pocket:
// deliberate colour: one random anchor hue, then colonies clustered tightly around it.
// analogous harmony, not a random rainbow. the hueSpread knob controls the tightness.
function harmonize(rng, n, spread) {
const base = rng() * 360; // the one anchor hue
const hues = [];
for (let i = 0; i < n; i++) {
const offset = (i / Math.max(1, n - 1) - 0.5) * spread; // spread around the anchor
hues.push((base + offset + 360) % 360);
}
return hues;
}
The anchor is random so every seed gets its own mood, but the spread is controlled, so the mood is always coherent. That's the balance we keep coming back to across this whole series: variety inside order. Random where it adds life, deliberate where it protects taste.
Now we assemble. First we scatter the colonies - each one is an id, a hue from our harmony, and a burst of agents all spawned at one seed point:
// scatter the colonies. each gets a colour and a burst of agents at one seed point.
function spawnColonies(rng, cfg, w, h) {
const hues = harmonize(rng, cfg.colonies, cfg.hueSpread);
const colonies = [];
for (let i = 0; i < cfg.colonies; i++) {
const sx = rng() * w, sy = rng() * h; // this colony's birthplace
const agents = [];
for (let k = 0; k < cfg.agentsPerColony; k++) {
agents.push({ x: sx, y: sy, colony: i, alive: true });
}
colonies.push({ id: i, hue: hues[i], agents });
}
return colonies;
}
Then the simulation: tick every living agent over and over until nobody's moving. Because they all share the flow field, they all curl the same way, but because they start in diffrent places and block each other, they carve out separate territories. I keep a guard counter so a colony trapped in a looping bit of the flow can't spin forever:
// run the whole thing: tick agents until every last one has stopped.
function grow(cfg, w, h) {
const rng = makeRng(cfg.seed);
const cols = Math.ceil(w / cfg.cell), rows = Math.ceil(h / cfg.cell);
const terr = makeTerritory(cols, rows);
const colonies = spawnColonies(rng, cfg, w, h);
let living = true, guard = 0;
while (living && guard++ < 6000) {
living = false;
for (const c of colonies) {
for (const a of c.agents) {
if (a.alive) { stepAgent(a, terr, cfg); living = true; }
}
}
}
return { terr, colonies, cell: cfg.cell };
}
That guard is me being honest with you: an agent wandering through its own territory following a looping flow field could, in theory, never reach a frontier and never die. Rather than pretend that can't happen, I cap the total ticks. Real generative systems always have one or two "just in case it runs away" guards like this, and there's no shame in them.
The world is grown; now we paint it. Walk the grid, and for every owned cell fill it with its colony's hue - but shade the lightness a touch by a slow noise so the territories aren't flat, dead fields of colour. That little wobble is what makes it read as something organic rather than a spreadsheet of coloured squares:
// paint the finished map. each owned cell gets its colony hue, with a noisy lightness wobble.
function render(ctx, result) {
const { terr, colonies, cell } = result;
for (let cy = 0; cy < terr.rows; cy++) {
for (let cx = 0; cx < terr.cols; cx++) {
const id = terr.cells[cy * terr.cols + cx];
if (id < 0) continue; // unclaimed: leave the background
const light = 44 + noise2(cx * 0.05, cy * 0.05) * 26; // 44%..70% lightness
ctx.fillStyle = `hsl(${colonies[id].hue}, 52%, ${light}%)`;
ctx.fillRect(cx * cell, cy * cell, cell, cell);
}
}
}
And one deliberate finishing touch, because borders are the whole point of this piece and I want them to sing. A second pass inks any cell that touches a different colony - a thin dark seam exactly along every negotiated frontier, like the black lines in stained glass:
// second pass: ink the negotiated borders. darken any cell that touches a foreign colony.
function inkBorders(ctx, result) {
const { terr, cell } = result;
for (let cy = 0; cy < terr.rows; cy++) {
for (let cx = 0; cx < terr.cols; cx++) {
const id = terr.cells[cy * terr.cols + cx];
if (id < 0) continue;
const neighbours = [
owner(terr, cx + 1, cy), owner(terr, cx - 1, cy),
owner(terr, cx, cy + 1), owner(terr, cx, cy - 1),
];
if (neighbours.some((n) => n >= 0 && n !== id)) {
ctx.fillStyle = "rgba(18, 16, 22, 0.55)"; // a dark seam along the frontier
ctx.fillRect(cx * cell, cy * cell, cell, cell);
}
}
}
}
Now the driver that runs the whole show. Dark background first so the empty gaps read as depth, then grow, render, and ink:
const canvas = document.querySelector("canvas");
canvas.width = 640; canvas.height = 400;
const ctx = canvas.getContext("2d");
ctx.fillStyle = "#141218"; // the void between territories
ctx.fillRect(0, 0, canvas.width, canvas.height);
const result = grow(CONFIG, canvas.width, canvas.height);
render(ctx, result);
inkBorders(ctx, result);
Run that and you get something I honestly find lovely: five colonies of a shared colour family, each grown from a single point, curling along the same hidden grain, meeting at soft organic borders inked in dark, with unclaimed voids left between them where nobody reached. It's a cousin of a Voronoi diagram, but where Voronoi computes crisp mathematical borders, ours grows them, so they're wobbly and alive and full of little peninsulas where one colony snuck through a gap. And it's completely reproducible: seed: 1234 is this exact map, forever.
This is the part nobody shows you, and it's where the actual design happens. We have a system with a control panel - so play the control panel. Every knob changes the feeling in a way you can predict:
// same system, four different moods - all from the knobs, zero new code.
const tight = { ...CONFIG, hueSpread: 14, colonies: 3 }; // near-monochrome, calm
const busy = { ...CONFIG, colonies: 9, agentsPerColony: 70 }; // crowded, competitive
const coarse = { ...CONFIG, cell: 9, step: 3.0 }; // chunky, blocky, poster-like
const wispy = { ...CONFIG, flowScale: 0.002, agentsPerColony: 18 }; // long, sweeping, sparse
Turn hueSpread down and the piece goes moody and near-monochrome. Turn colonies up and it gets crowded and competitive, borders everywhere. Make cell bigger and it turns chunky and graphic; make flowScale smaller and the territories stretch into long sweeping ribbons. None of that is new code - it's the same eleven functions, played like an instrument. When I make work I actually care about, I'll sit here for an hour just turning knobs, and that hour is the art. The code was the easy part; taste is the slow part.
Let me be really straight with you, because I think this matters. Almost nothing in this system was invented today. The PRNG is textbook. The flow field is from our noise arc. The palette harmony is from episode 28. Agents walking a field - that's the crawler idea from episode 56, and letting them leave a trail others react to is the stigmergy we saw in the slime-mold episode (58). I borrowed all of it.
The one original thing was the mechanic: agents that claim empty ground, pass through their own, and die on contact with a rival - growing negotiated borders instead of computing them. That single twist, sitting on top of a stack of borrowed parts, is enough to make something that didn't exist before. And that's the big lesson of this whole episode: originality in generative art is almost never a brand new algorithm from the void. It's a fresh combination of known parts, plus one small rule nobody else bothered to try. You already own the parts. You just have to be brave enough to add the twist and see what grows.
Math.random, or you've built a slot machine instead of a system - seed: 1234 must always give the exact same worldSo that's the whole craft, really, distilled into one project: you are not a collector of algorithms, you're a designer who happens to have a rich toolbox. The map we grew today is a nice picture, but the thing I actually wanted to hand you is the method - the little six-step path from a blank file and a vague feeling to a system you can sit and play for an hour. Go do it with your own sentence. Pick a feeling, grab three things you already know how to build, glue them together, and add one rule nobody told you to. I genuinly cannot wait to see what you grow.
And here's the thread forward. All arc long we've watched simple little rules produce things that feel far bigger and more alive than the rules themselves - a four-line agent making negotiated borders, a handful of grammar rules making a town. Why does that happen? Where exactly is the line between rules so simple the output is boring and rules so wild the output is noise, and why does all the good stuff seem to live right on that edge? There's actual theory about that edge, and next time we give it a proper name. 't Was plezant to invent something with you today :-).
Sallukes! Thanks for reading.
X