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
- Paste the snippet just before
</head>. - Add
data-score-popupto 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-points | Points per tap. Default 10. Several, like 10|10|50, are picked at random. |
data-label | Text beside the score. Default Score. |
data-autoplay | Pretend taps land while it’s in view, resting whenever someone plays for real. |
data-color | Popup 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.
<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><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>