← Particles

Fireflies

Soft glowing dots wander lazily about a section, each one swelling into light and dimming again on its own slow rhythm, like a summer evening. They drift over to the pointer, curious.

Midsummer

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-fireflies to a section with a dark background.

Options

Add any of these attributes to the same element.

data-countNumber of fireflies. Default about one per 100 × 100 px.
data-colorGlow colour. Default #d9f99d.
data-speedSpeed multiplier. Default 1.
data-follow="false"Ignore the pointer.

It’s drawn on a transparent canvas behind your content and paused when off screen. With reduced motion they glow, but stay put.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Fireflies: soft glowing dots wander lazily about a section, each
    // glowing up and dimming on its own slow rhythm. They drift towards the
    // pointer, curious. Drawn on a transparent canvas behind your content, and
    // paused when off screen.
    // Usage: <section data-fireflies style="background:#0b1d17">…</section>
    // Optional: data-count (default: about 1 per 100 × 100 px, 12 to 120),
    // data-color (default warm yellow-green #d9f99d), data-speed (default 1),
    // data-follow="false" (ignore the pointer).
    var DEFAULTS = { color: '#d9f99d', speed: 1 };

    var css =
      '[data-fireflies]{position:relative;isolation:isolate;overflow:hidden}' +
      '.ffl-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);

    // A pre-drawn glow: a bright core inside a wide soft halo.
    function sprite(color) {
      var S = 96, c = document.createElement('canvas');
      c.width = c.height = S;
      var g = c.getContext('2d');
      g.fillStyle = color;
      g.fillRect(0, 0, S, S);
      g.globalCompositeOperation = 'destination-in';
      var m = g.createRadialGradient(S / 2, S / 2, 0, S / 2, S / 2, S / 2);
      m.addColorStop(0, '#000');
      m.addColorStop(0.09, '#000');
      m.addColorStop(0.22, 'rgba(0,0,0,.55)');
      m.addColorStop(0.5, 'rgba(0,0,0,.18)');
      m.addColorStop(1, 'rgba(0,0,0,0)');
      g.fillStyle = m;
      g.fillRect(0, 0, S, S);
      // A white-hot centre.
      g.globalCompositeOperation = 'source-over';
      var core = g.createRadialGradient(S / 2, S / 2, 0, S / 2, S / 2, S * 0.06);
      core.addColorStop(0, 'rgba(255,255,255,.95)');
      core.addColorStop(1, 'rgba(255,255,255,0)');
      g.fillStyle = core;
      g.fillRect(0, 0, S, S);
      return c;
    }

    function build(el) {
      var d = el.dataset;
      var canvas = document.createElement('canvas');
      canvas.className = 'ffl-canvas';
      canvas.setAttribute('aria-hidden', 'true');
      el.insertBefore(canvas, el.firstChild);
      var ctx = canvas.getContext('2d');
      var img = sprite(d.color || DEFAULTS.color);
      var speed = d.speed !== undefined ? +d.speed : DEFAULTS.speed;
      var follow = d.follow !== 'false';
      var R = anime.utils.random;
      var w = 0, h = 0, dpr = 1, t = 0;
      var flies = [], pointer = null;

      function fly() {
        return {
          x: Math.random() * w, y: Math.random() * h,
          a: Math.random() * Math.PI * 2,     // heading
          turn: 0,
          v: R(0.012, 0.03, 3),               // px per ms
          size: R(22, 44),
          // Glow cycle: how long it takes and where in it this one starts.
          period: R(2200, 5200),
          offset: Math.random(),
          depth: R(0.5, 1, 2)
        };
      }

      function resize() {
        dpr = Math.min(2, window.devicePixelRatio || 1);
        w = el.clientWidth;
        h = el.clientHeight;
        canvas.width = w * dpr;
        canvas.height = h * dpr;
        var n = +d.count || Math.round(Math.min(120, Math.max(12, (w * h) / 10000)));
        while (flies.length < n) flies.push(fly());
        flies.length = n;
      }

      // Brightness over a glow cycle: a quick swell, a slow fade, a dim rest.
      function glow(f) {
        var k = ((t / f.period + f.offset) % 1);
        if (k < 0.15) return 0.15 + 0.85 * Math.sin((k / 0.15) * Math.PI / 2);
        if (k < 0.55) return 0.15 + 0.85 * Math.pow(1 - (k - 0.15) / 0.4, 2);
        return 0.15;
      }

      function draw() {
        ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
        ctx.clearRect(0, 0, w, h);
        ctx.globalCompositeOperation = 'lighter';
        for (var i = 0; i < flies.length; i++) {
          var f = flies[i], b = glow(f), s = f.size * (0.55 + b * 0.6) * f.depth;
          ctx.globalAlpha = b * f.depth;
          ctx.drawImage(img, f.x - s, f.y - s, s * 2, s * 2);
        }
        ctx.globalCompositeOperation = 'source-over';
        ctx.globalAlpha = 1;
      }

      function advance(dt) {
        t += dt;
        for (var i = 0; i < flies.length; i++) {
          var f = flies[i];
          // Wander: the heading drifts with a slowly changing turn.
          f.turn += R(-1, 1, 3) * 0.0006 * dt;
          f.turn *= 0.97;
          f.a += f.turn * dt * 0.05;
          if (pointer && Math.abs(pointer.x - f.x) + Math.abs(pointer.y - f.y) < 320) {
            // Lean the heading towards the pointer, if it's near.
            var want = Math.atan2(pointer.y - f.y, pointer.x - f.x);
            var diff = Math.atan2(Math.sin(want - f.a), Math.cos(want - f.a));
            f.a += diff * 0.0015 * dt;
          }
          // Steer back in from the edges.
          var m = 20;
          if (f.x < m || f.x > w - m || f.y < m || f.y > h - m) {
            var home = Math.atan2(h / 2 - f.y, w / 2 - f.x);
            f.a += Math.atan2(Math.sin(home - f.a), Math.cos(home - f.a)) * 0.004 * dt;
          }
          f.x += Math.cos(f.a) * f.v * dt * speed;
          f.y += Math.sin(f.a) * f.v * dt * speed;
        }
      }

      resize();
      if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
        // A still night: every firefly mid-glow.
        t = 600;
        flies.forEach(function (f) { f.offset = Math.random() * 0.3; });
        draw();
        if (window.ResizeObserver) new ResizeObserver(function () { resize(); draw(); }).observe(el);
        return;
      }
      if (window.ResizeObserver) new ResizeObserver(resize).observe(el);

      if (follow) {
        el.addEventListener('pointermove', function (e) {
          var r = el.getBoundingClientRect();
          pointer = { x: e.clientX - r.left, y: e.clientY - r.top };
        });
        el.addEventListener('pointerleave', function () { pointer = null; });
      }

      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;
          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-fireflies]').forEach(function (el) {
        if (el._ffl) return;
        el._ffl = true;
        build(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<section data-fireflies style="background: #0b1d17; color: #fff">
  <h1>Midsummer</h1>
</section>