Last time we gave our sketches honest knobs - a proper instrument panel, presets, the works - and right at the end I said something that's been buzzing in my head ever since: once you're thinking in ranges and systems instead of single outputs, a bigger idea comes knocking. What if you designed the whole thing, from the ground up, as a set of rules and relationships that generates a family of related pieces - all sharing a look, but every one different? A poster series that's really one machine. A brand mark that draws itself. That's not art-for-the-wall anymore, that's design, and it's a real discipline with real working designers behind it. Allez, today we cross into it :-).
I want to be clear about the shift, because it's a proper mental gear-change and not just "make prettier sketches". Up to now, almost everything we've made has been a single piece - one galaxy, one flow field, one shader. Even when we seeded the randomness back in episode 24 so it was reproducible, the goal was one good picture. A design system flips that on its head. The output isn't a picture, it's a machine that produces a whole family of pictures that belong together. Think of a magazine: every page is different, but you can tell at a glance they're from the same magazine. That "same-family-ness" is not an accident. Somebody designed the rules that make forty different layouts all feel like siblings. Today we build machines like that.
Here's the counter-intuitive heart of the whole thing, and it took me embarrassingly long to really get it. A generative design system is defined not by what it can draw, but by what it refuses to draw. The constraints are the design. When you say "this system only ever uses these five colours, this one typeface at these three sizes, and everything snaps to a twelve-column grid", you haven't limited the machine - you've given it an identity. Randomness inside tight constraints reads as style. Randomness with no constraints reads as noise. That difference is the entire episode.
This is really just episode 25 grown up. Way back in composition algorithms, we made rule-based layouts - "put things on a grid, leave breathing room, follow a ratio". A design system is that same instinct, but pushed all the way to a coherent language: a bundle of rules tight enough that anything the machine spits out is recognisably yours. So let's start where every design system starts - not with drawing, but with writing down the language.
Real design teams have a name for the atoms of a system: design tokens. It's a fancy phrase for a very simple idea - every decision (a colour, a spacing, a font size) is a named value stored in one place, and nothing in the system uses a raw number ever again. It's exactly the params object from last episode, except now it's not knobs to fiddle, it's the constitution of the whole thing. Let me write the tokens for a little poster system.
// design tokens: the entire vocabulary of the system, in one honest object.
// NOTHING downstream uses a raw colour or number - everything points back here.
const tokens = {
palette: ["#0d1b2a", "#e0e1dd", "#e63946", "#457b9d", "#f1faee"], // 5 colours, chosen ONCE
fonts: { display: "Georgia, serif", label: "Helvetica, sans-serif" },
scale: [12, 18, 28, 48, 88], // a type scale - only these sizes are allowed. no others.
grid: 12, // twelve columns. everything snaps to it.
margin: 0.08, // page margin as a fraction of the page (episode 25's breathing room)
gap: 16, // baseline spacing unit
};
Look at what that object is. It's not settings for one poster - it's the rulebook for every poster this system will ever make. Five colours, full stop. Five type sizes, full stop. A twelve-column grid, full stop. The moment you write it down this tightly, you've made a promise: no matter how wild the randomness gets, it can only ever choose from this vocabulary. That promise is what makes fifty outputs feel like one brand. See where this is going?
Everything in a tidy design lands on a grid - it's the invisible scaffolding under basically every poster, magazine and website you've ever seen. We touched grids in episode 5 and again in composition, but here the grid does a specific job: it converts the infinite "anywhere on the page" into a finite set of legal positions. That's huge for a generative system, because now our randomness doesn't pick pixel 483.7 - it picks column 4, which is a decision that always looks intentional.
// a grid maps tidy column/row coordinates onto real pixels, respecting the margin.
// col/row are 0..grid. this is the ONLY way anything gets positioned.
function makeGrid(size, cols, marginFrac) {
const m = size * marginFrac; // the margin in pixels
const inner = size - m * 2; // the usable area inside the margins
const cell = inner / cols; // width of one column
return {
x: (col) => m + col * cell, // column index -> x pixel
y: (row) => m + row * cell, // row index -> y pixel (square grid)
cell,
cols,
};
}
Now a position isn't a magic number, it's grid.x(3) - "the start of column three". And because everything routes through this one function, if I later change the margin token, the whole composition re-flows to match, automatically. That's the payoff of the tokens-plus-grid setup: the design has joints. You push one value and the whole thing bends in a way that still looks designed. At work we call this "single source of truth", and it's just as freeing for art as it is for a codebase.
Right, we have a vocabulary and a coordinate system. Now the fun part - the rules that actually build a layout. This is the brain of the system. I'll write a layout function that, given a seeded random source (hello again, episode 24), decides where the title goes, how big it is, which colours it uses. Crucially, every decision it makes is a choice from the tokens, never a free number.
// the layout engine: makes a full set of design decisions, but ONLY from the tokens.
// rng is our seeded random (episode 24) so the same seed => the same poster, always.
function layout(tokens, rng) {
const pick = (arr) => arr[Math.floor(rng() * arr.length)]; // choose one item from a list
return {
bg: pick(tokens.palette), // background: one of the 5 colours
ink: pick(tokens.palette), // text colour: one of the 5
accent: pick(tokens.palette), // a shape colour: one of the 5
titleSize: pick(tokens.scale.slice(2)), // title is a BIG size (from the top of the scale)
titleCol: 1 + Math.floor(rng() * 6), // which column the title starts in
titleRow: 1 + Math.floor(rng() * 8), // which row
accentCol: Math.floor(rng() * tokens.grid), // where the accent block sits
accentRows: 2 + Math.floor(rng() * 6), // how tall the accent block is
};
}
Every field there is a constrained choice. The title size isn't "some number between 40 and 90", it's "one of the big sizes we already decided on". The colour isn't a random hex, it's "one of our five". This is the whole trick, said in code: randomness picks from a vocabulary, it doesn't invent one. That's why the output will always look like it belongs to us, no matter which seed we throw at it.
Now we draw. Notice the renderer is dumb - it doesn't make any decisions, it just faithfully draws whatever the layout engine decided. That separation (a brain that decides, a hand that draws) is worth keeping clean, because it means you can change the look without touching the rules, and vice versa. I'll use the CC.sketch bootstrap and grid helper we've been building.
// the renderer just OBEYS the layout. no decisions here - all the thinking already happened.
function drawPoster(ctx, size, tokens, spec) {
const g = makeGrid(size, tokens.grid, tokens.margin);
ctx.fillStyle = spec.bg;
ctx.fillRect(0, 0, size, size); // background
// the accent block - a bold rectangle, a full column wide, snapped to the grid
ctx.fillStyle = spec.accent;
ctx.fillRect(g.x(spec.accentCol), g.y(0),
g.cell, g.cell * spec.accentRows);
// the title, positioned on the grid, sized from the scale
ctx.fillStyle = spec.ink;
ctx.font = `${spec.titleSize}px ${tokens.fonts.display}`;
ctx.textBaseline = "top";
ctx.fillText("FESTIVAL", g.x(spec.titleCol), g.y(spec.titleRow));
// a small label in the sans font, bottom-left, from the small end of the scale
ctx.font = `${tokens.scale[0]}px ${tokens.fonts.label}`;
ctx.fillText("2026 / ANTWERPEN", g.x(1), size - size * tokens.margin);
}
And wiring it up is now almost nothing - seed the randomness, ask the layout engine for a spec, hand it to the renderer:
// one seed in, one poster out. change the seed, get a different poster from the SAME system.
CC.sketch((ctx, { size }) => {
const rng = CC.seeded(7); // try 7, then 8, then 42 - all valid posters!
const spec = layout(tokens, rng); // the brain decides
drawPoster(ctx, size, tokens, spec); // the hand draws
});
Change that 7 to an 8 and you get a completely different poster - different colours, different placement - that still unmistakably belongs to the same festival. That, right there, is a generative design system in its smallest honest form. Everything from here is making it richer, not different in kind.
Here's where it stops being a party trick and starts being genuinly useful, and it's my favourite thing to do with a system like this. Because one seed makes one poster deterministically, I can loop over a hundred seeds and render a hundred posters side by side on one canvas - a contact sheet. Suddenly I'm not designing a poster, I'm browsing my system's imagination.
// render a grid of MANY variants at once - a "contact sheet" of the system's output.
// each little cell is a full poster from a different seed. this is the real superpower.
function contactSheet(ctx, size, tokens, cols = 5, rows = 4) {
const tile = size / cols; // each mini-poster's size
let seed = 1;
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
ctx.save();
ctx.translate(c * tile, r * (size / rows)); // move to this cell
const rng = CC.seeded(seed++); // a fresh seed per cell
const spec = layout(tokens, rng);
drawPoster(ctx, tile, tokens, spec); // draw a whole poster, shrunk
ctx.restore();
}
}
}
The first time you run something like this, it's a little hair-raising. You built the rules, sure, but you did not picture these twenty specific posters - the machine did. Some are boring, a couple are duds, and then two or three make you go "oh, that one, I'd never have thought to put the block there". The system surprised its own author. That feeling - being pleasantly ambushed by your own rules - is honestly the whole reason I love this corner of the craft. You're not the designer of the poster anymore, you're the designer of the space of possible posters, and then a curator who walks through it picking favourites.
Now, some of those twenty were duds, right? A dark title on a dark background you can't read, an accent colour that vibrates horribly against the bg. That's the system being too loose. And the fix is not to add randomness - it's to add constraints. This is the daily work of tuning a design system: you watch what goes wrong and you write a rule that forbids it. Let me add a couple of honest guardrails to the layout engine.
// good design systems have TASTE baked in as rules. here: never pick an unreadable pairing.
function layoutSafe(tokens, rng) {
const pick = (arr) => arr[Math.floor(rng() * arr.length)];
const bg = pick(tokens.palette);
// rule 1: the ink must contrast with the bg. keep re-picking until it does.
let ink = pick(tokens.palette);
let tries = 0;
while (contrast(bg, ink) < 0.4 && tries++ < 20) ink = pick(tokens.palette);
// rule 2: the accent must differ from BOTH bg and ink (so it actually reads as a third thing)
let accent = pick(tokens.palette);
while ((accent === bg || accent === ink) && tries++ < 40) accent = pick(tokens.palette);
return { bg, ink, accent,
titleSize: pick(tokens.scale.slice(2)),
titleCol: 1 + Math.floor(rng() * 6),
titleRow: 1 + Math.floor(rng() * 8),
accentCol: Math.floor(rng() * tokens.grid),
accentRows: 2 + Math.floor(rng() * 6) };
}
That contrast helper is just a rough brightness-difference check - we did the real colour maths back in episode 28, so I'll keep it simple here:
// rough contrast: how far apart are two colours in brightness? (0 = same, 1 = black vs white)
function luma(hex) { // perceived brightness of a hex colour
const n = parseInt(hex.slice(1), 16);
const r = (n >> 16) & 255, g = (n >> 8) & 255, b = n & 255;
return (0.299 * r + 0.587 * g + 0.114 * b) / 255; // the classic luma weights
}
function contrast(a, b) { return Math.abs(luma(a) - luma(b)); }
See the pattern? Every time the system embarrasses you, you don't reach for a slider - you write down a taste as a rule. "Ink must contrast with background." "Accent must be a genuinly third colour." Those rules are the design skill, encoded. A senior designer's whole eye, slowly turned into while loops the machine follows forever. That's a beautiful thing to build, and it's why these systems get better the longer you live with them.
Here's a thing that delighted me when it clicked - once you've got tokens plus a grid plus a seeded layout brain, you can point it at completely different products with barely any new code. Let's make the exact same system produce a repeating pattern tile instead of a poster - the kind of thing you'd use for wrapping paper, a textile, a website background. Same tokens, same grid thinking, brand-new output.
// SAME system, different product: a seamless repeating tile built from the tokens.
// fill each grid cell with a random motif from a tiny allowed set. still on-brand, because
// the colours and grid are the same tokens as the posters.
function drawTile(ctx, size, tokens, rng) {
const cells = 6;
const cell = size / cells;
const pick = (arr) => arr[Math.floor(rng() * arr.length)];
ctx.fillStyle = tokens.palette[1]; // a consistent paper colour
ctx.fillRect(0, 0, size, size);
for (let y = 0; y < cells; y++) {
for (let x = 0; x < cells; x++) {
const cx = x * cell + cell / 2, cy = y * cell + cell / 2;
ctx.fillStyle = pick(tokens.palette);
const motif = Math.floor(rng() * 3); // 0=dot, 1=bar, 2=empty. a tiny vocabulary.
if (motif === 0) {
ctx.beginPath();
ctx.arc(cx, cy, cell * 0.3, 0, CC.TAU); // a dot
ctx.fill();
} else if (motif === 1) {
ctx.fillRect(x * cell + cell * 0.35, y * cell, cell * 0.3, cell); // a vertical bar
}
}
}
}
It's the same idea wearing a different hat: a small vocabulary of motifs, placed on a grid, coloured from the shared palette, chosen by seeded randomness. Because it drinks from the same tokens as the posters, the pattern automatically matches the posters. That's the quiet superpower of a real design system - the poster, the pattern, the business card, they all share one set of tokens, so they can't help but look like family. Change one colour in tokens.palette and the posters and the patterns and everything else shift together. That's what a brand is, when you get down to it.
Let me push it one more step, to the thing designers get paid the real money for - a brand mark that's itself generative. Instead of one fixed logo, imagine a mark built from a rule, so it can flex - one version for the app icon, another for the poster corner, a giant one for the wall - all obviously the same mark. Here's a tiny one: a grid of squares where a seeded rule decides which are filled, giving a distinctive little glyph.
// a generative brand mark: a small grid where a seeded rule fills some cells.
// same seed => the SAME mark every time (that matters - a logo must be stable!).
function drawMark(ctx, size, tokens, seed) {
const rng = CC.seeded(seed);
const n = 4; // a 4x4 mark
const cell = size / n;
ctx.fillStyle = tokens.palette[0];
for (let y = 0; y < n; y++) {
for (let x = 0; x < n; x++) {
if (rng() > 0.45) { // the rule that carves out the glyph shape
ctx.fillRect(x * cell, y * cell, cell * 0.86, cell * 0.86); // small gap between cells
}
}
}
}
The point that matters here is stability: because it's seeded, drawMark(ctx, 512, tokens, 3) gives the identical glyph every single time, at any size. So you pick a seed you love once - that's your logo, forever - and the same code renders it as a 32-pixel favicon or a two-metre banner without ever storing an image file. The logo isn't a PNG anymore, it's a rule plus a seed. I find that genuinly poetic: the identity doesn't live in a file, it lives in a tiny piece of maths, and the file is just one frozen sneeze of it.
I want to slow down on the human side of this, because it's the deepest change and it's easy to miss under all the code. When you build a design system, your job moves. You stop being the person who places the title, and you become two new people: the rule-writer who decides what the system can and can't do, and the curator who browses the contact sheet and picks the keepers. Neither of those is "designing a poster" in the old sense, and both are, honestly, more powerful.
The curator half deserves its own note, because it's a real skill and nobody warns you about it. When you're staring at forty machine-made variants, "I like this one" is a decision, and making forty of those decisions well is exhausting and important. The system generates possibility; taste is what turns possibility into a choice. The machine can't do that part - it has no opinion, it just obeys your rules. So the loop of real practice looks like this:
// the actual workflow of designing WITH a system - a loop between machine and taste.
// 1. write rules (tokens + layout engine)
// 2. generate a big contact sheet of variants
// 3. CURATE: pick the ones you love, note WHY the duds are duds
// 4. turn each "why" into a new constraint (like the contrast rule)
// 5. go to 2. repeat until every variant is at least decent.
//
// you are done not when it makes a good poster, but when it can't make a BAD one.
Read that last line twice, because it's the whole philosophy: a mature design system is one that can't make a bad output. You're not tuning it toward one masterpiece, you're raising the floor until even its worst roll of the dice is something you'd happily ship. That's a totally different target from "make one nice picture", and it's the target every working generative designer is actually aiming at.
Building a system instead of a piece is a door that doesn't close again - once you've felt the pleasure of writing rules and then being surprised by your own machine, single one-off sketches start to feel a little lonely :-). And it opens onto two paths at once. The first: a system with tight tokens and fast rendering is begging to be driven in real time, in front of people - imagine tweaking the rules while an audience watches the whole family of outputs shift and breathe. That performance itch, from the instrument-panel thinking last episode, has a natural home, and it's close. The second path: everything we built today - tokens, a layout brain, a contact sheet, curation - is really the skeleton of a proper tool, the kind you'd hand to someone who can't code so they can generate on-brand posters all day. Wrapping a system like this in a real interface is a project all of its own, and it's one we'll take on together before too long. So this week, build one small system end to end - even just the poster engine above - and generate a contact sheet. Sit with the ones that surprise you. That surprise is the sound of the machine doing design with you, and it's the best reason I know to keep going.
params, promoted to a constitution). Nothing downstream ever uses a raw number again, so pushing one token re-flows the whole systemgrid.x(3) instead of pixel 483.7 - so every placement looks intentionalwhile loops. You're done not when it can make a good output, but when it can't make a bad oneSo there's the promise the GUI episode was quietly setting up, paid in full: we stopped making pictures and started making a machine that makes a family of them. And the wild part, as ever, is how few moving parts it took - a tokens object, a grid function, a little seeded brain that only picks from the vocabulary, and a loop to browse the results. Write your rules tight, generate a big messy contact sheet, keep the two or three that ambush you, and turn every dud into a fresh constraint. Do that a few rounds and you'll have built something that designs with you - which is a genuinly strange and lovely thing to have on your hard drive. Merci for reading, and go surprise yourself :-).
Sallukes! Thanks for reading.
X