← Particles

Falling Leaves

Autumn leaves tumble down a section, swinging from side to side like a pendulum, tilting into each swing and turning over as they fall. Maple, oak and plain oval leaves, in reds, oranges and golds.

Autumn collection

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-falling-leaves to a section, hero or card.

Options

Add any of these attributes to the same element.

data-densityLeaves per 100 × 100 px. Default 0.5.
data-sizeSize of the biggest leaves, in px. Default 44.
data-speedSpeed multiplier. Default 1.
data-windSideways drift, from -1 (left) to 1 (right). Default 0.2.
data-colorsComma-separated leaf colours.

It’s drawn on a transparent canvas behind your content and paused when off screen. With reduced motion the leaves hang still.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Falling Leaves: autumn leaves tumble down a section, swinging from side
    // to side like a pendulum, tilting into each swing and flipping over as
    // they fall. Drawn on a transparent canvas behind your content, and paused
    // when off screen.
    // Usage: <section data-falling-leaves>…</section>
    // Optional: data-density (leaves per 100 × 100 px, default 0.5),
    // data-speed (default 1), data-wind (sideways drift, -1 to 1, default
    // 0.2), data-size (size of the biggest leaves in px, default 44),
    // data-colors="#c2410c,#ea580c,…".
    var DEFAULTS = {
      density: 0.5, speed: 1, wind: 0.2, size: 44,
      colors: ['#c2410c', '#ea580c', '#d97706', '#b91c1c', '#ca8a04', '#9a3412']
    };

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

    // Leaf outlines on a 64 × 64 box, stem at the bottom, tip at the top.
    var SHAPES = [
      // Maple: five pointed lobes.
      function (g) {
        g.moveTo(32, 60);
        g.lineTo(31, 50);
        g.lineTo(18, 54); g.lineTo(20, 47); g.lineTo(6, 38); g.lineTo(11, 35); g.lineTo(5, 25);
        g.lineTo(16, 27); g.lineTo(17, 21); g.lineTo(26, 29); g.lineTo(23, 12); g.lineTo(28, 14);
        g.lineTo(32, 3);
        g.lineTo(36, 14); g.lineTo(41, 12); g.lineTo(38, 29); g.lineTo(47, 21); g.lineTo(48, 27);
        g.lineTo(59, 25); g.lineTo(53, 35); g.lineTo(58, 38); g.lineTo(44, 47); g.lineTo(46, 54);
        g.lineTo(33, 50);
        g.closePath();
      },
      // Oval leaf with a pointed tip.
      function (g) {
        g.moveTo(32, 58);
        g.bezierCurveTo(12, 48, 12, 22, 32, 4);
        g.bezierCurveTo(52, 22, 52, 48, 32, 58);
        g.closePath();
      },
      // Oak: wavy rounded lobes.
      function (g) {
        g.moveTo(32, 58);
        var L = [[24, 52], [16, 48], [22, 42], [13, 36], [20, 30], [14, 22], [23, 18], [22, 9], [32, 5]];
        g.quadraticCurveTo(26, 56, 24, 52);
        for (var i = 1; i < L.length; i++) g.quadraticCurveTo(L[i - 1][0] - 6, (L[i - 1][1] + L[i][1]) / 2, L[i][0], L[i][1]);
        for (i = L.length - 2; i >= 0; i--) g.quadraticCurveTo(70 - L[i + 1][0], (L[i + 1][1] + L[i][1]) / 2, 64 - L[i][0], L[i][1]);
        g.quadraticCurveTo(38, 56, 32, 58);
        g.closePath();
      }
    ];

    function sprites(colors) {
      var out = [];
      SHAPES.forEach(function (shape) {
        colors.forEach(function (color) {
          var c = document.createElement('canvas');
          c.width = c.height = 64;
          var g = c.getContext('2d');
          g.beginPath();
          shape(g);
          g.fillStyle = color.trim();
          g.fill();
          // A soft light-to-dark sheen, then the veins and stem.
          g.globalCompositeOperation = 'source-atop';
          var sheen = g.createLinearGradient(10, 6, 54, 60);
          sheen.addColorStop(0, 'rgba(255,255,255,.22)');
          sheen.addColorStop(1, 'rgba(0,0,0,.2)');
          g.fillStyle = sheen;
          g.fillRect(0, 0, 64, 64);
          g.globalCompositeOperation = 'source-over';
          g.strokeStyle = 'rgba(60,20,0,.35)';
          g.lineWidth = 1.4;
          g.lineCap = 'round';
          g.beginPath();
          g.moveTo(32, 63); g.lineTo(32, 10);
          g.moveTo(32, 44); g.lineTo(20, 34);
          g.moveTo(32, 44); g.lineTo(44, 34);
          g.moveTo(32, 32); g.lineTo(23, 22);
          g.moveTo(32, 32); g.lineTo(41, 22);
          g.stroke();
          out.push(c);
        });
      });
      return out;
    }

    function build(el) {
      var d = el.dataset;
      var canvas = document.createElement('canvas');
      canvas.className = 'flv-canvas';
      canvas.setAttribute('aria-hidden', 'true');
      el.insertBefore(canvas, el.firstChild);
      var ctx = canvas.getContext('2d');
      var density = +d.density || DEFAULTS.density;
      var speed = d.speed !== undefined ? +d.speed : DEFAULTS.speed;
      var wind = d.wind !== undefined ? +d.wind : DEFAULTS.wind;
      var maxSize = +d.size || DEFAULTS.size;
      var img = sprites(d.colors ? d.colors.split(',') : DEFAULTS.colors);
      var R = anime.utils.random;
      var w = 0, h = 0, dpr = 1, unit = 1;
      var leaves = [];

      function leaf(f, top) {
        f.z = Math.sqrt(Math.random());           // 0 far, 1 near
        f.size = maxSize * (0.5 + f.z * 0.5) * Math.max(0.75, Math.min(1, w / 600));
        f.x = Math.random() * w;
        f.y = top ? -f.size - Math.random() * 60 : Math.random() * h;
        f.img = img[Math.floor(Math.random() * img.length)];
        f.fall = R(36, 60) * (0.6 + f.z * 0.5);  // px/s
        f.swing = R(18, 46) * (0.6 + f.z * 0.6);  // px either side
        f.freq = R(1.1, 2, 2);
        f.phase = Math.random() * 6.28;
        f.flip = R(1.5, 4.5, 2) * (Math.random() < 0.5 ? -1 : 1);   // tumble speed
        f.flipPhase = Math.random() * 6.28;
        f.base = R(-0.8, 0.8, 2);                 // resting tilt
        f.alpha = 0.65 + f.z * 0.35;
        return f;
      }

      function resize() {
        dpr = Math.min(2, window.devicePixelRatio || 1);
        w = el.clientWidth;
        h = el.clientHeight;
        canvas.width = w * dpr;
        canvas.height = h * dpr;
        unit = Math.max(0.7, h / 400);
        var n = Math.round(Math.min(90, Math.max(7, (w * h * density) / 10000)));
        while (leaves.length < n) leaves.push(leaf({}, false));
        leaves.length = n;
        // Far leaves first, so near ones pass in front.
        leaves.sort(function (a, b) { return a.z - b.z; });
      }

      function draw() {
        ctx.setTransform(1, 0, 0, 1, 0, 0);
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        for (var i = 0; i < leaves.length; i++) {
          var f = leaves[i];
          var s = Math.sin(f.phase), c = Math.cos(f.phase);
          // Pendulum: out to the side and back, tilted into the swing.
          var x = f.x + s * f.swing, rot = f.base + c * 0.7;
          // Tumbling: the leaf turns over, so it looks thinner, then shows its back.
          var flip = Math.cos(f.flipPhase);
          var sx = Math.max(0.12, Math.abs(flip));
          var cr = Math.cos(rot), sr = Math.sin(rot), k = dpr * f.size / 64;
          ctx.setTransform(cr * k * sx, sr * k * sx, -sr * k, cr * k, x * dpr, f.y * dpr);
          ctx.globalAlpha = f.alpha * (flip < 0 ? 0.82 : 1);
          ctx.drawImage(f.img, -32, -32);
        }
        ctx.globalAlpha = 1;
      }

      function advance(dt) {
        for (var i = 0; i < leaves.length; i++) {
          var f = leaves[i];
          f.phase += f.freq * dt * speed;
          f.flipPhase += f.flip * dt * speed;
          // It drops fastest at the bottom of each swing and hangs at the ends.
          var c = Math.abs(Math.cos(f.phase));
          f.y += f.fall * (0.35 + c * 1.1) * unit * dt * speed;
          f.x += wind * 40 * (0.5 + f.z) * dt * speed;
          if (f.y - f.size > h + 10) leaf(f, true);
          var m = f.swing + f.size;
          if (f.x < -m) f.x += w + m * 2; else if (f.x > w + m) f.x -= w + m * 2;
        }
      }

      resize();
      if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
        draw();
        if (window.ResizeObserver) new ResizeObserver(function () { resize(); draw(); }).observe(el);
        return;
      }
      if (window.ResizeObserver) new ResizeObserver(resize).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) / 1000 : 1 / 60;
          last = now;
          advance(dt);
          draw();
        }
      });
      // 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-falling-leaves]').forEach(function (el) {
        if (el._flv) return;
        el._flv = true;
        build(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<section data-falling-leaves>
  <h1>Autumn collection</h1>
</section>