Assignment 11 — Fractals: Recursive Trees and L-Systems

The Concept

Fractals are the one topic this semester I had basically zero context for going in. I knew the word in a vague pop-science way, but I had no idea you could generate plant-shaped organic structures from nothing but string substitution rules. That was genuinely surprising to me when I reading about it in our book Nature of Code out of curiosity to come up with an idea for this assignment.

The thing that really clicked for me when I read was the idea that you can describe a plant as a sentence. Not metaphorically, literally as a string of characters with grammar rules that expand it. You start with the single character X. You apply a rule once and get a longer string. Apply it five times and that string is tens of thousands of characters long, and when you walk it with a turtle interpreter and treat each character as a drawing instruction you get something that looks genuinely botanical. I did not draw the plant. I did not specify any branch curves or angles by hand. The structure just falls out of the grammar and I think that is really interesting.

So the sketch ended up with two modes. A recursive fractal tree where you can adjust depth and angle in real time with the keyboard and mouse, and an L-system plant that you grow generation by generation with the G key. Both show the same underlying idea, self-similarity and self-reference, but from totally different directions. The recursive tree is top-down: the rules are explicit in the code. The L-system is bottom-up: the visual structure emerges from a grammar I never directly draw.

The Code Behind It

The recursive tree is the more intuitive one to explain. The branch() function draws one line segment, then calls itself twice, once rotated left and once rotated right, with a shorter length each time. That is literally the whole thing. It stops when depth hits zero.

function branch(d, len, pal) {
  line(0, 0, 0, -len);
  translate(0, -len);

  if (d > 0) {
    push();
    rotate(treeAngle);
    branch(d - 1, len * treeLenFactor, pal);
    pop();

    push();
    rotate(-treeAngle);
    branch(d - 1, len * treeLenFactor, pal);
    pop();
  }
}

The push() and pop() around each recursive call are the part I want to highlight because they are load-bearing here, not just visual isolation. Every time you go down a branch you save the current position and rotation with push(), draw the sub-branch, then restore with pop(). Without those, after drawing the left sub-branch the turtle would be stranded at some leaf tip with no way back to the fork. The call stack combined with the matrix stack is what makes the recursion actually work spatially. I had to mess that up once before I fully understood why it works.

What I really like about this is how much control two numbers give you. treeAngle and treeLenFactor are the only two parameters that shape the entire tree, but the space they cover is huge. A narrow angle gives you a tall thin conifer, a wide angle gives you a spreading oak, and if you push treeLenFactor above 0.8 it starts producing these weird dense spirals that do not look like trees at all. I mapped treeAngle to mouse drag so you can sweep it in real time and watch the tree morph continuously. That part I really enjoyed tuning.

The L-system works differently. The grammar is just a JavaScript object:

rules = {
  'X': 'F+[[X]-X]-F[-FX]+X',
  'F': 'FF',
};

Every generation I walk the entire sentence and replace each symbol with its expansion. The sentence starts as 'X' and by generation 5 it is around 50,000 characters. I then walk that sentence again as drawing instructions: F moves forward, + and - rotate, [ and ] push and pop the matrix stack. The plant shape is never stored anywhere explicitly, it gets assembled fresh by replaying the sentence, which is a strange way to think about drawing something but it works.

Milestones and Challenges

Milestone 1: Just Getting Recursion Working

I started with the most stripped down thing possible, just a recursive tree with white strokes on black, no color, no depth parameter, stopping when the branch length drops below 4px. The only interactive thing was adjusting the angle with arrow keys. The whole sketch was maybe 25 lines.

Here is Milestone 1:

This stage was about getting the coordinate system right before the recursion got deep enough to make bugs hard to trace. The tree was drawing upside down at first because I set up the translation at the bottom of the canvas but forgot to rotate the starting direction upward. Once I added rotate(-90) in the L-system render and just started from translate(width/2, height) pointing up in the recursive tree it clicked into place.

The other thing I had to get right was the push() and pop() pattern. I read about it in the chapter but I had to break it first to really understand it. I forgot the pop() on the left branch so the right branch started from wherever the left branch had ended, which gave me this diagonal zigzag that looked nothing like a tree. Once I wrapped both calls correctly the shape snapped into place immediately.

Milestone 2: Depth-Based Color and the L-System Plant

Once the tree was structurally solid I added two things at once: depth-based color interpolation on the recursive tree, and the L-system as a second mode.

For the color I wanted the trunk to read as warm brown and the tips to read as the accent color of the palette, with a smooth transition in between. I did this by manually lerping the three RGB channels since my palette stores them as arrays:

let r = lerp(pal.branch[0], pal.trunk[0], t);
let g = lerp(pal.branch[1], pal.trunk[1], t);
let b = lerp(pal.branch[2], pal.trunk[2], t);

Where t maps from 0 at the tips to 1 at the trunk. I think this was the change that made the sketch feel finished rather than just functional. Looking at the milestone 1 screenshot and then the colored version with depth is a pretty clear difference.

Challenge: The L-System Sentence Blowing Up at Generation 5

The hardest problem was performance on the L-system. The generate() function doubles the sentence length every generation because 'F' → 'FF' doubles every F in the string. By generation 4 the sentence is tens of thousands of characters and drawing it is fine. But I had a bug early on where I was calling generate() inside draw() instead of on a keypress, which meant it was trying to re-expand the sentence 60 times per second. By the time I noticed the tab had basically frozen and the sentence was somewhere around 50 million characters.

The fix was obvious after I saw it: move generate() entirely behind the keypress handler so it only runs once when you press G. But the lesson I took from it is that exponential growth from grammar rules is not abstract, it is immediate and it will kill your program without warning. Generation 4 is totally fine. Generation 7 would be several gigabytes of string data.

The Final Result

Two fractal modes in one sketch. Recursive tree with live mouse-drag angle control and keyboard depth adjustment. L-system plant that grows generation by generation up to gen 5. Four color palettes.

Controls (recursive tree):

  • arrow up/down – increase or decrease recursion depth
  • arrow left/right – adjust branch angle
  • drag mouse – sweep branch angle continuously

Controls (L-system):

  • G – grow one generation
  • R – reset to generation 0

Shared:

  • Space – toggle between modes
  • P – cycle color palette
  • H – hide UI
  • S – save frame

Reflection and Ideas for Future Work

The thing I keep thinking about is how simple the recursive tree code actually is. The branch() function is maybe 15 lines. But at depth 9 it has drawn 512 tip segments plus all the intermediate ones, and the result looks like a real tree. That ratio between code complexity and visual complexity feels different from the other techniques this semester, and I think it is because recursion is the right language for describing things that are naturally self-similar. Trees branch. Recursion branches. They match.

The L-system surprised me more though. With the recursive tree you can look at the code and trace the structure, you can see the two recursive calls and know they will produce a Y-shape at every node. With the L-system the sentence at generation 4 is 30,000 symbols and there is no way to read it and know what shape it will draw. The shape is legible in the output but completely opaque in the representation. I find that fascinating and also kind of unsettling in a good way.

What I learned:

  • push() and pop() are not just for visual isolation. They are structurally necessary when you are doing recursive drawing because the matrix stack is what makes spatial recursion possible.
  • Exponential growth from grammar rules is very real and dangerous if you are not careful about where you call the expand function. (feel free to try to change the sentence in the initLSystem() function and see the output yourself)
  • Two numbers fully parameterize a fractal tree and the range of shapes they cover is enormous, from a conifer to an oak to something that looks nothing like a tree at all.
  • L-systems produce organic structure from combinatorial substitution rules, which makes them feel so close to how biology actually works.

What I would add next:

  • 3D turtle graphics: move to WEBGL and rotate the turtle in 3D space to grow proper 3D branching structures instead of flat 2D ones.

Leave a Reply

Your email address will not be published. Required fields are marked *