Beyond Trees: Exploring Abstract Geometry with L-Systems in p5.js
Inspired by an article by Gin (@gin_graphic), I decided to dive into the world of L-systems.
【p5.js】L-systemで木を描く - ギンの備忘録
While L-systems are often associated with realistic trees, I found myself captivated by their potential for creating abstract geometric shapes.
Drawing with the L-system can vary depending on the combination of parameters. And it is hard for me to predict the result by the parameters.
I've created the L-system trial code in p5.js to find the attractive drawing result out of such an infinite number of combinations.
How Minimal Angle Changes Drastically Alter Results
I was fascinated by how a simple adjustment to the angle parameter can completely transform the visual outcome. For instance, shifting from 90 to 120 degrees creates an entirely different structural feel.
let distance = 20;
let angle = radians(90);
let distance = 20;
let angle = radians(120);
These geometric patterns are hypnotic. The interplay between production rules, recursion depth, and rotation angles offers an infinite playground for experimentation.
An L-System Parameter Explorer
I developed a small tester in p5.js to efficiently explore the vast combination of parameters. This tool allows me to fine-tune the recursion depth and rotation angles in real-time.
You can see the drawing result and the combination of the parameters on the screen.
- Controls:
- UP/DOWN Arrow Keys: Increase/Decrease recursion depth.
- LEFT/RIGHT Arrow Keys: Adjust rotation angles.
The production rule is important, and I don't want to lose it. So I decided to write it on the code.
/**
* L-system tester.
* ref. https://gin-graphic.hatenablog.com/entry/2022/12/20/000000
*
* @author @deconbatch
* @version 0.1
* @license GPL3
* p5.js 1.5.0
* created 2022.12.31
*/
const initProduction = '+F[+F]F';
const maxRepeat = 10;
const maxAngle = 180;
const w = 800;
const h = w;
let repeatNum = 2;
let rotateAngle = 15;
/**
* keyPressed : change parameters then re-draw.
*/
function keyPressed() {
if (keyCode == UP_ARROW) {
repeatNum++;
} else if (keyCode == DOWN_ARROW) {
repeatNum--;
} else if (keyCode == RIGHT_ARROW) {
rotateAngle += 15;
} else if (keyCode == LEFT_ARROW) {
rotateAngle -= 15;
}
repeatNum = constrain(repeatNum, 1, maxRepeat);
rotateAngle = constrain(rotateAngle, -maxAngle, maxAngle);
redraw();
}
/**
* setup : world famous setup function.
*/
function setup() {
createCanvas(w, h)
angleMode(DEGREES);
noLoop();
noFill();
stroke(0);
}
/**
* draw : generate the production then draw it.
*/
function draw() {
// generate the production
const production = genProduction(initProduction, repeatNum);
// fit size and position with canvas
const fit = fitting(production, rotateAngle);
// draw
background(240);
push();
translate(fit.x, fit.y);
strokeWeight(3 / repeatNum);
drawShape(production, rotateAngle, fit.z);
pop();
drawParams(initProduction, repeatNum, rotateAngle, fit.z);
}
/**
* genProduction : generate the production
*/
function genProduction(_baseP, _num) {
let word = 'F';
for (let i = 0; i < _num; i++) {
let replaced = word.replace(/F/g, _baseP);
word = replaced;
}
return word;
}
/**
* fitting : calculate the size and position to fit to the canvas
*/
function fitting(_prd, _angle) {
let len = 10; // size
let cX = 0; // center x
let cY = 0; // center y
let minX = width;
let minY = height;
let maxX = 0;
let maxY = 0;
let x = 0;
let y = 0;
let a = 0;
const saveX = [];
const saveY = [];
const saveA = [];
for (let c of _prd) {
switch (c) {
case "F":
x += len * sin(a);
y += len * cos(a);
break;
case "+":
a += _angle;
break;
case "-":
a -= _angle;
break;
case "[":
saveX.push(x);
saveY.push(y);
saveA.push(a);
break;
case "]":
x = saveX.pop();
y = saveY.pop();
a = saveA.pop();
break;
default:
break;
}
minX = min(minX, x);
minY = min(minY, y);
maxX = max(maxX, x);
maxY = max(maxY, y);
}
const rate = max((maxX - minX) / width, (maxY - minY) / height) * 1.5;
if (rate != 0) {
len /= rate;
const rminX = minX / rate;
const rminY = minY / rate;
const rmaxX = maxX / rate;
const rmaxY = maxY / rate;
cX = width * 0.5 - (rminX + (rmaxX - rminX) * 0.5);
cY = height * 0.5 - (rminY + (rmaxY - rminY) * 0.5);
}
return createVector(cX, cY, len);
}
/**
* drawShape : draw the shape
*/
function drawShape(_prd, _angle, _len) {
for (let c of _prd) {
switch (c) {
case "F":
line(0, 0, 0, _len);
translate(0, _len);
break;
case "+":
rotate(-_angle);
break;
case "-":
rotate(+_angle);
break;
case "[":
push();
break;
case "]":
pop();
break;
default:
break;
}
}
}
/**
* drawParams : draw parameters
*/
function drawParams(_f, _n, _a, _l) {
const siz = 12;
push();
noStroke();
fill(0);
textSize(siz);
textAlign(LEFT, CENTER);
textFont('monospace');
text('PRODUCTION:' + _f, 10, siz);
text(' REPEAT:' + _n, 10, siz * 2);
text(' ANGLE:' + _a, 10, siz * 3);
text(' LENGTH:' + _l, 10, siz * 4);
pop();
}
/*
Copyright (C) 2022- deconbatch
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>
*/
Curated Combinations
Let me show you some combinations and the results.
Ghostie
I applied blendMode(SCREEN) to create an ethereal, translucent effect reminiscent of a spectral shadow.
- rule : -F[+F+F]-F
- reccursion : 8
- angle : 150 degree
Harvest
By combining two results and utilizing ellipse() alongside line(), I achieved an organic texture similar to ears of wheat.
- rule : F[[[+FF]-F[-F]]F]-F
- reccursion : 4
- angle : 13 degree, -9 degree
With line() without ellipse()
Abstract painting
Angle Magic: Small, precise increments (like 60.1°) can lead to complex, non-repeating abstract forms.
- rule : F[+F]-F[+F[-F]+F]
- reccursion : 5
- angle : 60.1 degree, 120.1 degree
Product development.
Getting Lost in the L-System Maze
The beauty of L-systems lies in their unpredictability. Even a tiny deviation can lead to a breathtaking discovery. Feel free to use my code and get lost in the infinite possibilities—it's a truly rewarding way to spend your time.









