Chris Collins

Lab · Playground

L-system plants

A few rewriting rules and a turtle grow a meadow of plants that are never quite the same twice.

Code blocks on this page are editable — change them and press Run.

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.

A stochastic L-system meadow
// 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 — 18 gives tall grasses, 35 bushy shrubs, 90 (with JITTER = 0) a very square tree.

  • JITTER = 0 and 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'] with ANGLE = 25.

  • The stroke colour uses depth (how many branches deep the turtle is); change it to colour by height instead.