L-System Implementation
Iteration
In the draw function, we tell the L-System class to iterate for the total number of iterations, and then draw the entire system:
//call iterate method for total number of iterations
for(int i = 0; i < numIterations; i++)
{
lSys.iterate();
}
// Draw the LSystem using the turtle
lSys.drawLSystem(t);
This means the entire L-System is redrawn with each iteration, rather than only drawing subsequent iterations.
Rule Evaluation
We loop through each character of the (previous) iteration buffer, and use that character as the key in our rules dictionary. If the key is valid, we append the dictionary value to the “new” buffer. If the key is not valid, we append the original character - which is how we can handle constants.
String current = this.getIterationString();
this.clearCurrentStringBuffer();
for (int i = 0; i < current.length(); i++) {
Character c = current.charAt(i);
if(this.rules.get(c) != null)
{
this.currentIterationBuffer.append(this.rules.get(c));
} else
{
this.currentIterationBuffer.append(c);
}
}
Draw Vocabulary
In order to draw the L System, we again loop through each character in the current string buffer, and pass that character into a switch, where the turtle is given unique drawing instructions per character in the vocabulary.
I implemented the basic drawing functions (forward, angle left and right, push and pop) as well as some additional functions for creating curves and discrete shapes.
I also included blank utility characters (A, B, X, Y etc) which can be used in the rule set to set up branches without actively drawing.
public void drawLSystem(Turtle t) {
// Our turtle's move distance
float dist = this.moveDistance;
float angle = this.rotateAngle;
// Get the current iteration string
String currentIteration = this.getIterationString();
//do turtle operations based on the character
for (int i = 0; i < currentIteration.length(); i++) {
Character c = currentIteration.charAt(i);
switch (c) {
case 'F': // Forward
t.forward(dist);
break;
case 'f': // Forward without drawing
t.penUp();
t.forward(dist);
t.penDown();
break;
case '+': // Angle Right
t.right(angle);
break;
case '-': // Angle Left
t.left(angle);
break;
case '(': // Curve Right
for(int z = 0; z < dist; z++)
{
t.right(angle/dist);
t.forward(1);
}
break;
case ')': // Curve Left
for(int z = 0; z < dist; z++)
{
t.left(angle/dist);
t.forward(1);
}
break;
case 'T': //Triangle Leaf
float l = dist;
t.left(30);
t.forward(l);
t.right(120);
t.forward(l);
t.right(120);
t.forward(l);
break;
case 'O': // Circle
float resolution = 120;
float circumference = 2 * (dist/2) * PI;
t.left(90);
for(int z = 0; z < resolution; z++)
{
t.right(360/resolution);
t.forward(circumference / resolution);
}
break;
case '[':
t.push();
break;
case ']':
t.pop();
break;
case 'X': // Branch
break;
case 'Y': // Branch
break;
///etc.
}
Probability and Randomness
There are a number of ways to inject randomness into the L-System: we can randomize variables like distance and angle each iteration, or we can have rules with multiple possible outcomes.
In order to implement multiple possible outcomes per rule, we can use a string array as the mapped value per each character, and then select one of the options at random:
for (int i = 0; i < current.length(); i++) {
Character c = current.charAt(i);
String[] rule = this.rules.get(c);
if(rule != null)
{
int sel = rand.nextInt(rule.length);
String selection = rule[sel];
this.currentIterationBuffer.append(selection);
}
We can also randomize angle and distance for each iteration of the draw function, in this case constrained by a range variable:
public float getRandomAngle()
{
return rand.nextFloat(rotateAngle - angleRange/2, rotateAngle + angleRange/2);
}
public float getRandomDistance()
{
return rand.nextFloat(moveDistance - distRange/2, moveDistance + distRange/2);
}
Attempting Determinism
Since the L-system is being drawn from scratch each draw(), using a true random value for each case would mean the entire system would be randomized each iteration, breaking continuity between iterations and preventing natural “growth” with probabilistic branching. So I attempted to use seeded random instead, and initialize a new Random class on each reset with a global seed. The global seed is randomized each time the application is run. This should mean plant-like L-systems will appear to grow naturally, even with random variables, but be different between runs.
However, branching L systems lead to the random calls being evaluated in different orders regardless, so the system loses continuity after the first branch anyway. Oh well!
Complete Code
App: https://gist.github.com/colinegge/e2e9882e95c01166b710b3c5b0472017
L-System: https://gist.github.com/colinegge/f345bdb8853dde89cafc44c989209b7f
Probabilistic L-System: https://gist.github.com/colinegge/a6c5a22d3bc48830c475c7005d3fa110
Sources
An overview of some L System techniques by Paul Bourke c. 1991: https://paulbourke.net/fractals/lsys/
L System Designs
Circuit Tree
Non-probabilistic L System
distance = 15
ø = 45
axiom = ARL
| Rules | Notes |
|---|---|
| L → [))B]R | Curve left 90 degrees, B sprout, and continue right |
| R → [(A]L | Curve right 90 degrees, B sprout, and continue left |
| A → F[(X]FFB | Long branch with right X sprout |
| B → F[)Y]FFA | Long branch with left Y sprout |
| Y → ((FFBFFO | Y Sprout becomes straight branch with A recurrance and circle endcap |
| X → ))FFA((FO | X Sprout becomes curvy branch with B recurrance and circle endcap |
Outcome

| Iteration | String |
|---|---|
| 0 | ARL |
| 1 | F[(X]FFB[(A]L[))B]R |
| 2 | F[())FFA((FO]FFF[)Y]FFA[(F[(X]FFB][))B]R[))F[)Y]FFA][(A]L |
| 3 | F[())FFF[(X]FFB((FO]FFF[)((FFBFFO]FFF[(X]FFB[(F[())FFA((FO]FFF[)Y]FFA][))F[)Y]FFA][(A]L[))F[)((FFBFFO]FFF[(X]FFB][(F[(X]FFB][))B]R |
Garden
An attempt at more intensional illustration - leading to some pretty hacky and prescriptive rules, but a nice outcome.
Probabalistic branching, fixed distance and angles.
distance = 10
ø = 30
angle range = 0;
distance range = 0;
axiom = GG
| Rules | Notes |
|---|---|
| G → +++())X(())(())X(())(())X(())X(())(— | Ground tile, with X’s as “seeds” |
| X → “[–FfB]”,“X” | Seed has 50% chance of sprouting |
| F → “YF”, “F”, “F” | Stem has 1/3 change to branch and grow |
| Y → “F[-FL]”, “F[+FL]”, “F” | Branch left or right or cancel |
| L → “T”,“L”, “L”, “L” | Branch (FL) has 1/4 change to grown a leaf |
| B → “O”, “B”, “B” | 1/3 chance to bud |
| O → “OO”, “O” | Bud grows into a flower |
Outcome

Variations
| 1 | 2 | 3 | 4 |
|---|---|---|---|
![]() |
![]() |
![]() |
![]() |
| Iteration | String |
|---|---|
| 0 | GG |
| 1 | +++())X(())(())X(())(())X(())X(())(—+++())X(())(())X(())(())X(())X(())(— |
| 2 | +++())X(())(())[–FfB](())(())[–FfB](())[–FfB](())(—+++())[–FfB](())(())X(())(())[–FfB](())[–FfB](())(— |
| 3 | +++())X(())(())[–FfB](())(())[–FfB](())[–FfB](())(—+++())[–FfO](())(())X(())(())[–YFfO](())[–YFfB](())(— |
Trash Plant
Crawling Vine with randomized distance, angles, and branching
distance = 60
distance range = 120;
ø = 45
angle range = 90;
axiom = O
| Rules | Notes |
|---|---|
| O → “(O”, “)O”, “(O”, “)O”, “)YO”, “(YO” | Grow with curve right or left, or start branch |
| Y → “[+(O]”, “[-(O]”, “Y”, “Y” | 50% to sprout new branch with identical behavior |
Output

Variations
| 1 | 2 | 3 | 4 |
|---|---|---|---|
![]() |
![]() |
![]() |
![]() |
| Iteration | String |
|---|---|
| 0 | O |
| 1 | (O |
| 2 | ()O |
| 3 | ()(O |
| 18 | ()(([+())()))(()O])[-()(([+((YO]()[+(O])YO]()()([+())([-()(YO])[-())YO]))[+(O](YO]()))))[-(O])(YO |
Code
L System Designs: https://gist.github.com/colinegge/cc9ed70f14d48a9a15264baeb8162314
Fabrication
The circuit tree and garden systems were laser etched into acrylic and wood.








