← Particles

Coin Burst

Shiny gold coins burst out of a button, spinning on their edges as they arc up and tumble down. Point them at a wallet or score and they swoop into it instead, which bumps and counts up as each one lands.

120 coins

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-coin-burst to a button. It bursts on every click.
  3. To send the coins somewhere, add data-target with a selector for the wallet or score. Mark the number inside it with data-coin-count to have it count up.

Options

Add any of these attributes to the same element.

data-targetA selector for an element the coins fly into. It bumps as each one lands.
data-countCoins per burst. Default 18.
data-sizeCoin size in px. Default 22.
data-spreadHow far the coins fly, in px. Default 150.
data-trigger="view"Burst once when it scrolls into view instead of on click.
data-loopBurst by itself every few seconds while in view. Handy for demos.

The coins are drawn with CSS, so there are no images to load. With reduced motion no coins fly, but a target still counts up. From code, animacious.coinBurst(element) fires a burst, e.g. when a level is won.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Coin Burst: shiny gold coins burst out of an element, spinning as they
    // arc up and tumble down, for rewards, wins and "you earned it" moments.
    // Give it a target and the coins fly into it instead, like a wallet or a
    // score, which bumps as each one lands and can count them up.
    // Usage: <button data-coin-burst>Claim reward</button>   (bursts on click)
    // Optional: data-count (coins per burst, default 18), data-size (coin size
    // in px, default 22), data-spread (how far they fly, in px, default 150),
    // data-target="#wallet" (a selector: the coins fly into that element; if it
    // holds an element with data-coin-count, its number goes up one per coin),
    // data-trigger="view" (burst once when it scrolls into view instead of on
    // click), data-loop (burst by itself every few seconds while in view).
    // Script use: animacious.coinBurst(element) fires a burst from code.
    var DEFAULTS = { count: 18, size: 22, spread: 150, every: 2600 };

    var css =
      '[data-coin-burst]{position:relative}' +
      '.cnb-coin{position:absolute;left:50%;top:50%;pointer-events:none;z-index:10;will-change:transform,opacity}' +
      '.cnb-face{display:block;width:100%;height:100%;border-radius:50%;box-sizing:border-box;' +
      'background:radial-gradient(circle at 34% 28%,#fffbe0 0,#fde68a 18%,#facc15 42%,#eab308 66%,#b45309 100%);' +
      'box-shadow:inset 0 0 0 1.5px #a16207,inset 0 0 0 3px #fde047,0 1px 2px rgba(120,53,15,.35);' +
      'animation:cnb-spin var(--cnb-spin,600ms) linear infinite}' +
      '.cnb-face::after{content:"";position:absolute;inset:26%;border-radius:50%;border:1.5px solid rgba(161,98,7,.55);' +
      'box-shadow:inset 1px 1px 0 rgba(255,255,255,.45)}' +
      '.cnb-glint{position:absolute;left:50%;top:50%;width:14px;height:14px;margin:-7px 0 0 -7px;pointer-events:none;z-index:11;' +
      'background:radial-gradient(circle,#fff 0,rgba(255,255,255,.9) 12%,rgba(253,224,71,0) 60%);' +
      'clip-path:polygon(50% 0,60% 40%,100% 50%,60% 60%,50% 100%,40% 60%,0 50%,40% 40%)}' +
      // A coin turning on its axis: it narrows to its edge and darkens, then widens again.
      '@keyframes cnb-spin{0%{transform:scaleX(1);filter:brightness(1)}25%{transform:scaleX(.08);filter:brightness(.72)}' +
      '50%{transform:scaleX(-1);filter:brightness(1.08)}75%{transform:scaleX(-.08);filter:brightness(.72)}100%{transform:scaleX(1);filter:brightness(1)}}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

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

    function bump(target) {
      anime.animate(target, { scale: [1.14, 1], duration: 380, ease: 'out(3)' });
      var counter = target.matches('[data-coin-count]') ? target : target.querySelector('[data-coin-count]');
      if (counter) {
        var n = parseInt(counter.textContent.replace(/[^\d-]/g, ''), 10) || 0;
        counter.textContent = (n + 1).toLocaleString();
      }
    }

    function burst(el) {
      var d = el.dataset;
      var target = d.target ? document.querySelector(d.target) : null;
      if (reduced()) {
        // No flying coins: the target just counts up.
        if (target) for (var k = 0; k < (+d.count || DEFAULTS.count); k++) bump(target);
        return;
      }
      var count = +d.count || DEFAULTS.count;
      var size = +d.size || DEFAULTS.size;
      var spread = +d.spread || DEFAULTS.spread;
      var R = anime.utils.random;

      // The element pops as the coins leave it.
      anime.animate(el, { scale: [{ to: 0.92, duration: 90 }, { to: 1, duration: 420, ease: 'outElastic(1, .5)' }] });

      // A couple of glints at the source.
      for (var g = 0; g < 3; g++) {
        var glint = document.createElement('span');
        glint.className = 'cnb-glint';
        el.appendChild(glint);
        anime.animate(glint, {
          x: R(-30, 30), y: R(-24, 10),
          scale: [0, R(1.2, 2, 2), 0], rotate: R(0, 90),
          duration: 520, delay: g * 90, ease: 'out(2)',
          onComplete: (function (n) { return function () { n.remove(); }; })(glint)
        });
      }

      var er = el.getBoundingClientRect();
      var tx = 0, ty = 0;
      if (target) {
        var trc = target.getBoundingClientRect();
        tx = trc.left + trc.width / 2 - (er.left + er.width / 2);
        ty = trc.top + trc.height / 2 - (er.top + er.height / 2);
      }

      for (var i = 0; i < count; i++) {
        var coin = document.createElement('span');
        coin.className = 'cnb-coin';
        coin.setAttribute('aria-hidden', 'true');
        var s = Math.round(size * R(0.75, 1.15, 2));
        coin.style.width = coin.style.height = s + 'px';
        coin.style.margin = (-s / 2) + 'px 0 0 ' + (-s / 2) + 'px';
        var face = document.createElement('span');
        face.className = 'cnb-face';
        face.style.setProperty('--cnb-spin', R(420, 900) + 'ms');
        face.style.animationDelay = -R(0, 900) + 'ms';
        coin.appendChild(face);
        el.appendChild(coin);

        // Up and out in a fan, mostly upwards.
        var a = -Math.PI / 2 + R(-1.15, 1.15, 2);
        var reach = spread * R(0.45, 1, 2);
        var px = Math.cos(a) * reach, py = Math.sin(a) * reach * 0.9 - spread * 0.15;
        var up = R(380, 520);
        var delay = i * R(10, 28);
        var done = (function (c) { return function () { c.remove(); }; })(coin);

        if (target) {
          // Arc out, hang for a beat, then swoop into the target.
          var fly = R(420, 620);
          anime.animate(coin, {
            x: [{ to: px, duration: up, ease: 'out(3)' }, { to: tx, duration: fly, delay: R(40, 160), ease: 'inOut(3)' }],
            y: [{ to: py, duration: up, ease: 'out(3)' }, { to: ty, duration: fly, delay: 0, ease: 'in(2)' }],
            scale: [{ from: 0.3, to: 1, duration: up * 0.6, ease: 'out(3)' }, { to: 0.55, duration: fly, delay: up * 0.4, ease: 'in(2)' }],
            delay: delay,
            onComplete: (function (c) { return function () { c.remove(); bump(target); }; })(coin)
          });
        } else {
          // Arc out, then fall under gravity with a little sideways drift.
          var fallDur = R(700, 1000);
          var fall = spread * R(0.9, 1.5, 2);
          anime.animate(coin, {
            x: { to: px * 1.35, duration: up + fallDur, ease: 'out(2)' },
            y: [{ to: py, duration: up, ease: 'out(2)' }, { to: py + fall, duration: fallDur, ease: 'in(2)' }],
            scale: { from: 0.3, to: 1, duration: up * 0.6, ease: 'out(3)' },
            opacity: [{ to: 1, duration: up + fallDur * 0.6 }, { to: 0, duration: fallDur * 0.4 }],
            delay: delay,
            onComplete: done
          });
        }
      }
    }

    function init() {
      var once = new IntersectionObserver(function (entries) {
        entries.forEach(function (e) {
          if (!e.isIntersecting) return;
          once.unobserve(e.target);
          burst(e.target);
        });
      }, { threshold: 0.5 });
      document.querySelectorAll('[data-coin-burst]').forEach(function (el) {
        if (el._cnb) return;
        el._cnb = true;
        var d = el.dataset;
        if (d.trigger === 'view') once.observe(el);
        else el.addEventListener('click', function () { burst(el); });
        if (d.loop === undefined || reduced()) return;
        var timer = null;
        new IntersectionObserver(function (entries) {
          clearInterval(timer);
          if (!entries[0].isIntersecting) return;
          setTimeout(function () { burst(el); }, 400);
          timer = setInterval(function () { burst(el); }, DEFAULTS.every);
        }, { threshold: 0.5 }).observe(el);
      });
    }

    window.animacious = window.animacious || {};
    window.animacious.coinBurst = function (el) { if (el) burst(el); };
    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<span id="wallet"><span data-coin-count>120</span> coins</span>
<button data-coin-burst data-target="#wallet">Claim reward</button>

<!-- or just a burst: -->
<button data-coin-burst>Spin to win</button>