JavaScript tutorial on the fern fractal

There’s a quiet magic in the Barnsley fern—a simple set of rules that blossoms into a living, leafy pattern. This tutorial guides you from the core math to a complete JavaScript implementation, with clear steps and an interactive canvas to make the fern dance.


What the fern fractal is

The Barnsley fern is generated using an Iterated Function System (IFS): you repeatedly apply one of several affine transformations to a point, chosen at random with specific probabilities. Over many iterations, the points settle into the shape of a fern.

The math behind the fern

Each fern point update uses one of four affine transformations of the form:

f(x, y) =
| a  b | | x | + | e |
| c  d | | y |   | f |

The classic Barnsley fern IFS uses these transforms with associated probabilities:

Stem (1%):

x' = 0.00 * x + 0.00 * y + 0.00
y' = 0.00 * x + 0.16 * y + 0.00

Successive leaflet (85%):

x' = 0.85 * x + 0.04 * y + 0.00
y' = -0.04 * x + 0.85 * y + 1.60

Left leaflet (7%):

x' = 0.20 * x - 0.26 * y + 0.00
y' = 0.23 * x + 0.22 * y + 1.60

Right leaflet (7%):

x' = -0.15 * x + 0.28 * y + 0.00
y' = 0.26 * x + 0.24 * y + 0.44

Project setup with canvas

<!-- Minimal HTML skeleton -->
<div id="controls">
  <label>Points: <span id="countLabel">100000</span></label>
  <input id="count" type="range" min="20000" max="500000" step="10000" value="100000">
  <label>Hue: <span id="hueLabel">140</span></label>
  <input id="hue" type="range" min="0" max="360" step="1" value="140">
  <button id="draw">Draw</button>
  <button id="animate">Animate</button>
  <button id="clear">Clear</button>
</div>

<canvas id="fern" width="600" height="900"></canvas>

Implementing the fern in JavaScript

Core logic and drawing

// Inline, runnable implementation
(() => {
  const canvas = document.getElementById('fern');
  const ctx = canvas.getContext('2d');
  const countSlider = document.getElementById('count');
  const hueSlider = document.getElementById('hue');
  const countLabel = document.getElementById('countLabel');
  const hueLabel = document.getElementById('hueLabel');
  const btnDraw = document.getElementById('draw');
  const btnAnimate = document.getElementById('animate');
  const btnClear = document.getElementById('clear');

  // Fern coordinate bounds
  const bounds = { xmin: -3, xmax: 3, ymin: 0, ymax: 10 };

  function toPixel(x, y) {
    const w = canvas.width, h = canvas.height;
    const sx = (x - bounds.xmin) / (bounds.xmax - bounds.xmin) * w;
    const sy = h - (y - bounds.ymin) / (bounds.ymax - bounds.ymin) * h; // invert y
    return [sx, sy];
  }

  // Affine transforms with probabilities
  const transforms = [
    // stem
    { a: 0.0,  b: 0.0,  c: 0.0,  d: 0.16, e: 0.0,  f: 0.0,  p: 0.01 },
    // main leaflet
    { a: 0.85, b: 0.04, c: -0.04, d: 0.85, e: 0.0, f: 1.6, p: 0.85 },
    // left leaflet
    { a: 0.20, b: -0.26, c: 0.23, d: 0.22, e: 0.0, f: 1.6, p: 0.07 },
    // right leaflet
    { a: -0.15, b: 0.28, c: 0.26, d: 0.24, e: 0.0, f: 0.44, p: 0.07 },
  ];

  // Convert probabilities to cumulative thresholds
  const thresholds = (() => {
    let acc = 0;
    return transforms.map(t => (acc += t.p));
  })();

  function nextPoint(x, y) {
    const r = Math.random();
    let i = 0;
    while (r > thresholds[i]) i++;
    const t = transforms[i];
    const nx = t.a * x + t.b * y + t.e;
    const ny = t.c * x + t.d * y + t.f;
    return [nx, ny];
  }

  function clearCanvas() {
    ctx.fillStyle = '#0b0f14';
    ctx.fillRect(0, 0, canvas.width, canvas.height);
  }

  function drawFern(nPoints, hueBase = 140) {
    clearCanvas();
    // Use additive blending for glow
    ctx.globalCompositeOperation = 'lighter';
    let x = 0, y = 0; // start at origin
    // Skip initial iterations to reach attractor faster
    for (let i = 0; i < 50; i++) ([x, y] = nextPoint(x, y));

    for (let i = 0; i < nPoints; i++) {
      [x, y] = nextPoint(x, y);
      const [u, v] = toPixel(x, y);
      const lightness = 40 + 20 * Math.random(); // subtle variation
      const hue = hueBase + (Math.sin(i * 0.0002) * 40);
      ctx.fillStyle = `hsl(${hue}, 70%, ${lightness}%)`;
      ctx.fillRect(u, v, 1, 1);
    }
    ctx.globalCompositeOperation = 'source-over';
  }

  // UI wiring
  function updateLabels() {
    countLabel.textContent = countSlider.value;
    hueLabel.textContent = hueSlider.value;
  }
  updateLabels();

  countSlider.addEventListener('input', updateLabels);
  hueSlider.addEventListener('input', updateLabels);

  btnDraw.addEventListener('click', () => {
    drawFern(parseInt(countSlider.value, 10), parseInt(hueSlider.value, 10));
  });

  let animId = null;
  btnAnimate.addEventListener('click', () => {
    if (animId) { cancelAnimationFrame(animId); animId = null; return; }
    clearCanvas();
    ctx.globalCompositeOperation = 'lighter';
    let x = 0, y = 0;
    for (let i = 0; i < 50; i++) ([x, y] = nextPoint(x, y));
    let i = 0;
    const target = parseInt(countSlider.value, 10);
    const hueBase = parseInt(hueSlider.value, 10);

    function step() {
      for (let k = 0; k < 2000 && i < target; k++, i++) {
        [x, y] = nextPoint(x, y);
        const [u, v] = toPixel(x, y);
        const hue = hueBase + (Math.sin(i * 0.0003) * 60);
        ctx.fillStyle = `hsl(${hue}, 75%, 55%)`;
        ctx.fillRect(u, v, 1, 1);
      }
      if (i < target) animId = requestAnimationFrame(step);
      else { ctx.globalCompositeOperation = 'source-over'; animId = null; }
    }
    step();
  });

  btnClear.addEventListener('click', () => {
    if (animId) { cancelAnimationFrame(animId); animId = null; }
    clearCanvas();
  });

  // Draw initial fern
  drawFern(parseInt(countSlider.value, 10), parseInt(hueSlider.value, 10));
})();

Live demo: Use the sliders and buttons below to render and animate the fern.


Check out these JavaScript tutorials:

Animated Julia fractal (32 lines)

Minesweeper game (100 lines)

Optical illusion (18 lines)

Oldschool fire effect (20 lines)

8-bit style sine text scroller (30 lines)

Physics engine for beginners

Starfield (21 lines)

Yin Yang with a twist (4 circles and 20 lines)

Interactive animated sprites

Your first program in JavaScript: you need 5 minutes and a notepad


Fractals in Excel