Dice Roll
A 3D die tumbles in from the side, bouncing lower each time and spinning end over end, then settles on a number. Loop it and it rolls away and comes back with a new number; click or tap to roll again. Roll two dice and show the total.
How to use it
- Paste the snippet just before
</head>. - Add
data-dice-rollto an empty element for a random roll, ordata-dice-roll="6"to land on 6. Size it withfont-size; each die is 2.6em.
Options
Add any of these attributes to the same element.
data-dice | How many dice. Default 1. |
data-dice-roll="3,4|6,6" | Fixed results, taken in turn: dice separated by commas, rolls by |. |
data-loop | Roll again every few seconds. |
data-hold | How long each result stays. Default 2200 ms. |
data-duration | How long a roll takes. Default 1500 ms. |
data-total | Show the result (or the sum of the dice) underneath. |
data-color / data-pip | Die and pip colours. Default white and #1c1c24. |
The die takes the full width of its container so it has room to roll in. Click or tap it to roll.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Dice Roll: a 3D die tumbles in from the side, bouncing and spinning, and
// lands showing a number. Loop it and it rolls away and comes back with a
// new number; click or tap to roll again.
// Usage: <div data-dice-roll></div> (a random number each roll)
// <div data-dice-roll="6"></div> (always lands on 6)
// Optional: data-dice (how many dice, default 1), data-dice-roll="3,4|6,6"
// (fixed results: dice separated by commas, rolls by |, taken in turn),
// data-loop (roll again every few seconds), data-hold (ms each result
// stays, default 2200), data-duration (ms a roll takes, default 1500),
// data-total (show the total under the dice), data-color (die colour,
// default white), data-pip (pip colour, default #1c1c24).
// Size it with font-size: each die is 2.6em, the whole thing 100% wide.
var DEFAULTS = { dice: 1, hold: 2200, duration: 1500, color: '#ffffff', pip: '#1c1c24' };
var PIPS = { 1: [4], 2: [2, 6], 3: [2, 4, 6], 4: [0, 2, 6, 8], 5: [0, 2, 4, 6, 8], 6: [0, 2, 3, 5, 6, 8] };
// Where each value sits on the cube (rotation of its face) and the cube
// rotation that turns that face to the front.
var FACE = { 1: 'rotateY(0deg)', 6: 'rotateY(180deg)', 3: 'rotateY(90deg)', 4: 'rotateY(-90deg)', 2: 'rotateX(90deg)', 5: 'rotateX(-90deg)' };
var SHOW = { 1: [0, 0], 6: [0, 180], 3: [0, -90], 4: [0, 90], 2: [-90, 0], 5: [90, 0] };
var css =
'.dcr{--dcr-s:2.6em;position:relative;width:100%;overflow:hidden;cursor:pointer;-webkit-tap-highlight-color:transparent;user-select:none}' +
'.dcr-floor{position:relative;height:calc(var(--dcr-s) * 2.5)}' +
'.dcr-die{position:absolute;left:50%;bottom:calc(var(--dcr-s) * .35);width:var(--dcr-s);height:var(--dcr-s);margin-left:calc(var(--dcr-s) * -.5);perspective:calc(var(--dcr-s) * 8)}' +
'.dcr-shadow{position:absolute;left:-10%;right:-10%;bottom:calc(var(--dcr-s) * -.22);height:calc(var(--dcr-s) * .3);border-radius:50%;background:radial-gradient(closest-side,rgba(20,10,40,.32),rgba(20,10,40,0))}' +
'.dcr-view{position:absolute;inset:0;transform-style:preserve-3d;transform:rotateX(-20deg) rotateY(24deg)}' +
'.dcr-cube{position:absolute;inset:0;transform-style:preserve-3d}' +
'.dcr-face,.dcr-core{position:absolute;inset:0;border-radius:18%;backface-visibility:hidden}' +
'.dcr-face{display:grid;grid-template:repeat(3,1fr)/repeat(3,1fr);padding:14%;box-sizing:border-box;background:var(--dcr-c);' +
'box-shadow:inset 0 0 0 calc(var(--dcr-s) * .02) rgba(0,0,0,.08),inset 0 calc(var(--dcr-s) * -.08) calc(var(--dcr-s) * .16) rgba(0,0,0,.12)}' +
'.dcr-core{backface-visibility:visible;border-radius:4%;background:var(--dcr-c);filter:brightness(.82)}' +
'.dcr-face i{display:block;width:72%;height:72%;margin:auto;border-radius:50%;background:var(--dcr-p);box-shadow:inset 0 .06em .08em rgba(0,0,0,.45)}' +
'.dcr-face[data-v="1"] i{width:100%;height:100%}' +
'.dcr-total{min-height:1.5em;padding-bottom:.15em;line-height:1.3;text-align:center;font-weight:700;font-size:.62em;letter-spacing:-.01em;font-variant-numeric:tabular-nums;color:inherit}' +
'.dcr-total b{display:inline-block;font-size:1.6em;font-weight:900;line-height:1;vertical-align:-.12em}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function rand(a, b) { return a + Math.random() * (b - a); }
function faceHtml(v, tf) {
var cells = '';
for (var i = 0; i < 9; i++) cells += PIPS[v].indexOf(i) >= 0 ? '<i></i>' : '<span></span>';
return '<div class="dcr-face" data-v="' + v + '" style="transform:' + tf + ' translateZ(calc(var(--dcr-s) * .5))">' + cells + '</div>';
}
function setup(el) {
var d = el.dataset, attr = el.getAttribute('data-dice-roll') || '';
var rounds = attr.split('|').map(function (r) { return r.split(/[,\s]+/).filter(Boolean).map(Number); }).filter(function (r) { return r.length; });
var D = {
el: el, rounds: rounds, round: 0, count: +d.dice || (rounds[0] ? rounds[0].length : DEFAULTS.dice),
hold: +d.hold || DEFAULTS.hold, duration: +d.duration || DEFAULTS.duration, total: d.total !== undefined,
loop: d.loop !== undefined && d.loop !== 'false', still: reduced(), visible: false, rolling: false, timer: 0, dice: []
};
el.classList.add('dcr');
el.style.setProperty('--dcr-c', d.color || DEFAULTS.color);
el.style.setProperty('--dcr-p', d.pip || DEFAULTS.pip);
var faces = '', v;
for (v = 1; v <= 6; v++) faces += faceHtml(v, FACE[v]);
var core = '<div class="dcr-core" style="transform:scale(.97)"></div><div class="dcr-core" style="transform:rotateX(90deg) scale(.97)"></div><div class="dcr-core" style="transform:rotateY(90deg) scale(.97)"></div>';
var html = '<div class="dcr-floor" aria-hidden="true">';
for (var k = 0; k < D.count; k++) html += '<div class="dcr-die"><div class="dcr-shadow"></div><div class="dcr-view"><div class="dcr-cube">' + core + faces + '</div></div></div>';
el.innerHTML = html + '</div>' + (D.total ? '<div class="dcr-total" aria-hidden="true"></div>' : '');
D.totalEl = el.querySelector('.dcr-total');
el.setAttribute('role', 'img');
[].forEach.call(el.querySelectorAll('.dcr-die'), function (node, i) {
D.dice.push({ node: node, cube: node.querySelector('.dcr-cube'), shadow: node.querySelector('.dcr-shadow'), slot: (i - (D.count - 1) / 2) * 1.7, value: 1 });
});
el.addEventListener('click', function () { if (!D.rolling) { clearTimeout(D.timer); exit(D); } });
return D;
}
function nextValues(D) {
var r = D.rounds.length ? D.rounds[D.round++ % D.rounds.length] : [];
return D.dice.map(function (die, i) { return Math.max(1, Math.min(6, r[i] || Math.floor(rand(1, 7)))); });
}
function label(D, vals) {
var sum = vals.reduce(function (a, b) { return a + b; }, 0);
D.el.setAttribute('aria-label', (vals.length > 1 ? 'Dice rolled ' + vals.join(' and ') + ', total ' : 'Die rolled ') + sum);
if (D.totalEl) D.totalEl.innerHTML = vals.length > 1 ? vals.join(' + ') + ' = <b>' + sum + '</b>' : 'You rolled <b>' + sum + '</b>';
}
function pose(die, x, lift, rx, ry, rz) {
die.node.style.transform = 'translate(' + x.toFixed(1) + 'px,' + (-lift).toFixed(1) + 'px)';
die.cube.style.transform = 'rotateZ(' + rz.toFixed(1) + 'deg) rotateX(' + rx.toFixed(1) + 'deg) rotateY(' + ry.toFixed(1) + 'deg)';
var h = Math.min(1, lift / (die.node.offsetHeight * 1.2 || 1));
die.shadow.style.transform = 'translateY(' + lift.toFixed(1) + 'px) scale(' + (1 - h * 0.45).toFixed(3) + ')';
die.shadow.style.opacity = (1 - h * 0.6).toFixed(3);
}
function roll(D) {
if (!D.visible) return;
D.rolling = true;
var vals = nextValues(D), size = D.dice[0].node.offsetWidth, half = D.el.clientWidth / 2, done = 0;
if (D.totalEl) anime.animate(D.totalEl, { opacity: 0, duration: 150 });
D.dice.forEach(function (die, i) {
var v = vals[i], end = SHOW[v], fx = die.slot * size;
var sx = -half - size * 1.5 - fx, rx0 = end[0] - 720 - Math.round(rand(0, 1)) * 90, ry0 = end[1] - 360 - Math.round(rand(0, 2)) * 90;
var rz0 = -540 + rand(-60, 60), rzT = rand(-10, 10), bounces = rand(2.6, 3.4), H = size * rand(0.8, 1.05);
var st = { t: 0 };
pose(die, sx, 0, rx0, ry0, rz0);
die.anim = anime.animate(st, {
t: 1, duration: D.duration + i * 160, delay: i * 120, ease: 'linear',
onUpdate: function () {
var t = st.t, e = 1 - Math.pow(1 - t, 3);
var lift = Math.abs(Math.sin(t * Math.PI * bounces)) * Math.pow(1 - t, 1.6) * H;
pose(die, sx + (fx - sx) * e, lift, rx0 + (end[0] - rx0) * e, ry0 + (end[1] - ry0) * e, rz0 + (rzT - rz0) * e);
},
onComplete: function () {
die.rest = { x: fx, rx: end[0], ry: end[1], rz: rzT };
pose(die, fx, 0, end[0], end[1], rzT);
die.value = v;
if (++done === D.dice.length) landed(D, vals);
}
});
});
}
function landed(D, vals) {
D.rolling = false;
label(D, vals);
if (D.totalEl) {
anime.animate(D.totalEl, { opacity: [0, 1], duration: 250 });
var b = D.totalEl.querySelector('b');
if (b) anime.animate(b, { scale: [0.4, 1], duration: 500, ease: 'outBack(3)' });
}
if (D.loop) D.timer = setTimeout(function () { exit(D); }, D.hold);
}
// Rolls off to the right before the next roll comes in from the left.
function exit(D) {
if (!D.visible) return;
if (!D.dice[0].rest) { roll(D); return; }
D.rolling = true;
var size = D.dice[0].node.offsetWidth, far = D.el.clientWidth / 2 + size * 2, n = 0;
D.dice.forEach(function (die) {
var r = die.rest, st = { t: 0 };
anime.animate(st, {
t: 1, duration: 520, ease: 'in(2)',
onUpdate: function () { pose(die, r.x + (far - r.x + size) * st.t, Math.sin(st.t * Math.PI) * size * 0.4, r.rx, r.ry + 200 * st.t, r.rz + 320 * st.t); },
onComplete: function () { if (++n === D.dice.length) roll(D); }
});
});
}
function init() {
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
var D = e.target._dcr;
D.visible = e.isIntersecting;
if (D.visible && !D.rolling) { clearTimeout(D.timer); D.timer = setTimeout(function () { exit(D); }, D.dice[0].rest ? 600 : 200); }
if (!D.visible) clearTimeout(D.timer);
});
}, { threshold: 0.25 });
document.querySelectorAll('[data-dice-roll]').forEach(function (el) {
if (el._dcr) return;
var D = (el._dcr = setup(el));
if (D.still) {
var vals = nextValues(D);
D.dice.forEach(function (die, i) { var s = SHOW[vals[i]]; pose(die, die.slot * die.node.offsetWidth, 0, s[0], s[1], 0); });
label(D, vals);
return;
}
D.dice.forEach(function (die) { pose(die, -9999, 0, 0, 0, 0); });
io.observe(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div data-dice-roll data-total></div>
<!-- two red dice, rolling every few seconds: -->
<div data-dice-roll data-dice="2" data-total data-loop data-color="#e11d48" data-pip="#fff" style="font-size: 32px"></div>