LSystemTS: Rendering Recursive Lindenmayer Fractals & Space-Filling Curves in TypeScript
Computer GraphicsTypeScript & WebJune 23, 2020• 3 min read

LSystemTS: Rendering Recursive Lindenmayer Fractals & Space-Filling Curves in TypeScript

Table of Contents (3 sections)▸

In my previous post on Deploying My First TypeScript Library, I walked through packaging and publishing turtle-ts to the npm registry so I would have a clean 2D Turtle Graphics foundation in TypeScript.

Two weeks later, I completed the second half of that experiment: lsystemts (daniazar/lsystemts)—a Lindenmayer System (L-System) grammar evaluator that consumes turtle-ts to generate self-similar fractals and space-filling curves in the browser.


What is an L-System Grammar?#

Introduced in 1968 by biologist Aristid Lindenmayer to model cellular and plant growth, a deterministic context-free L-System is a formal grammar G=(V,ω,P)G = (V, \omega, P) defined by:

  1. Alphabet (VV): Terminal drawing symbols (F = draw forward, f = move forward with pen up, + = turn right by θ\theta, - = turn left by θ\theta) and non-terminal variables (X, Y).
  2. Axiom (ω\omega): The initial starting string at iteration 00.
  3. Production Rules (PP): Rewrite rules (Rule.ts) that map each symbol to a replacement string at every recursion step.

Why Depth-First Recursion Beats String Expansion#

A naive L-System implementation rewrites the entire string in memory at each iteration (S0→S1→⋯→SnS_0 \rightarrow S_1 \rightarrow \dots \rightarrow S_n) before drawing. However, because production rules expand exponentially—for example, the Sierpinski rule X → XF+XF-XF-XF+XF multiplies string length by 5×5\times per step—allocating the full expanded string at higher iteration depths quickly wastes megabytes of heap memory.

In lsystemts, src/LSystem.ts avoids materializing intermediate strings altogether by evaluating the production rules recursively on the fly via depth-first traversal (iterateAxiom):

typescript
import { Turtle } from "turtle-ts"; import { Options } from "./Options"; import { Rule } from "./Rule"; export class LSystem { private _axiom: string; private _rules: Record<string, Rule> = {}; protected _options: Options; protected _turtle: Turtle; public constructor(axiom: string, rules: Rule[], options: Options, turtle: Turtle) { this._axiom = axiom; this._options = options; for (const rule of rules) { this._rules[rule.letter] = rule; } this._turtle = turtle; } public start() { this._turtle.setColor(this._options.color); this.iterateAxiom(this._axiom, this._options.iterations); } public iterateAxiom(axiom: string, iterations: number) { if (iterations === 0) return; for (const c of axiom) { switch (c) { case '+': this._turtle.right(this._options.angle); break; case '-': this._turtle.left(this._options.angle); break; case 'F': this._turtle.forward(this._options.length); break; case 'f': this._turtle.penUp(); this._turtle.forward(this._options.length); this._turtle.penDown(); break; default: { const rule = this._rules[c.toUpperCase()]; if (rule) { this.iterateAxiom(rule.axiom, iterations - 1); } } } } } }
Interactive Architecture Diagram
Rendering architecture diagram...

Because iterateAxiom only holds the current call-stack frames (bounded by iterations, typically 3≤n≤83 \le n \le 8), memory usage stays O(n)O(n) regardless of how many thousands of line segments are drawn.


Built-In Fractal Presets (src/Examples.ts)#

To make testing and visual experimentation immediate, lsystemts includes a static Examples catalog (src/Examples.ts) defining classic fractals and space-filling curves:

Preset MethodAxiom (ω\omega)Production Rules (PP)Turn Angle (θ\theta)Default Depth
Examples.SierpinskiTriangle()X+X+XX → XF+XF-XF-XF+XF120∘120^\circ33
Examples.KochSnowflake()X++X++XX → XF-XF++XF-XF60∘60^\circ44
Examples.HilbertCurve()XX → -YF+XFX+FY-, Y → +XF-YFY-FX+90∘90^\circ44

Check out the full source code on GitHub at github.com/daniazar/lsystemts.

Share or cite this engineering article