GODRICH
#css-animation#button#loader#transition#scroll#dashboard#ui#design-trends

L-System Fractal Tree Generator: 9 Demos

One F steps forward, one + turns. Reread those few letters enough times and branches grow on their own — the whole recursion engine, copy-paste.

Auto-plays · click a tile to jump to its section · all nine in one zip

Every scene below runs the same two-stage pipeline: a string gets rewritten character by character for a fixed number of generations (an axiom like F or A expanded through a rule map such as F → F[+F][-F]F), then a turtle walks that finished string once — F steps forward, +/- turn by a fixed angle, [/] push and pop position and heading onto a stack so a branch can fork and the turtle can return to it. No arc or quadraticCurveTo ever draws a structural line; every branch, coastline, and fold is a straight lineTo segment, and the curve you see is only the accumulation of many short segments at a fixed angle. Zero libraries, zero WebGL, zero CDN — the zip behind this post carries all nine as vanilla HTML plus a React port of each, unlocked with the password in this post.

From a Single F's Recursion to Curves and Parameters: How the L-System Order Spreads

01 is the pipeline at its smallest: one rule, F → F[+F][-F]F, at a 26° angle carried to generation 6, and a two-way branching tree appears — the item every other rule in this set builds on. 02 swaps the string rewrite for direct geometric recursion (two squares resting on a square's hypotenuse, shrinking by 0.707 each generation) to show that self-similarity does not require turtle graphics at all. 03 and 04 are the classic pair: Koch's F → F+F--F+F folds a triangle's three sides into an infinite coastline, while Sierpinski's triangle comes from two characters, A and B, rewriting each other. 05 changes medium entirely — it rewrites a grid cell instead of a line segment, and reads the result back with getImageData instead of tweening a number. 06 pushes the same idea to generation 11, folding a strip in half over and over without ever crossing itself. 07 turns the axiom into a parameter lab, where two numbers reshape a whole plant species. 08 and 09 add a second layer of computation on top of a fixed string: 08 drives the turn angle with a depth-scaled sine wave, and 09 grows five trees under three cycling rules toward a spotlighted tagline. Seven of the nine transitioned on 23 of 24 sampled frames; 05's grid rewrite and 07's parametric plant both stall on one sampled frame (22/24) — expected for 05, whose cells update only when a generation completes, but incidental for 07, which otherwise grows continuously.

# Item Rule Angle / gen Painted area Peak frame Avg intensity
01 Recursive branch F → F[+F][-F]F 26° · 0→6 2.123% 1.581% 82.9
02 Pythagoras tree square on hypotenuse shrink 0.707 · 0→8 1.792% 1.481% 104.6
03 Koch snowflake F → F+F--F+F 60° · 0→4 8.315% 3.773% 45.0
04 Sierpinski triangle A→B-A-B, B→A+B+A 60° · 0→6 5.87% 4.984% 83.7
05 Sierpinski carpet 1 → 111101111 grid · 0→4 16.167% 14.143% 141.3
06 Dragon curve X→X+YF+, Y→-FX-Y 90° · 0→11 5.192% 5.162% 88.4
07 Parametric plant X→F+[[X]-X]-F[-FX]+X 25°±6° · 0→5 6.449% 4.406% 89.2
08 Wind-sway tree F → F[+F]F[-F] 24° · 0→5 1.706% 0.904% 70.2
09 Hero grove 3 rules cycled 0→4 · 5 trees 11.5% 9.198% 63.6

Painted area sitting low across most rows is the nature of this set, not a defect: only 05's carpet fills whole grid cells with fillRect, so its 16.167% maps straight onto pixel occupancy, and 09's five overlapping trees push the total to 11.5%. The other seven items draw nothing but 1–5px lineTo strokes, so a branch or a coastline barely grazing the canvas is exactly what a turtle-graphics tree looks like once it is measured instead of eyeballed.

Code per item

Each fence below is copied straight out of that item's own index.html — same variable names, same rule strings — with only the inline comments translated. The full nine vanilla files plus nine React ports ship in the zip.

01Recursive branch — the minimal two-way tree

A trunk drawn with F, rewritten into F[+F][-F]F for six generations at a 26° angle, is the entire tree: no other rule in this set does less to earn a branch. Measured against the canvas, it paints 2.123% of the field (1.581% peak frame), average intensity 82.9, transitioning on 23 of 24 sampled frames.

Recursive branch — the minimal two-way tree
Open in a new tab

01 · Recursive branch — the minimal two-way tree — Measured area 2.123% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

Math.sinMath.coslineTobeginPath

Rewriting F six generations before a single pixel is drawn

  // L-system string rewriting -- expand axiom F through the rule for n generations
  var RULE = { 'F': 'F[+F][-F]F' };
  function expand(axiom, gen) {
    var s = axiom;
    for (var g = 0; g < gen; g++) {
      var out = '';
      for (var i = 0; i < s.length; i++) {
        var ch = s.charAt(i);
        out += RULE[ch] !== undefined ? RULE[ch] : ch;
      }
      s = out;
    }
    return s;
  }

02Pythagoras tree — squares upon squares

Resting two squares on a square's hypotenuse and recursing that rule through generation 8 grows a barrel-shaped grove of roughly 250 squares, each side 0.707 (root 2 over 2) shorter than its parent. Once fully grown it paints only 1.792% of the field (1.481% peak, intensity 104.6) but cycles color from summer green through autumn amber and back, so the low-area reading never means the screen sits still.

Pythagoras tree — squares upon squares
Open in a new tab

02 · Pythagoras tree — squares upon squares — Measured area 1.792% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

Math.sinMath.coslineTofillRect

One render loop scheduling growth, season, and a resting sway

  function render(loop) {
    var ctx = cv.getContext('2d');
    // growth schedule: 0-0.55 generations 0->8 growing, 0.55-1.0 full grove + season color cycle
    var grPhase = Math.min(loop / 0.55, 1);
    var genF = grPhase * MAXGEN;
    var seasonT = 0.5 - 0.5 * Math.cos((loop - 0.55) / 0.45 * Math.PI * 2);  // autumn -> spring -> autumn round trip
    if (loop < 0.55) seasonT = 0;

    ctx.fillStyle = rgb(C_INK);
    ctx.fillRect(0, 0, W, H);

    var s0 = H * SIDE0;
    var cx = W * 0.5, cy = H * 0.94;
    // a very faint sway after growth completes -- root fixed, keeps grove edge pixels
    // refreshing even outside the season transition (a micro-vibration, unlike item 08's wide-amplitude wind)
    var sway = loop >= 0.55 ? Math.sin(loop * Math.PI * 5) * 0.028 : 0;
    ctx.save();
    ctx.translate(cx, cy);
    ctx.rotate(sway);
    ctx.translate(-cx, -cy);
    drawTree(ctx, cx - s0 / 2, cy - s0, s0, 0, genF, seasonT);
    ctx.restore();
  }

03Koch snowflake — lifting edges into an infinite coast

Starting from a triangle's three sides (axiom F--F--F) and rewriting every F into F+F--F+F at 60° for four generations lengthens the coastline every pass while the enclosed area barely grows — the textbook demonstration of fractal dimension. It paints 8.315% of the canvas (3.773% peak, intensity 45.0), the third-highest area in the set after 05 and 09.

Koch snowflake — lifting edges into an infinite coast
Open in a new tab

03 · Koch snowflake — lifting edges into an infinite coast — Measured area 8.315% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

Math.sinMath.coslineTostrokeStyle

The one-line rewrite behind the whole coastline

  var RULE = { 'F': 'F+F--F+F' };
  function expand(axiom, gen) {
    var s = axiom;
    for (var g = 0; g < gen; g++) {
      var out = '';
      for (var i = 0; i < s.length; i++) {
        var ch = s.charAt(i);
        out += RULE[ch] !== undefined ? RULE[ch] : ch;
      }
      s = out;
    }
    return s;
  }

04Sierpinski triangle — triangles hiding triangles

Two characters that rewrite each other — A → B-A-B, B → A+B+A — unfolded with a 60° turtle walk trace a one-stroke arrowhead curve, and by generation 6 that single unbroken line fills exactly the gaps of a Sierpinski triangle. It measures 5.87% painted area with a 4.984% peak frame, the closest peak-to-total ratio in the set, meaning most of its ink lands in a short burst near the end of growth.

Sierpinski triangle — triangles hiding triangles
Open in a new tab

04 · Sierpinski triangle — triangles hiding triangles — Measured area 5.87% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

Math.sinMath.coslineTobeginPath

The turtle walk that a two-character mutual rewrite produces

  // turtle interpretation -- A and B both step forward one unit, +/- turn 60 degrees; builds an open point list
  function turtlePath(str, step) {
    var x = 0, y = 0, a = 0;
    var pts = [{ x: x, y: y }];
    for (var i = 0; i < str.length; i++) {
      var ch = str.charAt(i);
      if (ch === 'A' || ch === 'B') {
        x += Math.cos(a) * step;
        y += Math.sin(a) * step;
        pts.push({ x: x, y: y });
      } else if (ch === '+') {
        a -= ANGLE * Math.PI / 180;
      } else if (ch === '-') {
        a += ANGLE * Math.PI / 180;
      }
    }
    return pts;
  }

05Sierpinski carpet — holes drilling in cell by cell

Every filled cell (1) rewrites into nine characters arranged 3×3 with only the center left as a hole (0) — 1 → 111101111 — and across four generations that drills a square full of holes like moth-eaten cloth, while getImageData reads the actual non-background pixels back for the "remaining area" readout instead of tweening a number. It paints the most of the set, 16.167% (14.143% peak) and the brightest average intensity, 141.3, and is one of two items in this set (with 07) that stall on a single sampled frame (22 of 24) — expected here since its display only refreshes at each generation boundary.

Sierpinski carpet — holes drilling in cell by cell
Open in a new tab

05 · Sierpinski carpet — holes drilling in cell by cell — Measured area 16.167% · moving frames 22/23Original by GODRICH · SCSS and React ship in the zip · See the password

fillRectgetImageDatarequestAnimationFrame

Interpreting the rewritten string as a filled grid, not a turtle walk

  // interpret the generation-gen string as a 3^gen x 3^gen grid, fillRect only the filled cells
  function drawCarpet(ctx, str, gen, ox, oy, side, thisF, bright) {
    var n = Math.pow(3, gen);
    var cell = side / n;
    for (var idx = 0; idx < str.length; idx++) {
      if (str.charAt(idx) === '0') continue;
      var row = Math.floor(idx / n), col = idx % n;
      var cx = ox + col * cell, cy = oy + row * cell;
      var pad = cell * (1 - thisF) * 0.5;
      ctx.fillRect(cx + pad, cy + pad, Math.max(0.4, cell - pad * 2), Math.max(0.4, cell - pad * 2));
    }
  }

06Dragon curve — a folding rule that grows a paper dinosaur

Two rules, X → X+YF+ and Y → -FX-Y, applied to the single-line axiom FX at a 90° turn produce the fold pattern of a strip of paper folded in half again and again — carried all the way to generation 11 so the folding density fills in completely, without the curve ever crossing itself. It paints 5.192% of the canvas with a 5.162% peak frame — the two numbers sit almost equal, meaning nearly all of its ink lands in that single final frame as the deepest folds resolve.

Dragon curve — a folding rule that grows a paper dinosaur
Open in a new tab

06 · Dragon curve — a folding rule that grows a paper dinosaur — Measured area 5.192% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

Math.sinMath.coslineTorequestAnimationFrame

The two mutually-rewriting rules behind the fold

  var RULE = { 'X': 'X+YF+', 'Y': '-FX-Y' };
  function expand(axiom, gen) {
    var s = axiom;
    for (var g = 0; g < gen; g++) {
      var out = '';
      for (var i = 0; i < s.length; i++) {
        var ch = s.charAt(i);
        out += RULE[ch] !== undefined ? RULE[ch] : ch;
      }
      s = out;
    }
    return s;
  }

07Parametric plant — leaves grown from an F+-[] string

Adding F → FF to a single rule, X → F+[[X]-X]-F[-FX]+X, and letting generation and angle drive it produces a houseplant leaf; once fully grown the base angle of 25° wobbles inside a ±6° band via a sine wave, showing in real time that the same grammar with a different parameter reads as a different plant. It paints 6.449% of the canvas (4.406% peak, intensity 89.2).

Parametric plant — leaves grown from an F+-[] string
Open in a new tab

07 · Parametric plant — leaves grown from an F+-[] string — Measured area 6.449% · moving frames 22/23Original by GODRICH · SCSS and React ship in the zip · See the password

Math.sinMath.coslineToglobalAlpha

The single rule pair the whole leaf comes from

  var RULE = { 'X': 'F+[[X]-X]-F[-FX]+X', 'F': 'FF' };
  function expand(axiom, gen) {
    var s = axiom;
    for (var g = 0; g < gen; g++) {
      var out = '';
      for (var i = 0; i < s.length; i++) {
        var ch = s.charAt(i);
        out += RULE[ch] !== undefined ? RULE[ch] : ch;
      }
      s = out;
    }
    return s;
  }

08Wind-sway tree — a sway whose phase drifts per generation

The rule F → F[+F]F[-F] at 24° and generation 0→5 is left completely untouched; instead every turtle turn adds a sine term whose amplitude scales with branch depth (peak amplitude 30°, frequency 1.6 cycles per loop), while branch length shrinks by 0.78 to the power of depth. The root barely moves while only the twig tips whip hard into the wind, and the item paints the least of the set — 1.706% (0.904% peak, intensity 70.2) — because it is exactly what a thin-stroke branching tree should measure at rest.

Tree in wind — a sway whose phase drifts per generation
Open in a new tab

08 · Tree in wind — a sway whose phase drifts per generation — Measured area 1.706% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

Math.sinlineTorequestAnimationFrameglobalAlpha

The same fixed string every wind-sway frame reuses

  var RULE = { 'F': 'F[+F]F[-F]' };
  function expand(axiom, gen) {
    var s = axiom;
    for (var g = 0; g < gen; g++) {
      var out = '';
      for (var i = 0; i < s.length; i++) {
        var ch = s.charAt(i);
        out += RULE[ch] !== undefined ? RULE[ch] : ch;
      }
      s = out;
    }
    return s;
  }

09Hero grove — a fractal forest growing over the brand line

Five seeds grow simultaneously, each cycled from one of three rules (F[+F]F[-F], F[+F][-F]F, FF[+F][-F]) through generations 0→4, with its own angle, scale, and growth delay so the scene reads as genuinely different rules rather than one seed copy-pasted five times. A radial-gradient spotlight brightens behind them as growth fills up, and the scene paints 11.5% of the canvas (9.198% peak, intensity 63.6), the second-highest area in the set.

Hero grove — a fractal forest growing over the brand line
Open in a new tab

09 · Hero grove — a fractal forest growing over the brand line — Measured area 11.5% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

Math.sinlineTocreateRadialGradientrequestAnimationFrame

Three rules, cycled by index, so no two neighboring trees repeat

  var RULES = [
    { 'F': 'F[+F]F[-F]' },
    { 'F': 'F[+F][-F]F' },
    { 'F': 'FF[+F][-F]' }
  ];
  function expand(rule, gen) {
    var s = 'F';
    for (var g = 0; g < gen; g++) {
      var out = '';
      for (var i = 0; i < s.length; i++) {
        var ch = s.charAt(i);
        out += rule[ch] !== undefined ? rule[ch] : ch;
      }
      s = out;
    }
    return s;
  }

Why a few letters grow a tree — rewriting and self-similarity

The string is the entire walking order

Once expand() has run its generations, the resulting string is not a description of a tree — it is a literal, ordered list of turtle instructions. A fractal tree generator built this way never stores coordinates for a branch; it stores one character (F, +, -, [, ]) per step and lets the walk assemble the geometry as a side effect of reading the string left to right. That is why 01's six-generation string and 06's eleven-generation string share the same expand() shape (a rule map applied once per generation) despite drawing a tree and a dragon curve — the rule table, not the walker, decides what the shape becomes.

Self-similarity is a rewrite applied to its own output

Every rule map in this set replaces a character with a string that can contain that same character again (F maps to a string that still holds an F inside it), so applying the rule to its own output makes each new generation a scaled copy of the whole shape sitting inside itself — a branch is a smaller tree, a triangle is three smaller triangles, a grid cell is nine smaller cells. This is also why generation count is the one dial every item exposes: 02's 8 generations and 05's 4 generations are not arbitrary, they are how many times self-similarity gets to repeat before the browser would spend more time computing invisible sub-pixel detail than drawing anything new.

An l system fractal tree is math you can watch happen

A search for l system fractal tree usually turns up a still picture; every item here instead grows from generation 0 to its maximum live, because a finished frame with no growth removes the one thing that makes this an L-system demo instead of a picture of one. Even after growth completes, tips keep breathing or swaying — 01's leaf pulse, 08's wind — because a fully-grown, perfectly still tree is indistinguishable from a raster export of the same shape.

Where this actually gets used — a landing page hero and a loading screen

The first place these nine scenes tend to land is a hero section like item 09's grove, growing over a brand line while the rest of the page loads behind it — a few seconds of motion that reads as intentional rather than a stall. Item 01's minimal two-way branch works just as well swapped in for a plain loading spinner: change two rule characters and the same code reads as a completely different brand without touching the turtle logic underneath.

Item 07 fits the opposite kind of screen, one with an actual control on it. Expose its angle or generation depth to a slider and the scene becomes a small toy the visitor drives directly — next to a plant-shop product photo, or as the live example on a coding-tutorial page, the same twenty lines of rule-and-turtle code plays a different role each time.

Branch angle, depth, and reduced-motion — three dials that change the read

Name Change Feel
Branch angle item 01 angle 25° → 45° branches spread wide and the tree's silhouette shifts from conifer to broadleaf
Recursion depth item 07 depth 4 → 8 doubling the generations fills the screen with leaves while compute cost jumps by a power of two
Shrink ratio item 08 length ratio 0.72 → 0.85 the less each branch shrinks, the denser the grove becomes and the sway feels heavier and slower

Reduced motion freezes each scene at a grown state

Every scene checks prefers-reduced-motion before scheduling its requestAnimationFrame loop; when the preference is set, the loop never starts and the scene renders once instead, then the root element takes an is-still class. Eight of the nine land on their true maximum generation — only 01 stops one generation short: its reduced-motion frame renders at loop 0.6, but 01's own growth schedule only reaches full generation 6 at loop 0.72, so the fixed frame lands at generation 5 of 6. Even one generation short, a two-way branching tree is what's on screen — nothing here regresses to a blank canvas for users who have motion turned off.

One measurement line, one aria-label, per canvas

Each item's canvas host carries a human-readable aria-label (a fractal tree maker like this one calls item 01's stage a "recursive branch lab") and a tabindex, and a line under the canvas always shows the rule, angle, and generation range currently rendering — so the same fact a screen reader announces is also printed as visible text.

Rewriting the axiom for a different shape is the fastest way to break it

Rewriting the axiom differently starves the shape of self-similarity

Every rule map here only makes sense against its own axiom — 03's F--F--F axiom is an equilateral triangle's three sides at 120° apart, and rewriting a different starting string with the same rule folds the wrong shape entirely. Change 04's axiom from A to F and the two-character mutual rewrite that fills the Sierpinski gaps never triggers, because the rule map only knows what to do with A and B.

Re-expanding the string every frame instead of once per generation

expand() triples or worse the string length per generation, and by 06's generation 11 the string is long enough that calling expand() inside requestAnimationFrame — at 60 calls a second — would turn a one-time cost into a per-frame one. Every item here precomputes the full array of generation strings once, before the loop starts, and the frame loop only ever indexes into that array.

Fixing 05's remaining-area number without reading real pixels

The temptation is to tween the displayed percentage toward a precomputed target ((8/9)^gen) instead of measuring it. That number would be correct in theory but would stop being a measurement — 05 calls getImageData on the actual canvas region and counts non-background pixels directly, which is also the reason its display only refreshes once per generation rather than every frame: the count does not change until the next rewrite lands. Take the whole set home as a fractal tree generator online you can retype from these fences, or grab the zip of nine vanilla files plus nine React ports that opens with z8hwyvrw and keep its one law: straight segments only, curves by accumulation.

Giving 08's wind the same amplitude at every depth

If the sine sway in item 08 were applied uniformly regardless of branch depth, the whole tree would rock as one stiff unit instead of swaying like a real one. The amplitude term scales with depthT * depthT specifically so the trunk barely deflects while only the outermost twigs whip near the full 30° — a squared depth ratio, not a linear one, is what keeps the base still.

Questions before you paste this fractal tree code

Is this a fractal tree generator example I can paste into a plain HTML file

Yes — each of the nine items is one self-contained index.html with no external script, CDN, or image, so copying its <canvas> markup and script tag into any page runs the scene as-is. The zip behind this post adds a React port of every item for component-based projects, using the same rule strings and turtle logic shown above.

What is a fractal tree, mechanically, in this set

It is the output of walking a string that a rule map has rewritten for a fixed number of generations — nothing about the shape is drawn directly. Item 01 rewrites F into F[+F][-F]F six times and walks the result with a turtle; item 02 recurses geometrically instead of rewriting a string, which is why its "generation" still means the same thing (one more self-similar layer) even without an expand() call.

Why does my copy only show a flat line instead of a branching tree

Almost always the [/] stack push and pop is missing or mismatched — without it, the second branch off any fork starts from wherever the first branch ended, not from the fork point, so a tree collapses into a zigzag. Check that every [ in the rule string pushes the current x, y, and angle, and every ] pops the same three values back, in that order.

Sources: MDN — CanvasRenderingContext2D.lineTo() · MDN — CanvasRenderingContext2D.save() · MDN — CanvasRenderingContext2D.restore() · MDN — Window.requestAnimationFrame() · Wikipedia — L-system · this set also sits alongside a canvas scene where thread hops between pins by rule and a crystal that grows one random-walking particle at a time — both compute a shape from a repeated rule rather than drawing it directly.

Enter the archive password

The password is inside this article. You will find it as you read.