A grammar for growth
In 1968, botanist Aristid Lindenmayer devised a string-rewriting formalism to model how algae cells divide. An L-system is a formal grammar G = (V, ω, P): an alphabet V of symbols, an axiom ω (the starting string), and a set of production rules P mapping each variable to a replacement string. What sets L-systems apart from a standard Chomsky grammar is that every symbol in the string is rewritten simultaneously at each generation — parallel replacement, not sequential substitution — and that is exactly what produces self-similar, branching structure.
Algae (Lindenmayer's original 1968 example): Variables: A B Axiom: A Rules: A → AB, B → A Gen 0: A Gen 1: AB Gen 2: ABA Gen 3: ABAAB Gen 4: ABAABABA // length grows as Fibonacci numbers: 1,2,3,5,8,13…
Turtle graphics: giving the string a shape
An L-system string only becomes a picture once each character is interpreted as a command for a turtle holding a position, a heading angle, and a stack for branching: F moves forward drawing a segment, +/− turn left/right by a fixed angle δ, and [/] push and pop the turtle's state. The bracket pair is what creates a branch: push at the base, draw the side shoot, pop back to exactly where the branch started.
Fractal Plant (δ=25°): axiom X, rules X → F+[[X]−X]−F[−FX]+X, F → FF Koch curve (δ=60°): axiom F, rule F → F+F−−F+F // D = log4/log3 ≈ 1.262 Sierpiński (δ=60°): F−G−G, F→F−G+F+G−F, G→GG // D = log3/log2 ≈ 1.585
JavaScript: expand then draw
The engine has two independent phases: expand the axiom for n generations, then walk the resulting string once on Canvas 2D.
function expand(axiom, rules, gens) {
let str = axiom;
for (let g = 0; g < gens; g++)
str = [...str].map(ch => rules[ch] ?? ch).join('');
return str;
}
function draw(ctx, str, x, y, angle, step, delta) {
const stack = [];
ctx.beginPath(); ctx.moveTo(x, y);
for (const ch of str) {
if (ch === 'F') { x += step*Math.cos(angle); y -= step*Math.sin(angle); ctx.lineTo(x, y); }
else if (ch === '+') angle += delta;
else if (ch === '-') angle -= delta;
else if (ch === '[') stack.push({ x, y, angle });
else if (ch === ']') ({ x, y, angle } = stack.pop());
}
ctx.stroke();
}
A rule like F → FF roughly doubles the string length every generation — generation 10 already produces 1,024 segments, generation 20 over a million. Past a handful of generations it is far cheaper to skip materialising the full string and interpret the grammar recursively with a depth counter, producing the identical drawing in constant stack space.
Stochastic rules and 3D turtles
A deterministic rule can be replaced by several alternative productions, each with a probability that sums to 1 — running the same grammar twice then produces two different-looking plants, essential for avoiding the artificial uniformity of a purely deterministic system. In 3D, the turtle carries a full heading/left/up rotation frame instead of a single angle, and the symbols & ^ \ / apply rotation matrices around that frame's axes, letting a branch twist and pitch in space before F commits a segment to a Three.js LineSegments buffer.
Beyond closed grammars
Prusinkiewicz and Lindenmayer's 1990 book The Algorithmic Beauty of Plants extends the basic model considerably: parametric L-systems attach numbers to symbols (F(l,w)) so branches can taper with each generation; open L-systems exchange information with a simulated environment, letting a light-field simulation starve shaded branches and produce realistic shade avoidance; and context-sensitive rules condition a replacement on neighbouring symbols, modelling the inter-cell chemical signalling that motivated Lindenmayer's original biology.
Frequently asked questions
What makes L-system rewriting different from a normal grammar?
Every symbol in the string is replaced simultaneously in each generation, rather than one symbol at a time as in a standard Chomsky grammar. That parallel rewriting is what produces naturally self-similar, branching structure instead of an arbitrary derivation sequence.
How do the push [ and pop ] symbols create branches?
[ saves the turtle's current position and heading onto a stack; ] restores the most recently saved state. Everything drawn between a matching pair happens on a side branch that returns exactly to its starting point afterward — the fundamental idiom behind every tree, shrub and fern L-system.
Why do rules like F → FF cause the string to explode in length?
Because the string length roughly doubles every generation, so generation 10 already produces 1,024 segments and generation 20 over a million. High-generation L-systems should skip materialising the full string and instead use a recursive interpreter with a depth counter, which gives the same drawing in constant stack space.
Try it live
Change the axiom, rules and iteration depth and watch the plant regrow instantly in L-Systems — Procedural Plants.
▶ Open L-Systems simulation