← Generative art

Barnsley Fern

A fern draws itself point by point. Each new point comes from the last by one of four simple stretch-and-turn rules, picked at random: one makes the stem, one the smaller copy above it, and two the first leaflets on either side. A few points at first, then thousands a second, until a whole fern appears.

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-barnsley-fern to a box with a height and a dark background.

Options

Add any of these attributes to the same element.

data-pointsHow many points in all. Default fits the box, 20,000 to 160,000.
data-colorsStem and tip colours. Default #166534,#86efac.
data-durationRoughly how long it takes to draw. Default 7000 ms.
data-loopHold the finished fern, fade it and draw another, mirrored every other time.

Michael Barnsley described it in 1988: four rules, each a stretch, a turn and a shift, picked 1%, 85%, 7% and 7% of the time. It’s paused when off screen, and with reduced motion the finished fern appears at once.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Barnsley Fern: a fern draws itself point by point. Each new point comes
    // from the last by one of four simple stretch-and-turn rules, picked at
    // random: one makes the stem, one the ever-smaller copy above it, and two
    // the first leaflets on either side. A few points at first, then
    // thousands a second, until a whole fern has appeared. Paused when off
    // screen.
    // Usage: <div data-barnsley-fern style="height:360px"></div>
    // Optional: data-points (how many points in all, default fits the box),
    // data-colors (stem and tip colours, default "#166534,#86efac"),
    // data-duration (roughly how long it takes to draw in ms, default 7000),
    // data-loop (hold the finished fern, fade it and draw another, mirrored
    // every other time).
    var DEFAULTS = { colors: '#166534,#86efac', duration: 7000, hold: 3200 };
    // Barnsley's four maps: x' = a x + b y + e, y' = c x + d y + f, chance p.
    var MAPS = [
      [0, 0, 0, 0.16, 0, 0, 0.01],
      [0.85, 0.04, -0.04, 0.85, 0, 1.6, 0.85],
      [0.2, -0.26, 0.23, 0.22, 0, 1.6, 0.07],
      [-0.15, 0.28, 0.26, 0.24, 0, 0.44, 0.07]
    ];
    var SHADES = 24;

    var css =
      '[data-barnsley-fern]{position:relative;isolation:isolate;overflow:hidden}' +
      '.bfn-canvas{position:absolute;inset:0;width:100%;height:100%;z-index:-1}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }

    function rgb(hex) {
      var n = parseInt(hex.trim().replace('#', ''), 16);
      return [n >> 16, n >> 8 & 255, n & 255];
    }

    function build(el) {
      var d = el.dataset;
      var canvas = document.createElement('canvas');
      canvas.className = 'bfn-canvas';
      canvas.setAttribute('aria-hidden', 'true');
      el.insertBefore(canvas, el.firstChild);
      var ctx = canvas.getContext('2d');
      var cs = (d.colors || DEFAULTS.colors).split(',').map(rgb);
      if (cs.length < 2) cs.push(cs[0]);
      var shades = [];
      for (var i = 0; i < SHADES; i++) {
        var k = i / (SHADES - 1);
        shades.push('rgb(' + Math.round(cs[0][0] + (cs[1][0] - cs[0][0]) * k) + ',' + Math.round(cs[0][1] + (cs[1][1] - cs[0][1]) * k) + ',' + Math.round(cs[0][2] + (cs[1][2] - cs[0][2]) * k) + ')');
      }
      var duration = +d.duration || DEFAULTS.duration;
      var loop = d.loop !== undefined && d.loop !== 'false';
      var w = 0, h = 0, dpr = 1, total = 0, scale = 1, ox = 0, oy = 0, size = 1;
      var x = 0, y = 0, placed = 0, age = 0, phase = 'draw', fading = false, flip = 1;
      // Points binned by shade, so each frame sets the colour only a few times.
      var bins = [];

      function resize() {
        var nw = el.clientWidth, nh = el.clientHeight;
        if (nw === w && nh === h) return false;
        w = nw; h = nh;
        dpr = Math.min(2, window.devicePixelRatio || 1);
        canvas.width = Math.round(w * dpr);
        canvas.height = Math.round(h * dpr);
        // The fern spans x -2.2..2.7 and y 0..10.
        var pad = Math.max(10, Math.min(w, h) * 0.05);
        scale = Math.min((h - pad * 2) / 10, (w - pad * 2) / 4.9);
        ox = w / 2;
        oy = h - pad - (h - pad * 2 - scale * 10) / 2;
        total = +d.points || Math.round(Math.max(20000, Math.min(160000, scale * scale * 50)));
        size = Math.max(0.55, Math.min(1.1, scale / 40));
        return true;
      }

      function reset() {
        ctx.setTransform(1, 0, 0, 1, 0, 0);
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        x = 0; y = 0;
        placed = 0;
        age = 0;
        phase = 'draw';
      }

      // Place n more points.
      function plot(n) {
        ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
        for (var b = 0; b < SHADES; b++) bins[b] = [];
        for (var i = 0; i < n && placed < total; i++) {
          var r = Math.random(), m = MAPS[3];
          if (r < 0.01) m = MAPS[0];
          else if (r < 0.86) m = MAPS[1];
          else if (r < 0.93) m = MAPS[2];
          var nx = m[0] * x + m[1] * y + m[4];
          y = m[2] * x + m[3] * y + m[5];
          x = nx;
          placed++;
          if (placed < 20) continue;
          // Lighter towards the tips and the leaflet edges.
          var shade = Math.min(SHADES - 1, Math.floor((y / 10 * 0.7 + Math.min(1, Math.abs(x - 0.25 - y * 0.05) / 2.2) * 0.5) * SHADES));
          bins[shade].push(ox + (x - 0.24) * scale * flip, oy - y * scale);
        }
        ctx.globalAlpha = 0.8;
        for (b = 0; b < SHADES; b++) {
          var pts = bins[b];
          if (!pts.length) continue;
          ctx.fillStyle = shades[b];
          for (var j = 0; j < pts.length; j += 2) ctx.fillRect(pts[j] - size / 2, pts[j + 1] - size / 2, size, size);
        }
        ctx.globalAlpha = 1;
      }

      function tick(dt) {
        age += dt;
        if (phase === 'draw') {
          // Slowly at first, then faster: about duration ms in all.
          var k = Math.min(1, age / duration);
          var want = Math.round(total * Math.pow(k, 2.4));
          plot(Math.max(2, want - placed));
          if (placed >= total) { phase = 'hold'; age = 0; }
        } else if (loop && age > DEFAULTS.hold && !fading) {
          fading = true;
          anime.animate(canvas, {
            opacity: 0, duration: 700, ease: 'inOut(2)',
            onComplete: function () { flip = -flip; reset(); fading = false; anime.utils.set(canvas, { opacity: 1 }); }
          });
        }
      }

      resize();
      reset();
      if (reduced()) {
        plot(total);
        if (window.ResizeObserver) new ResizeObserver(function () { if (resize()) { reset(); plot(total); } }).observe(el);
        return;
      }
      if (window.ResizeObserver) new ResizeObserver(function () { if (resize()) reset(); }).observe(el);

      var last = 0;
      var timer = anime.createTimer({
        loop: true,
        duration: 1000,
        autoplay: false,
        onUpdate: function () {
          var now = performance.now();
          var dt = last ? Math.min(50, now - last) : 16;
          last = now;
          tick(dt);
        }
      });
      // Pause while off screen to save battery.
      new IntersectionObserver(function (entries) {
        if (entries[0].isIntersecting) { last = 0; timer.play(); } else timer.pause();
      }).observe(el);
    }

    function init() {
      document.querySelectorAll('[data-barnsley-fern]').forEach(function (el) {
        if (el._bfn) return;
        el._bfn = true;
        build(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<div data-barnsley-fern data-loop style="height: 360px; background: #06140c"></div>