Lab · Playground
L-system plants
A few rewriting rules and a turtle grow a meadow of plants that are never quite the same twice.
An L-system (Aristid Lindenmayer’s model of how plants grow, from 1968) is a string-rewriting game. Start with a short string — the axiom — and on every generation replace each symbol by its rule. After four or five generations the string is thousands of characters long. Then hand it to a turtle that reads it left to right:
-
F— draw a line forward -
+and-— turn left or right by the angle -
[and]— remember where you are, and later go back there (that is what makes a branch)
The rule F → FF-[-F+F+F]+[+F-F-F] is a classic from The Algorithmic Beauty of Plants. This
sketch makes it stochastic: each F picks one of three rules at random, and the angle wobbles a
little at every turn, so each plant in the row is a sibling rather than a clone. The plants grow in
over a couple of seconds; click to plant a new row.
// Things to change: the rules, the angle, and how many generations.
const AXIOM = 'X';
const RULES = {
X: ['F[+X][-X]FX', 'F[-X]F[+X]X', 'F[+X]F[-X]+X'], // one is picked at random each time
F: ['FF', 'FF', 'F'],
};
const GENERATIONS = 5;
const ANGLE = 24; // degrees
const JITTER = 7; // random wobble added to each turn, degrees
const PLANTS = 5;
let plants = [];
let grown = 0;
function setup() {
createCanvas(min(windowWidth, 640), 320);
angleMode(DEGREES);
plantRow();
}
function grow(s) {
let out = '';
for (const c of s) out += RULES[c] ? random(RULES[c]) : c;
return out;
}
function plantRow() {
randomSeed(Math.floor(Math.random() * 1e9)); // draw() reseeds, so start fresh here
plants = [];
for (let i = 0; i < PLANTS; i++) {
let s = AXIOM;
for (let g = 0; g < GENERATIONS; g++) s = grow(s);
plants.push({
s,
x: (i + 0.5) * width / PLANTS + random(-15, 15),
len: random(2.6, 3.6) * height / 320,
hue: random(80, 140),
seed: floor(random(1e6)),
});
}
grown = 0;
}
function draw() {
background(210, 230, 240);
noStroke();
fill(120, 95, 70);
rect(0, height - 24, width, 24);
grown = min(1, grown + 0.012);
for (const p of plants) drawPlant(p, grown);
}
function drawPlant(p, f) {
randomSeed(p.seed); // the same wobbles every frame
const limit = floor(p.s.length * f); // grow in: draw only the first part of the string
push();
translate(p.x, height - 24);
rotate(-90);
let depth = 0;
for (let i = 0; i < limit; i++) {
const c = p.s[i];
if (c === 'F') {
stroke(70 - depth * 6, p.hue + depth * 8, 60 - depth * 4);
strokeWeight(max(0.6, 3.2 - depth * 0.6));
line(0, 0, p.len, 0);
translate(p.len, 0);
} else if (c === '+') rotate(ANGLE + random(-JITTER, JITTER));
else if (c === '-') rotate(-ANGLE + random(-JITTER, JITTER));
else if (c === '[') { push(); depth++; }
else if (c === ']') {
// A leaf where a branch ends.
noStroke(); fill(90, p.hue + 60, 80, 170); ellipse(2, 0, 5, 3);
pop(); depth--;
}
}
pop();
}
function mousePressed() {
if (mouseX >= 0 && mouseX <= width && mouseY >= 0 && mouseY <= height) plantRow();
}
Things to change
-
ANGLE—18gives tall grasses,35bushy shrubs,90(withJITTER = 0) a very square tree. -
JITTER = 0and a single rule per symbol turns it back into a classic deterministic L-system: every plant identical. -
GENERATIONS— each extra generation roughly doubles or triples the string; past 6 it gets slow. -
Try the fern rule set
X: ['F+[[X]-X]-F[-FX]+X'],F: ['FF']withANGLE = 25. -
The stroke colour uses
depth(how many branches deep the turtle is); change it to colour by height instead.