Skip to the plate
FORMA PUBLIC DOMAIN GENERATIVE ATLAS / ED. 0.28
Plate 47, L-System Plant: a still of the l-system / bracketed plate as the atlas renders it, in the fractals accent.

PL. 47  ·  FRACTALS / L-SYSTEM / BRACKETED

L-System Plant

Aristid Lindenmayer, 1968 · Przemysław Prusinkiewicz, 1990

OPEN THE LIVE PLATE ▸

DEFINITION

X → F−[[X]+X]+F[FX]−X
F → FF
δ = 22.5°

NOTES

Lindenmayer was a biologist; the grammar was a model of how cells divide and differentiate, and the drawing came later. Rewrite the axiom a few times, then read the string as turtle instructions — F grows a shoot, brackets push and pop the turtle so a branch can return to its stem. This particular rule is the arching plant from The Algorithmic Beauty of Plants, and everything about its shape lives in the grammar, not the renderer.

PROVENANCE

Origin
A. Lindenmayer, "Mathematical models for cellular interactions in development", J. Theoretical Biology 18, 1968; the plant is figure 1.24f of Prusinkiewicz & Lindenmayer, "The Algorithmic Beauty of Plants", 1990
Standing
Public domain — a rewriting grammar from a scientific paper
Constants
Generations are locked: each one quadruples the string
Source
doi:10.1016/0022-5193(68)90079-9

HOUDINI · VEX

The same published mathematics as a Detail Wrangle body. Paste it into a Wrangle with Run Over set to Detail; every constant is the published value plus a tweak channel, so Create Spare Parameters gives a slider that starts where the paper does.

// FORMA — PL. 47 · L-SYSTEM PLANT — Aristid Lindenmayer, 1968 · Przemysław Prusinkiewicz, 1990
//   X → F−[[X]+X]+F[FX]−X
//   F → FF
//   δ = 22.5°
// Paste into a Detail Wrangle (Run Over: Detail), no inputs needed.
// Written from the published mathematics, not adapted from any code.
// Constants arrive at their published values. Press the node's Create
// Spare Parameters button and every tweak becomes a slider — starting
// at 0, the published figure, and moving in the constant's own units.
// https://forma-gen.com/#plate=lsystem

float p_ang  = 22.5 + chf('ang_tweak');     // δ — branch angle · live 14 .. 32
float p_gens = 5 + chf('gens_tweak');       // generations · live 3 .. 5

// The plate's own colour: FORMA's FRACTALS accent as a cosine ramp,
// brightest near t = 0 and t = 1, near-black around t = 0.5.
vector forma_ramp(float t){
  return set(
    0.46 + 0.5 * cos(6.28318530718 * (t + 0)),
    0.1389 + 0.151 * cos(6.28318530718 * (t + 0.05)),
    0.1912 + 0.2078 * cos(6.28318530718 * (t + 0.1)));
}

// X → F−[[X]+X]+F[FX]−X, F → FF, δ = p_ang: Lindenmayer's grammar read as
// turtle instructions — F grows a unit shoot, brackets push and pop the
// turtle so a branch returns to its stem. The grammar is held as integer
// tokens (0 X, 1 F, 2 +, 3 −, 4 [, 5 ]), and the bracket stack is explicit
// arrays — VEX has no recursion and needs none here. The plate leans each
// plant by its grid phase; the port grows straight up. Colour and alpha
// are the plate's own, climbing the bright lobe by branch depth.
int forma_ruleX[] = {1, 3, 4, 4, 0, 5, 2, 0, 5, 2, 1, 4, 1, 0, 5, 3, 0};
int forma_ruleF[] = {1, 1};

int gens = int(rint(p_gens));
int seq[] = {0};                       // the axiom, X
for (int g = 0; g < gens; g++){
    int next[];
    foreach (int sym; seq){
        if (sym == 0)      foreach (int r; forma_ruleX) push(next, r);
        else if (sym == 1) foreach (int r; forma_ruleF) push(next, r);
        else               push(next, sym);
    }
    seq = next;
}

float dl = p_ang * 3.14159265359 / 180.0;
float x = 0.0, y = 0.0, h = -3.14159265359 / 2.0;   // up, in canvas terms
int depth = 0, maxd = 1;
float stx[], sty[], sth[];
int std[];
// first pass: the segments, in turtle order, so depth is known before colour
float sx0[], sy0[], sx1[], sy1[];
int sd[];
foreach (int sym; seq){
    if (sym == 1){
        float nx = x + cos(h), ny = y + sin(h);
        push(sx0, x);  push(sy0, y);  push(sx1, nx);  push(sy1, ny);  push(sd, depth);
        x = nx;  y = ny;
    }
    else if (sym == 2) h += dl;
    else if (sym == 3) h -= dl;
    else if (sym == 4){
        push(stx, x);  push(sty, y);  push(sth, h);  push(std, depth);
        depth++;  maxd = max(maxd, depth);
    }
    else if (sym == 5){
        h = pop(sth);  y = pop(sty);  x = pop(stx);  depth = pop(std);
    }
}
for (int i = 0; i < len(sx0); i++){
    vector col = forma_ramp(0.86 + 0.2 * float(sd[i]) / float(maxd));
    int prim = addprim(0, "polyline");
    // canvas y runs down; negated so the plant grows upward
    int pa = addpoint(0, set(sx0[i], -sy0[i], 0.0));
    int pb = addpoint(0, set(sx1[i], -sy1[i], 0.0));
    setpointattrib(0, "Cd", pa, col);
    setpointattrib(0, "Cd", pb, col);
    setpointattrib(0, "Alpha", pa, 0.85);
    setpointattrib(0, "Alpha", pb, 0.85);
    addvertex(0, prim, pa);
    addvertex(0, prim, pb);
}

AFTER EFFECTS · EXPRESSION

The same published mathematics as a Shape Layer path expression. Paste it onto a Path property; every constant is the published value plus a Slider Control named _tweak, so a bare paste already draws the figure and each slider moves one constant in its own units. Trim Paths is the comet.

// FORMA — PL. 47 · L-SYSTEM PLANT — Aristid Lindenmayer, 1968 · Przemysław Prusinkiewicz, 1990
//   X → F−[[X]+X]+F[FX]−X
//   F → FF
//   δ = 22.5°
// After Effects port — paste onto a Shape Layer's Path property
// (Contents › Shape › Path). Written from the published mathematics, not
// adapted from any code. Constants arrive at their published values; add a
// Slider Control (Effect › Expression Controls) named <k>_tweak and that
// constant moves in its own units, starting at 0 — the published figure.
// The plate's comet and its reveal are Trim Paths; the stroke colour is
// FORMA's FRACTALS accent, #FF4D6A. Animation runs on time.
// https://forma-gen.com/#plate=lsystem

// A missing slider reads 0, so a bare paste already draws the figure.
function forma_tweak(n){ try { return effect(n)("Slider"); } catch (e){ return 0; } }
var p_ang  = 22.5 + forma_tweak("ang_tweak");   // δ — branch angle · live 14 .. 32
var p_gens = 5 + forma_tweak("gens_tweak");     // generations · live 3 .. 5

// The frame: the plate's W × H canvas is this comp, with the origin at the
// layer's anchor; canvas y already runs down, as After Effects' does.
var forma_W = thisComp.width, forma_H = thisComp.height, forma_t = time;
var forma_phase = 0.049964714562520385;   // this plate's own fixed phase, as the page has it
function forma_pt(x, y){ return [x - forma_W / 2, y - forma_H / 2]; }

// Lindenmayer's bracketed plant, the page's own system: X → F−[[X]+X]+F[FX]−X,
// F → FF, turning by the slider's angle, leaning by this plate's own phase so
// a grid is not a row of clones. A tree is one continuous path when every
// branch is walked out and back: on ] the turtle does not jump to the saved
// state, it retraces its own spine to the fork, and a retraced segment
// overlays itself invisibly. So the figure is 2 × the segment count in
// vertices, in turtle order — and Trim Paths grows it shoot by shoot exactly
// as the page's reveal does, each branch returning to its stem. Fitted to 0.9
// of the width and 0.92 of the height.
var dl = p_ang * Math.PI / 180, gens = Math.round(p_gens);
var str = 'X';
for (var g = 0; g < gens; g++)
  str = str.replace(/[XF]/g, function (ch){ return ch === 'X' ? 'F-[[X]+X]+F[FX]-X' : 'FF'; });
var x = 0, y = 0, h = -Math.PI / 2 + (forma_phase - 0.5) * 0.3;
var raw = [[0, 0]], spine = [[0, 0]], stack = [];
var minx = 0, maxx = 0, miny = 0, maxy = 0;
for (var i = 0; i < str.length; i++){
  var ch = str.charAt(i);
  if (ch === 'F'){
    x += Math.cos(h); y += Math.sin(h);
    var q = [x, y];
    raw.push(q); spine.push(q);
    if (x < minx) minx = x; if (x > maxx) maxx = x;
    if (y < miny) miny = y; if (y > maxy) maxy = y;
  } else if (ch === '+') h += dl;
  else if (ch === '-') h -= dl;
  else if (ch === '['){ stack.push([h, spine]); spine = [[x, y]]; }
  else if (ch === ']'){
    for (var j = spine.length - 2; j >= 0; j--) raw.push(spine[j]);   // walk back to the fork
    var top = stack.pop();
    h = top[0]; spine = top[1];
    x = spine[spine.length - 1][0]; y = spine[spine.length - 1][1];
  }
}
var s = Math.min(forma_W * 0.9 / ((maxx - minx) || 1), forma_H * 0.92 / ((maxy - miny) || 1));
var ox = (forma_W - (maxx - minx) * s) / 2 - minx * s;
var oy = (forma_H - (maxy - miny) * s) / 2 - miny * s;
var pts = [];
for (var m = 0; m < raw.length; m++) pts.push(forma_pt(ox + raw[m][0] * s, oy + raw[m][1] * s));
createPath(pts, [], [], false);