Learn Creative Coding (#150) - Evolutionary Art: Genetic Algorithms
Learn Creative Coding (#150) - Evolutionary Art: Genetic Algorithms

The last few episodes we kept chasing the same holy grail: output that's varied but coherent. Stochastic grammar rules gave us buildings that all differed yet clearly belonged to one family. Seed-based art, way back in episode 24, gave us reproducible variety - one number, a whole picture. But in every single one of those, I was the one tuning the knobs. I picked the weights, I picked the palette, I picked the rules, and then randomness rattled around inside the box I'd built. Allez, today we do something that still feels a little bit like cheating: we let the artwork tune its own knobs. We put a whole population of pictures in a room, keep the ones we like, breed them, and let the next generation be a bit better than the last. That's a genetic algorithm, and using one to make art is genuinly one of the most fun corners of this whole field.
I want to be honest about why this clicked so hard for me. For years I thought "genetic algorithm" was a scary optimisation thing from a textbook - fitness landscapes, schema theorems, greek letters. Then someone showed me it's basically three tiny ideas glued together: make a bunch of random things, score them, and build the next bunch mostly out of the winners. That's it. Everything else is detail. So let me show you what I figured out, and by the end you'll have a picture that evolves in front of you.
Genotype and phenotype: the one idea that makes this work
Here's the mental model you have to get first, because everything hangs off it. In biology there's the genotype - your DNA, a compact recipe - and the phenotype - the actual you that the recipe grows into. We steal that split exactly. The genotype is a small list of numbers. The phenotype is the picture those numbers draw. We evolve the numbers; we look at the pictures.
// a genome is just a flat list of numbers in 0..1 - the "DNA".
// it means nothing on its own. the RENDERER decides what each gene controls.
function randomGenome(length = 30) {
const genes = [];
for (let i = 0; i < length; i++) genes.push(Math.random());
return genes;
}
Notice the genome has zero idea it's art. It's thirty numbers between 0 and 1. That deliberate dumbness is the whole trick - the same list could grow a face, a plant, a colour scheme, a sound, depending on how you read it. Remember episode 24, where a single seed produced a whole reproducible piece? A genome is that idea grown up: instead of one seed we carry a little bag of them, and each one steers a different part of the result.
Growing the phenotype: read the genes, draw a picture
So we need a renderer - the thing that "grows" a genome into an image. I'll keep mine simple and honest: chop the gene list into groups of five, and let each group describe one translucent circle (x, y, radius, hue, alpha). Overlap enough soft circles and you get these lovely stained-glass blobs. The mapping is arbitrary - that's the point, you invent it - but once you fix it, the same genome always grows the same picture.
// grow a genome into a picture. here every 5 genes = one translucent circle.
// the mapping from numbers -> visuals is OURS to invent. keep it fixed.
function drawPhenotype(ctx, genome, w, h) {
ctx.clearRect(0, 0, w, h);
ctx.fillStyle = "#0f1020";
ctx.fillRect(0, 0, w, h);
for (let i = 0; i + 4 < genome.length; i += 5) {
const x = genome[i] * w;
const y = genome[i + 1] * h;
const r = 8 + genome[i + 2] * 60;
const hue = Math.floor(genome[i + 3] * 360);
const alpha = 0.2 + genome[i + 4] * 0.5;
ctx.fillStyle = `hsla(${hue}, 70%, 60%, ${alpha})`;
ctx.beginPath();
ctx.arc(x, y, r, 0, Math.PI * 2);
ctx.fill();
}
}
Run that on a random genome and you get a random splat of coloured blobs. Do it for a dozen random genomes and you get a dozen different splats. None of them are any good yet - they're the primordial soup. The magic isn't in any one picture, it's in what we do to the population next. Makes sense so far?
A population is just an array of genomes
The unit of evolution is never one individual, it's a population - a crowd of them living and dying together. In code that's the least glamorous line in the whole episode: an array.
// a population is a crowd of genomes. we'll evolve the whole crowd at once.
function randomPopulation(size = 40, genomeLength = 30) {
const pop = [];
for (let i = 0; i < size; i++) pop.push(randomGenome(genomeLength));
return pop;
}
Forty random genomes, forty ugly splats. Now we need the two forces that turn a random crowd into something beautiful over time: variation (mutation and crossover, which shuffle the genes around) and selection (fitness, which decides who gets to breed). Let me build variation first, because it's the easy, mechanical half.
Mutation: small random nudges
Mutation is the simplest operator and honestly the one that does most of the heavy lifting in art. You walk the gene list and, with some small probability per gene, nudge that number a little. Not replace it - nudge it. Small steps mean a child looks mostly like its parent with a few tweaks, which is exactly what you want: you're exploring the neighbourhood of a good picture, not teleporting to a random new one.
// mutation: with small probability, nudge each gene by a little.
// rate controls HOW OFTEN, amount controls HOW FAR. both stay small.
function mutate(genome, rate = 0.1, amount = 0.15) {
return genome.map(g => {
if (Math.random() < rate) {
g += (Math.random() * 2 - 1) * amount; // nudge up or down
g = Math.max(0, Math.min(1, g)); // keep it in 0..1
}
return g;
});
}
That Math.max(0, Math.min(1, g)) clamp is not optional. If a gene drifts below 0 or above 1, your renderer starts drawing circles off-canvas or with negative radius, and you spend an afternoon confused (ask me how I know). Keeping every gene politely inside 0..1 means the renderer can trust its inputs and never has to defend itself. Clean contracts between the genome and the phenotype save you so much pain later.
Crossover: mixing two parents
Crossover is the sexy one, literally - it takes two parent genomes and builds a child from bits of both. The classic version picks a cut point and takes the left part from parent A and the right part from parent B. There's also uniform crossover, where each gene is a coin-flip between the two parents. For art I usually prefer uniform, because it blends features from all over both pictures instead of splicing "left half of mum, right half of dad".
// uniform crossover: each child gene is a coin-flip from one of two parents.
// blends features from all over both, rather than splicing halves.
function crossover(a, b) {
const child = [];
for (let i = 0; i < a.length; i++) {
child.push(Math.random() < 0.5 ? a[i] : b[i]);
}
return child;
}
Here's a thing nobody tells you at first: for a lot of generative art, crossover matters less than you'd think, and mutation matters more. Mixing two blob-fields doesn't always give you a nice blend the way mixing two solutions to a maths problem does. So don't stress if your crossover feels weak - I'll show you a mutation-only mode later that works beautifully. But it's a good tool to have, especially when different genomes have found different good ideas and you want a child that inherits both.
Fitness: the hard, interesting part
Everything up to now was mechanical. This is where art gets genuinely tricky, because fitness is the function that says "this picture is a 7, that one is a 3", and beauty does not come with a numeric score attached. There are two honest answers to "how do we score a picture", and we'll do both.
The first answer is a heuristic - we write a function that measures something we believe correlates with "nice". Symmetry is a great one; human eyes love it. Let me measure horizontal symmetry by rendering the genome, reading the pixels, and comparing the left half against the mirrored right half.
// heuristic fitness #1: horizontal symmetry.
// render, read pixels, compare left half to the mirrored right half.
function symmetryScore(ctx, genome, w, h) {
drawPhenotype(ctx, genome, w, h);
const data = ctx.getImageData(0, 0, w, h).data;
let diff = 0, samples = 0;
for (let y = 0; y < h; y += 4) {
for (let x = 0; x < w / 2; x += 4) {
const li = (y * w + x) * 4;
const ri = (y * w + (w - 1 - x)) * 4;
diff += Math.abs(data[li] - data[ri]); // red channel is enough
samples++;
}
}
return 1 - (diff / samples) / 255; // 1 = perfectly symmetric, 0 = totally not
}
See what we did? We turned a fuzzy human feeling ("that looks balanced") into a number by sampling pixels and measuring mirror-mismatch. It's rough, it only looks at the red channel for speed, but it works - evolve a population against this and it will absolutely march toward symmetric layouts. That's the strange thrill of this whole technique: you describe what you want indirectly, as a score, and evolution finds pictures that satisfy it in ways you didn't anticipate.
We can bolt on a second heuristic - reward a bit of colour variety, so we don't converge on a boring monochrome blob - and blend the two into one number with weights.
// heuristic fitness #2: colour variety, so we don't collapse to one flat colour.
function varietyScore(genome) {
const hues = [];
for (let i = 3; i < genome.length; i += 5) hues.push(genome[i]);
const mean = hues.reduce((a, b) => a + b, 0) / hues.length;
const spread = hues.reduce((a, b) => a + Math.abs(b - mean), 0) / hues.length;
return Math.min(1, spread * 3); // more spread of hues -> higher score
}
// combine heuristics into one fitness. the WEIGHTS are your aesthetic taste.
function fitness(ctx, genome, w, h) {
return 0.7 * symmetryScore(ctx, genome, w, h) + 0.3 * varietyScore(genome);
}
Those weights - 0.7 and 0.3 - are you, the artist, encoded as numbers. Crank symmetry up and you get formal, mandala-ish things. Crank variety up and it gets wilder and more chaotic. Tuning that single line is a surprisingly expressive act. It's the closest thing this method has to holding a brush.
Selection: letting fitness pick the parents
Now we use those scores to decide who breeds. My favourite method is tournament selection because it's dead simple and you can't get it wrong: grab a few random genomes, and the fittest of that little group wins a breeding slot. Do it again for the next parent. Small tournaments keep some weaker genomes in the game (which protects variety); big tournaments are ruthless and converge fast.
// tournament selection: pick k random genomes, return the fittest one.
// small k = gentle (keeps diversity), large k = ruthless (fast convergence).
function tournament(pop, scores, k = 4) {
let best = -1;
for (let i = 0; i < k; i++) {
const idx = Math.floor(Math.random() * pop.length);
if (best < 0 || scores[idx] > scores[best]) best = idx;
}
return pop[best];
}
Why not just always breed the single top genome? Because that's how you get premature convergence - the whole population collapses onto one idea in three generations and then nothing new ever happens. You need weaker genomes hanging around carrying odd genes, because one of those odd genes might be the seed of something brilliant a few generations later. Evolution needs losers. Keep some around.
The generation step: build the next population
Now we assemble the pieces into one generation. Score everyone, then build a fresh population: keep a couple of the very best unchanged (that's elitism, so we never lose our current champion), and fill the rest by selecting two parents, crossing them, and mutating the child. That loop is the beating heart of the whole thing.
// one generation: score everyone, keep the best (elitism), breed the rest.
function nextGeneration(ctx, pop, w, h) {
const scores = pop.map(g => fitness(ctx, g, w, h));
// sort a copy by fitness so we can grab the elite
const ranked = pop
.map((g, i) => ({ g, s: scores[i] }))
.sort((a, b) => b.s - a.s);
const next = [ranked[0].g, ranked[1].g]; // elitism: 2 champions survive as-is
while (next.length < pop.length) {
const mum = tournament(pop, scores);
const dad = tournament(pop, scores);
next.push(mutate(crossover(mum, dad)));
}
return { pop: next, best: ranked[0], scores };
}
Run that once and the average fitness of the crowd ticks up. Run it fifty times and the population is unrecognisable from the random soup you started with - it's full of symmetric, colourful things, because every generation the losers were filtered out and the winners got mixed and nudged. Nobody designed those final pictures. They were found.
Putting it in motion: an evolving canvas
Let me wire the whole loop to an animation frame so you can literally watch it improve. Each tick we advance one generation and paint the current champion big on the canvas. This is the payoff - open it and evolution runs live in front of you.
// live loop: each frame advance one generation and show the champion.
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
const W = canvas.width, H = canvas.height;
let population = randomPopulation(40, 30);
let gen = 0;
function tick() {
const result = nextGeneration(ctx, population, W, H);
population = result.pop;
drawPhenotype(ctx, result.best.g, W, H); // paint the current best, big
ctx.fillStyle = "white";
ctx.fillText(`gen ${gen++} fitness ${result.best.s.toFixed(3)}`, 12, 20);
if (gen < 120) requestAnimationFrame(tick);
}
tick();
Watch that number climb. The first few generations jump fast - there's tons of easy improvement to grab. Then it slows, plateaus, occasionally leaps when a lucky mutation cracks something open. That shape - fast then slow then rare leaps - is the signature of every evolutionary run, on art or anything else. Once you've seen it a few times you start to feel when a population is stuck versus still climbing.
The best fitness function is a human: interactive evolution
Here's the twist that makes this properly art and not just optimisation. Every heuristic we wrote is a guess at what's beautiful, and guesses are always a bit wrong - you'll evolve something that scores a perfect symmetry 1.0 and looks like a dead beetle. So the artist Karl Sims had a gorgeous idea back in 1991: throw the heuristic away and make the human the fitness function. Show a grid of candidates, let the person click the ones they like, and breed only from those. You're not describing beauty anymore, you're just pointing at it.
// interactive evolution: render the whole population as a clickable grid.
// the artist's clicks ARE the fitness function.
function renderGrid(container, pop, cols = 6) {
container.innerHTML = "";
pop.forEach((genome, i) => {
const c = document.createElement("canvas");
c.width = 120; c.height = 120;
drawPhenotype(c.getContext("2d"), genome, 120, 120);
c.style.outline = "2px solid transparent";
c.onclick = () => {
genome._selected = !genome._selected; // toggle a pick
c.style.outline = genome._selected ? "2px solid #4af" : "2px solid transparent";
};
container.appendChild(c);
});
}
Then breeding uses only the picked genomes as the parent pool. No pixel-reading, no symmetry maths, no weights - just "these ones, please, make more like these".
// breed the next generation from ONLY the genomes the human selected.
function breedFromSelection(pop) {
const chosen = pop.filter(g => g._selected);
if (chosen.length === 0) return randomPopulation(pop.length, pop[0].length);
const next = [];
while (next.length < pop.length) {
const a = chosen[Math.floor(Math.random() * chosen.length)];
const b = chosen[Math.floor(Math.random() * chosen.length)];
const child = mutate(crossover(a, b), 0.15, 0.2); // a touch more mutation
child._selected = false;
next.push(child);
}
return next;
}
I cannot overstate how different this feels to use. You click two or three blobs that catch your eye, hit breed, and the next grid is full of their children - some closer to what you wanted, some off in a weird direction. You click again. Within five or six rounds you've steered the population somewhere genuinely surprising, somewhere you would never have thought to type coordinates for. You're not the author exactly, and you're not a passenger either - you're a breeder, guiding a thing that has its own momentum. It's a completely different relationship to the work, and it's worth building once just to feel it.
When you DO have a target: evolving toward an image
One more flavour, because it's a lovely trick. Sometimes you do know the goal - you want the population to evolve toward a specific target image (a face, a logo, a photo). Then fitness is just "how close are your pixels to the target's pixels", and evolution becomes a slow, painterly reconstruction of the image out of your primitive shapes.
// fitness toward a TARGET image: lower pixel difference = higher score.
// evolves your blobs into an approximation of any picture you feed it.
function targetFitness(ctx, genome, target, w, h) {
drawPhenotype(ctx, genome, w, h);
const got = ctx.getImageData(0, 0, w, h).data;
let diff = 0;
for (let i = 0; i < got.length; i += 16) { // sample every 4th pixel (RGBA)
diff += Math.abs(got[i] - target[i]) + Math.abs(got[i + 1] - target[i + 1]);
}
return 1 / (1 + diff / 100000); // shrink diff into a nice 0..1-ish score
}
Feed that a photo and let it run for a few thousand generations and you get that famous effect - the Mona Lisa slowly emerging out of fifty translucent triangles. It's mesmerising, and it's the exact same engine we already built, with nothing swapped but the fitness function. That's the recurring lesson of this episode, said one more way: the engine is fixed and dumb, and all the intent lives in how you score things. Change the score, change the art, don't touch the machinery.
Where this is heading
Let me pull it together, because we covered real ground. We borrowed biology's split between genotype (a small list of numbers) and phenotype (the picture those numbers grow into), exactly the seed-based idea from episode 24 with more seeds in the bag. We made a population, then gave it the two engines of evolution: variation through mutation and crossover, and selection through fitness. We scored pictures three completely different ways - a symmetry heuristic, a human's clicks, and distance to a target image - and every time the same tiny loop turned a random crowd into something shaped and intentional. That separation, a fixed dumb engine plus a swappable fitness function, is the same design pattern we keep meeting: a small fixed interpreter and swappable data on top. You've now seen it in grammars, in seeds, and in evolution. Hold onto it, it's one of the deepest ideas in generative work.
So here's your homework, and it's a proper playground. Get the live loop running first and just watch the fitness climb. Then rewrite the renderer - swap my translucent circles for triangles, or lines, or little rotated rectangles - and notice the engine doesn't care one bit; only drawPhenotype changes. Next, build the interactive grid and breed a few generations purely by clicking what you like - I promise you'll end up somewhere you couldn't have designed on purpose. If you're feeling brave, wire in a target image and watch a photo assemble itself out of shapes overnight. And play with the knobs that are really you: the fitness weights, the mutation rate, the tournament size. Every one of them changes the mood of what evolves.
And keep the big picture in your back pocket, because it matters for what's coming. We just built a system where we don't design the output directly - we design the pressures and let structure emerge under them. A population, a bit of randomness, a rule for who survives, and out comes complexity nobody typed. That idea - simple local rules and selection producing rich global order - is about to come roaring back when we look at systems that grow and learn their own patterns, and later when we generate whole worlds. Today an image evolved on your canvas. Soon we let much bigger things do the same. 't Was plezant to breed a little population of pictures with you :-).
't Komt erop neer...
- Genotype vs phenotype is the whole trick - the genome is a small list of numbers that means nothing on its own; a renderer grows it into a picture. We evolve the numbers, we look at the pictures. It's episode 24's seed idea with a bag of seeds instead of one
- Evolution is three tiny ideas - make a random population, score it, and build the next population mostly from the winners. Everything else is detail
- Variation = mutation + crossover - mutation nudges genes a little (and does most of the artistic work); crossover mixes two parents. Always clamp genes back into range so the renderer can trust them
- Selection = fitness + tournaments - fitness scores each picture, tournament selection picks parents while keeping some weaker genomes around so the crowd doesn't collapse onto one idea too early
- Elitism keeps your champion - carry the top genome or two into the next generation unchanged so you never lose your best result to a bad breeding roll
- Fitness is where the art lives - a heuristic (symmetry, colour variety), a human clicking what they like (interactive evolution, Karl Sims 1991), or distance to a target image. Same engine, totally different art, just by swapping the score
- The engine stays dumb - a fixed, blind evolution loop plus a swappable fitness function. All the intent lives in the scoring, never in the machinery
So that's evolutionary art - genotypes and phenotypes, mutation and crossover, fitness and selection, all wired into one little loop that turns random soup into something you'd hang on a wall. The one takeaway, plainly: don't design the picture, design the pressure and let evolution find the picture for you. Go breed a population by clicking what you love, then swap the renderer and watch the same engine make something completely different. Merci voor het lezen, en tot de volgende keer :-).
Sallukes! Thanks for reading.
X