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
// 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);