GODRICH
#css-animation#button#loader#transition#scroll#dashboard#ui#design-trends

Dice Roll Animation: 9 Scenes with Top-Face Math

Throw, bounce, settle — one roll is just three verbs, and scene 03 reads the top-face number out of the last one by calculation.

See All Nine Dice Scenes Roll Right on This Page

Each canvas in the grid rolls by itself, and every one has a roll button for throwing again. Eight scenes draw on a canvas with no library, and one, scene 08, is plain DOM and CSS.

Auto-plays · click a tile to jump to its section · all nine in one zip

Dice Roll Animation: 9 Scenes Where One Rotation Matrix Decides the Top Face

A die's orientation is one 3×3 rotation matrix R, and every scene except 08 runs on top of it. Scene 01 keeps turning the matrix in mid air and hides the faces turned away from the camera, 02 adds gravity and floor impacts, and 03 slowly stands the matrix up so the winning face points straight up. Scene 04 draws each throw from a seeded sequence, and 05 adds up two dice.

Scene 06 leaves dust and marks where the die lands, 07 adds a noise offset to the matrix so the dice rattle in a cup, 08 fakes the whole thing with one squeezed flat face, and 09 puts three dice on a felt table. The browser has no built-in 3D die, so this order goes from the bare projection to a scene you could put behind a hero headline.

If you want the same idea as a dice roll animation gif, remember that a gif plays one fixed loop forever, while here the top face is calculated again for every throw. For the neighbours of this set, see the bouncing ball patterns with physics and the spinning top canvas scenes.

Code for Each Dice Roll Technique

Every snippet below is an unbroken run of lines copied from that scene's index.html, chosen where the scene does something the others do not. The complete files are in the zip.

013D Dice Turned by a Rotation Matrix

The die floats on a sine and cosine orbit while its eight corners are multiplied by R and projected with a focal length of 275, and any face whose normal points away from the camera is skipped. DEMO_KICK_MS gives it a new axis and speed every 1.5 seconds, which makes this a calm opening frame for a dice scene.

3D Dice Turned by a Rotation Matrix
Open in a new tab

01 · 3D Dice Turned by a Rotation Matrix — Measured area 13.776% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

rotAxismatVectopIndexSPIN_BASEDEMO_KICK_MS
  function matVec(m, v) {
    return [m[0] * v[0] + m[1] * v[1] + m[2] * v[2], m[3] * v[0] + m[4] * v[1] + m[5] * v[2], m[6] * v[0] + m[7] * v[1] + m[8] * v[2]];
  }
  // Rodrigues formula: rotation matrix for turning by ang around the unit axis ax
  function rotAxis(ax, ang) {
    var c = Math.cos(ang), s = Math.sin(ang), t = 1 - c, x = ax[0], y = ax[1], z = ax[2];
    return [t * x * x + c, t * x * y - s * z, t * x * z + s * y,
      t * x * y + s * z, t * y * y + c, t * y * z - s * x,
      t * x * z - s * y, t * y * z + s * x, t * z * z + c];
  }

Measured: 13.8% accumulated area, intensity 72.4, moved in 23/23 frames.

02Dice Thrown on an Arc That Bounce on the Floor

Gravity bends the throw into an arc, and at each floor hit the speed of the lowest corner is multiplied by DICE_RESTITUTION, so the bounce apex shrinks with a shadow and a dotted trail to show it. It suits the moment of throwing, where the feel of the hit matters more than the result.

Dice Thrown on an Arc That Bounce on the Floor
Open in a new tab

02 · Dice Thrown on an Arc That Bounce on the Floor — Measured area 23.341% · moving frames 21/23Original by GODRICH · SCSS and React ship in the zip · See the password

GRAVITYDICE_RESTITUTIONfloorStepwallStepINERTIA
    var low = d.p[1] + my;
    if (low >= 0) return;
    d.p[1] -= low;
    var cvx = d.v[0] + d.w[1] * rz - d.w[2] * ry;
    var cvy = d.v[1] + d.w[2] * rx - d.w[0] * rz;
    var cvz = d.v[2] + d.w[0] * ry - d.w[1] * rx;
    if (cvy >= 0) return;
    var j = -(1 + DICE_RESTITUTION) * cvy / (1 + (rx * rx + rz * rz) / INERTIA);
    d.v[1] += j;
    d.w[0] += -rz * j / INERTIA; d.w[2] += rx * j / INERTIA;

Measured: 23.3% area, intensity 61.8, moved in 21/23 frames.

03Dice That Read Their Top Face as They Settle

When the spin slows, the die picks the face whose normal has the largest y value and treats its pip count as the result, then turns a fixed share of the remaining angle each step until that face is exactly on top. Use it when the result has to lock in with a visible click, and the six bars under the die show every face's y value while it happens.

Dice That Read Their Top Face as They Settle
Open in a new tab

03 · Dice That Read Their Top Face as They Settle — Measured area 13.378% · moving frames 18/23Original by GODRICH · SCSS and React ship in the zip · See the password

settleStepSETTLE_RATESETTLE_AFTER_MSreadNormalstopIndex
  // Settle: read the face with the largest normal y as the top, then turn by SETTLE_RATE of the remaining angle each step until that face points straight up
  function settleStep(d) {
    var k = topIndex(d);
    d.top = FACE_VAL[k];
    var nw = matVec(d.R, FACE_N[k]);
    var ang = Math.acos(clamp(nw[1], -1, 1));
    if (ang > 1e-4) {
      var ax = unit(cross(nw, [0, 1, 0]));
      d.R = orthonormalize(matMul(rotAxis(ax, ang * SETTLE_RATE), d.R));
    }

Measured: 13.4% area, intensity 65.7, moved in 18/23 frames.

04Dice That Repeat the Same Sequence from a Seed

A linear congruential generator decides each throw's start spin and speed, so one seed always gives the same faces in the same order. The scene hides six throws before it starts and lays them in a top row, then compares them with the row you are rolling now, and a chip with a matching face gets a mint border.

Dice That Repeat the Same Sequence from a Seed
Open in a new tab

04 · Dice That Repeat the Same Sequence from a Seed — Measured area 18.185% · moving frames 19/23Original by GODRICH · SCSS and React ship in the zip · See the password

seedRandDICE_SEEDSEQ_LENseqThrowrunUntilHold
  function seqThrow(d, rnd) {
    launch(d, rnd, -2.8, rnd() * 2.4 - 0.8, 3.2, 0.1, 3.0, 14);
  }
  function sceneInit() {
    var r2 = seedRand(DICE_SEED);
    var tmp = newDie(2, -3.0, 3.0, -1.6, 2.4);
    for (var k = 0; k < SEQ_LEN; k += 1) { seqThrow(tmp, r2); prevRun.push(runUntilHold(tmp)); }
    die.phase = PH_IDLE;
  }
  function sceneRoll(manual) {
    if (throwNo % SEQ_LEN === 0) { rng = seedRand(DICE_SEED); curRun = []; }
    throwNo += 1;
    seqThrow(die, rng);
    setStatus(T_ROLLING, '', true);
  }

Measured: 18.2% area, intensity 66.1, moved in 19/23 frames.

05Two Dice Whose Sum Stacks into Bars

Two dice are thrown from opposite sides, their top faces are added, and one count is stacked onto the bars from 2 to 12 that 24 hidden rolls already built. Because the middle sums can be reached in more ways than 2 or 12, the middle bars grow tallest, which suits a page that explains sums.

Two Dice Whose Sum Stacks into Bars
Open in a new tab

05 · Two Dice Whose Sum Stacks into Bars — Measured area 20.543% · moving frames 20/23Original by GODRICH · SCSS and React ship in the zip · See the password

PRELOAD_ROLLSthrowBothcountsbarHrunUntilHold
  function throwBoth(a, b, rnd) {
    launch(a, rnd, -3.0, rnd() * 2.2 - 0.6, 3.0, 0.2, 3.0, 14);
    launch(b, rnd, 3.0, rnd() * 2.2 - 0.6, -3.0, 0.2, 3.0, 14);
  }
  function sceneInit() {
    var ta = newDie(0, -3.4, -0.6, -1.6, 2.4);
    var tb = newDie(3, 0.6, 3.4, -1.6, 2.4);
    for (var k = 0; k < PRELOAD_ROLLS; k += 1) {
      throwBoth(ta, tb, rng);
      counts[runUntilHold(ta) + runUntilHold(tb)] += 1;
    }
    for (var s = 2; s <= 12; s += 1) barH[s] = counts[s];
    dieA.phase = PH_IDLE; dieB.phase = PH_IDLE;
  }

Measured: 20.5% area, intensity 69.3, moved in 20/23 frames.

06Dice That Leave Roll Marks and Dust on the Floor

Every floor contact scatters between 7 and 17 dust specks, and the path of the shadow point stays on the floor as a pale violet line for 3.6 seconds. The marks are kept until the next throw and fade slowly, so a roll leaves a trace you can still read after the die has stopped.

Dice That Leave Roll Marks and Dust on the Floor
Open in a new tab

06 · Dice That Leave Roll Marks and Dust on the Floor — Measured area 18.76% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

DUST_FADE_MSspawnDustdustRngFLOOR_FRICTION
  function spawnDust(x, z, speed) {
    var n = 7 + Math.min(10, Math.round(speed * 1.1));
    for (var i = 0; i < n; i += 1) {
      var an = dustRng() * 6.2832, sp = 0.6 + dustRng() * 1.8;
      dust.push({ x: x, y: 0.05, z: z, vx: Math.cos(an) * sp, vy: 0.8 + dustRng() * 1.8, vz: Math.sin(an) * sp, born: elapsed, life: 900 + dustRng() * 700, size: 0.07 + dustRng() * 0.11 });
    }
    marks.push([x, z, elapsed, 0.3 + Math.min(0.5, speed * 0.04)]);
    if (marks.length > 24) marks.shift();
  }

Measured: 18.8% area, intensity 52.5, moved in 23/23 frames.

07Dice Shaken Inside a Cup

A one-dimensional value noise reads a table of 256 seeded numbers with smooth interpolation, and the result is added to the cup and to the dice position and angle, so the rattle never repeats a simple loop. Once the shake has grown for 1.3 seconds the dice are shot out of the cup, and the cup's front wall is drawn after the dice so it hides their lower half.

Dice Shaken Inside a Cup
Open in a new tab

07 · Dice Shaken Inside a Cup — Measured area 24.347% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

NOISE_FREQnoise1SHAKE_MSshakePosedrawCupFront
  function shakePose() {
    var t = elapsed / 1000 * NOISE_FREQ;
    var I = clamp(shakeT / SHAKE_MS, 0, 1);
    ampNow = 0.1 + 0.9 * I * I;
    cupDX = noise1(t + 31) * ampNow * 0.5;
    cupLift = Math.abs(noise1(t * 1.2 + 53)) * ampNow * 0.35;
    var cx = CUP_X + cupDX;
    die.p = [cx + noise1(t * 1.3 + 70) * ampNow * 0.7, 1.3 + cupLift + Math.abs(noise1(t * 1.1 + 110)) * ampNow * 0.9, CUP_Z + noise1(t * 1.2 + 150) * ampNow * 0.4];
    die.R = matMul(rotAxis([0, 1, 0], noise1(t * 1.4 + 190) * ampNow), matMul(rotAxis([1, 0, 0], noise1(t * 1.5 + 230) * ampNow * 0.6), baseR));
  }

Measured: 24.3% area, intensity 68.4, moved in 23/23 frames.

08Flat 2D Dice Rolling by Face Swaps

This is the dice roll animation 2d version: no matrix at all, only a face squeezed to 0.18 of its width and widened again while the pip layer is swapped at the flattest moment. The floor stripes slide one tile each hop, and the button plays four hops of 360 ms with the Web Animations API, so it works as a light flourish on a button.

Flat 2D Dice Rolling by Face Swaps
Open in a new tab

08 · Flat 2D Dice Rolling by Face Swaps — Measured area 15.748% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

HOP_MSHOP_COUNTTILE_PXpickFacesseedRand
  function pickFaces() {
    var seq = [];
    var prev = Number(dieEl.getAttribute('data-face')) || 2;
    for (var k = 0; k < HOP_COUNT; k += 1) {
      var f = 1 + Math.floor(rng() * 6);
      if (f === prev) f = (f % 6) + 1;
      seq.push(f);
      prev = f;
    }
    return seq;
  }

Measured: 15.7% area, intensity 48.1, moved in 23/23 frames.

09Hero Background Dice Table

Three dice, each with its own seed, take turns being thrown over a felt table and stay inside three separate lanes, then each result face is written into a chip row under the table. Because the scene never loops visibly, it works as a landing page background that keeps changing.

Hero Background Dice Table
Open in a new tab

09 · Hero Background Dice Table — Measured area 23.037% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

DIE_COUNTLANESthrowOneboardDICE_SEED
  function throwOne(k) {
    var d = dice[k];
    var lane = LANES[k];
    launch(d, rngs[k], (lane[0] + lane[1]) / 2 - 0.4, rngs[k]() * 2.2 - 1.2, 0.6, 0.2, 2.8, 14);
    tossCount[k] += 1;
  }
  function sceneInit() {
    for (var k = 0; k < DIE_COUNT; k += 1) {
      dice.push(newDie(k, LANES[k][0], LANES[k][1], -1.6, 2.2));
      rngs.push(seedRand(DICE_SEED + 100 * (k + 1)));
    }
  }

Measured: 23.0% area, intensity 69.4, moved in 23/23 frames.

Where the Values Change the Scene — Restitution, Spin Damping, Seed

Three constants change the character of the scene the most. DICE_RESTITUTION in 02 sets how much speed survives a floor hit, SPIN_DAMPING in 03 sets how much spin each step removes, and DICE_SEED in 04 reorders the whole sequence of throws. A higher restitution keeps the height up longer, but within the two-second cap the count of strong bounces can even drop from 13 to 7, so read this value as height and not as a bounce count.

The table collects all nine measurements. Scene 07 covers the most area because the cup and both dice shake together, scene 03 covers the least because one die is thrown gently, and no scene falls under ten percent.

Item Cumulative area Avg. intensity Moved frames
01 Rotation-matrix 3D die 13.8% 72.4 23/23
02 Arc throw and bounce 23.3% 61.8 21/23
03 Top-face reading 13.4% 65.7 18/23
04 Seeded sequence 18.2% 66.1 19/23
05 Two-dice sum bars 20.5% 69.3 20/23
06 Dust and roll marks 18.8% 52.5 23/23
07 Cup shake noise 24.3% 68.4 23/23
08 Flat 2D die 15.7% 48.1 23/23
09 Hero table 23.0% 69.4 23/23

Where It Breaks — Resting on an Edge, Narrow Screens, Overlapping Dice

The riskiest moment is a die balanced on an edge. Reading the top face is a maximum over normal y, so in a diagonal pose where two faces have almost the same y the choice can flip within one step, and the code forces the settle with SETTLE_AFTER_MS or a bounce count. Each correction step turns only a share (SETTLE_RATE, 0.16) of the remaining angle, but the face it aims at can change between steps, so the direction the die lies down in may look unsteady. In 200 throws none failed to stop, with an average of 110 steps (about 1.76 seconds), a longest run of 157 steps, and 8 warm-up steps before the first frame.

On a narrow screen the canvas shrinks with its width and only the status line, button and caption text get smaller below 340px. Scenes 05 and 09 do not test two dice against each other at all, they keep each die in its own lane and draw the die farthest from the camera first, so pips can hide each other at the border and nothing more is done about it. The faces are also not evenly spread: 200 throws with seed 7 gave the faces 1 to 6 counts of 51, 25, 31, 29, 26 and 38, so never use these scenes to draw a winner or settle a bet, and keep them as visual effects. To change the constants yourself, open the zip with the password ky895umc and edit the files.

Three Variants and Accessibility

Variant Value Effect
Adjust the restitution 02 DICE_RESTITUTION 0.45 → 0.7 Raising it keeps more speed at each floor hit, so the bounces stay higher for longer (in the same throw the first and second bounce apex rise from 0.82 and 0.15 to 1.17 and 0.89, in half-die units). Lowering it gives one or two bounces before the die lies flat.
Adjust the spin damping 03 SPIN_DAMPING 0.985 → 0.96 Lowering it takes more spin off every step: averaged over the same 20 throws the top-face correction starts at step 61 instead of 78, and the angular speed at one second drops from 3.8 to 2.7 rad/s. Raising it makes the die spin longer before the correction starts.
Change the seed 04 DICE_SEED 7 → 19 Changing the seed reorders the throw angles, speeds and spins so the first six faces change from 2, 6, 4, 4, 3, 1 to 5, 6, 2, 1, 5, 1; the same value always gives the same order.

With prefers-reduced-motion turned on, a canvas scene never creates a requestAnimationFrame loop. At the start and on every button press it runs the same calculation in one go, draws only the final settled scene and writes the result into the status line. Scene 08 switches all animation off and the button swaps the face at once, while scene 01 multiplies a fresh rotation into the matrix on each press. The roll control is a native <button>, so Tab focuses it and Enter or Space clicks it, and the result sits inside an <output aria-live="polite"> that a screen reader announces.

What Does the Code Use to Find the Top Face of a Die?

The top face is the face whose normal has the largest y value after it is rotated into world space. topIndex() multiplies the six normals by the y row of the rotation matrix and keeps the biggest, and the pips are laid out as +y is 1, -y is 6, +x is 2, -x is 5, +z is 3 and -z is 4, so opposite faces always add up to 7. The same calculation drives the live label in 01, the correction target in 03 and the final result in the other scenes.

Drawing on a canvas is documented in the Canvas API reference. For 3D without a canvas see the CSS 3D cube effects, for scenes computed down to the pixel see the HTML5 canvas droplet and warp effects, and for layered backgrounds see the depth-layer 3D backgrounds.

Frequently Asked Questions Before You Use These Dice Scenes

How do I turn a dice roll animation js demo into a game?

Each scene stops at storing the pip count in result and writing it to the status line. Score, turns and win rules are yours to add, and the sum in 05 and the result chip row in 09 are the easiest places to attach them.

Do I need an external library or image?

No. Scene 08 runs on DOM and CSS, the other eight on a 2D canvas and one script, and nothing calls an outside address, so a copied scene runs as it is.

What do I change if the die bounces too long or lies down too soon?

The bounce comes from DICE_RESTITUTION in 02 and the speed at which spin fades from SPIN_DAMPING in 03, while SETTLE_AFTER_MS decides when settling begins. To replay the same throw, fix DICE_SEED in 04 and you get the same list of faces each time.

Enter the archive password

The password is inside this article. You will find it as you read.