← Numbers

Score Popup

Tap or click anywhere in the area and a floating “+10” pops up from that spot and drifts away, with a ring and a few sparks, while the score in the corner ticks up to its new total. Bigger rewards pop bigger, in gold.

Tap anywhere

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-score-popup to the play area and give it a height. Every tap or click inside earns points.

Options

Add any of these attributes to the same element.

data-pointsPoints per tap. Default 10. Several, like 10|10|50, are picked at random.
data-labelText beside the score. Default Score.
data-autoplayPretend taps land while it’s in view, resting whenever someone plays for real.
data-colorPopup colour. Default #6a0dad.

Elements inside with their own data-points earn that many when tapped. From a game, call animacious.scorePopup(el, x, y, 25) with a spot inside the area in pixels.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Score Popup: tap or click anywhere in the area and a floating "+10" pops
    // up from that spot and drifts away, while the score in the corner ticks up
    // to its new total with a little bump. Bigger rewards pop bigger, in gold.
    // Usage: <div data-score-popup style="height: 300px"></div>
    // Optional: data-points (points per tap, default 10; several like
    // "10|10|10|50" are picked at random), data-label (text by the score,
    // default "Score"), data-autoplay (pretend taps land while it's in view,
    // until someone taps for real), data-color (popup colour, default #6a0dad).
    // Elements inside with their own data-points earn that many when tapped.
    // Script use: animacious.scorePopup(el, x, y, points)   (x, y inside el, in px)
    var DEFAULTS = { points: '10', label: 'Score', color: '#6a0dad', gold: '#f59e0b' };

    var css =
      '.scp{--scp-color:#6a0dad;position:relative;overflow:hidden;cursor:pointer;touch-action:manipulation;user-select:none;-webkit-user-select:none;' +
      '-webkit-tap-highlight-color:transparent}' +
      '.scp-hud{position:absolute;top:0.75em;right:0.75em;z-index:3;display:flex;align-items:baseline;gap:0.45em;padding:0.4em 0.85em;border-radius:999px;' +
      'background:#fff;box-shadow:0 0.2em 0.8em rgba(15,15,30,.1);font-weight:800;font-variant-numeric:tabular-nums;pointer-events:none}' +
      '.scp-hud span{font-size:0.68em;letter-spacing:.1em;text-transform:uppercase;opacity:.5}' +
      '.scp-total{font-size:1.15em;display:inline-block;transition:color .3s}' +
      '.scp-total.scp-hot{color:var(--scp-color)}' +
      '.scp-pop{position:absolute;z-index:2;left:0;top:0;pointer-events:none;font-weight:900;font-size:1.7em;line-height:1;white-space:nowrap;letter-spacing:-0.02em;' +
      'color:var(--scp-color);text-shadow:0 0.04em 0 #fff,0 -0.04em 0 #fff,0.04em 0 0 #fff,-0.04em 0 0 #fff,0 0.12em 0.3em rgba(15,15,30,.18)}' +
      '.scp-pop.scp-big{font-size:2.4em;color:#f59e0b}' +
      '.scp-ring{position:absolute;z-index:1;left:0;top:0;width:2.6em;height:2.6em;margin:-1.3em 0 0 -1.3em;border-radius:50%;border:2px solid var(--scp-color);pointer-events:none}' +
      '.scp-spark{position:absolute;z-index:1;left:0;top:0;width:0.4em;height:0.4em;margin:-0.2em 0 0 -0.2em;border-radius:50%;background:var(--scp-color);pointer-events:none}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

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

    function make(tag, cls, parent, text) {
      var n = document.createElement(tag);
      n.className = cls;
      if (text !== undefined) n.textContent = text;
      parent.appendChild(n);
      return n;
    }

    function pick(list) { return list[Math.floor(Math.random() * list.length)]; }

    function setup(el) {
      var d = el.dataset;
      el.classList.add('scp');
      if (d.color) el.style.setProperty('--scp-color', d.color);
      var s = {
        el: el, total: 0, shown: { v: 0 }, still: reduced(),
        points: (d.points || DEFAULTS.points).split('|').map(function (x) { return parseInt(x, 10) || 10; })
      };
      s.min = Math.min.apply(null, s.points);
      var hud = make('div', 'scp-hud', el);
      make('span', '', hud, d.label !== undefined ? d.label : DEFAULTS.label);
      s.out = make('b', 'scp-total', hud, '0');
      s.out.setAttribute('aria-live', 'polite');
      el.addEventListener('pointerdown', function (e) {
        if (e.button > 0) return;
        if (e.isTrusted) { s.user = true; s.lastUser = Date.now(); }
        var r = el.getBoundingClientRect();
        var own = e.target.closest && e.target.closest('[data-points]');
        var pts = own && own !== el ? parseInt(own.getAttribute('data-points'), 10) || s.min : pick(s.points);
        earn(s, e.clientX - r.left, e.clientY - r.top, pts);
      });
      return s;
    }

    function earn(s, x, y, pts) {
      var big = pts >= s.min * 2;
      var el = s.el;
      s.total += pts;
      var pop = make('div', 'scp-pop' + (big ? ' scp-big' : ''), el, '+' + pts);
      pop.setAttribute('aria-hidden', 'true');
      // Keep the label inside the area.
      var w = pop.offsetWidth, h = pop.offsetHeight;
      var px = Math.max(w / 2 + 6, Math.min(el.clientWidth - w / 2 - 6, x));
      var py = Math.max(h + 50, y);
      if (s.still) {
        anime.utils.set(pop, { x: px - w / 2, y: py - h / 2 - 30 });
        setTimeout(function () { pop.remove(); }, 700);
      } else {
        var drift = (Math.random() - 0.5) * 30;
        anime.utils.set(pop, { x: px - w / 2, y: py - h / 2, scale: 0.3, opacity: 0 });
        anime.createTimeline({ onComplete: function () { pop.remove(); } })
          .add(pop, { scale: [0.3, big ? 1.25 : 1.1], opacity: [0, 1], duration: 220, ease: 'out(3)' })
          .add(pop, { scale: 1, duration: 260, ease: 'outBack(3)' })
          .add(pop, { y: '-=' + (big ? 70 : 54), x: '+=' + drift, duration: 900, ease: 'out(2)' }, 0)
          .add(pop, { opacity: 0, duration: 320, ease: 'in(2)' }, 680);
        burst(s, x, y, big);
      }
      tally(s);
    }

    // A ring and a few sparks where the points were earned.
    function burst(s, x, y, big) {
      var ring = make('span', 'scp-ring', s.el);
      ring.setAttribute('aria-hidden', 'true');
      anime.utils.set(ring, { x: x, y: y });
      anime.animate(ring, { scale: [0.2, big ? 2 : 1.5], opacity: [0.8, 0], duration: 600, ease: 'out(3)', onComplete: function () { ring.remove(); } });
      var n = big ? 10 : 6;
      for (var i = 0; i < n; i++) {
        var sp = make('span', 'scp-spark', s.el);
        sp.setAttribute('aria-hidden', 'true');
        if (big) sp.style.background = DEFAULTS.gold;
        var a = (i / n) * Math.PI * 2 + Math.random() * 0.5;
        var dist = (big ? 46 : 32) + Math.random() * 14;
        anime.utils.set(sp, { x: x, y: y });
        anime.animate(sp, {
          x: x + Math.cos(a) * dist, y: y + Math.sin(a) * dist, scale: [1, 0], duration: 560 + Math.random() * 200, ease: 'out(3)',
          onComplete: (function (node) { return function () { node.remove(); }; })(sp)
        });
      }
    }

    // The score counts up to its new total and gives a little bump.
    function tally(s) {
      var out = s.out;
      if (s.still) { out.textContent = s.total.toLocaleString(); return; }
      if (s.counter) s.counter.pause();
      s.counter = anime.animate(s.shown, {
        v: s.total, duration: 650, ease: 'out(3)', delay: 120,
        onUpdate: function () { out.textContent = Math.round(s.shown.v).toLocaleString(); }
      });
      out.classList.add('scp-hot');
      clearTimeout(s.cool);
      s.cool = setTimeout(function () { out.classList.remove('scp-hot'); }, 700);
      anime.animate(out, { scale: [1.35, 1], duration: 500, delay: 120, ease: 'outElastic(1, .5)' });
    }

    // Pretend taps at random spots, resting while a real user plays and for a
    // few seconds after.
    function autoplay(s) {
      (function next() {
        setTimeout(function () {
          if (s.visible && !(s.lastUser && Date.now() - s.lastUser < 5000)) {
            var w = s.el.clientWidth, h = s.el.clientHeight;
            earn(s, w * (0.15 + Math.random() * 0.7), h * (0.35 + Math.random() * 0.5), pick(s.points));
          }
          next();
        }, 650 + Math.random() * 500);
      })();
    }

    function init() {
      var io = new IntersectionObserver(function (entries) {
        entries.forEach(function (e) {
          var s = e.target._scp;
          s.visible = e.isIntersecting;
          if (s.visible && s.auto && !s.autoOn) { s.autoOn = true; autoplay(s); }
        });
      }, { threshold: 0.2 });
      document.querySelectorAll('[data-score-popup]').forEach(function (el) {
        if (el._scp) return;
        var s = el._scp = setup(el);
        s.auto = el.dataset.autoplay !== undefined && el.dataset.autoplay !== 'false';
        io.observe(el);
      });
    }

    window.animacious = window.animacious || {};
    window.animacious.scorePopup = function (el, x, y, points) {
      if (el._scp) earn(el._scp, x, y, points || el._scp.min);
    };

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<div data-score-popup style="height: 320px"></div>

<!-- targets worth different amounts: -->
<div data-score-popup style="height: 320px">
  <button data-points="50">Gem</button>
</div>