08Flat 2D Dice Rolling by Face Swaps
Without any 3D math, squeezes one flat face's width and widens it again while swapping the pip layer and sliding the floor stripes one tile at a time, so a flat die rolls using keyframe style rules.
Best forLight UI button flourish
HOP_MSHOP_COUNTTILE_PXpickFacesseedRand

<main class="stage">
<div class="bp is-demo" id="bpRoot">
<div class="fd" aria-hidden="true">
<div class="fd__floor" id="fdFloor"></div>
<div class="fd__shadow" id="fdShadow"></div>
<div class="fd__hop" id="fdHop">
<div class="fd__squeeze" id="fdSqueeze">
<div class="fd__die" id="fdDie" data-face="2">
<div class="fd__face fd__face--1"><i class="pip r2 c2"></i></div>
<div class="fd__face fd__face--2"><i class="pip r1 c1"></i><i class="pip r3 c3"></i></div>
<div class="fd__face fd__face--3"><i class="pip r1 c1"></i><i class="pip r2 c2"></i><i class="pip r3 c3"></i></div>
<div class="fd__face fd__face--4"><i class="pip r1 c1"></i><i class="pip r1 c3"></i><i class="pip r3 c1"></i><i class="pip r3 c3"></i></div>
<div class="fd__face fd__face--5"><i class="pip r1 c1"></i><i class="pip r1 c3"></i><i class="pip r2 c2"></i><i class="pip r3 c1"></i><i class="pip r3 c3"></i></div>
<div class="fd__face fd__face--6"><i class="pip r1 c1"></i><i class="pip r2 c1"></i><i class="pip r3 c1"></i><i class="pip r1 c3"></i><i class="pip r2 c3"></i><i class="pip r3 c3"></i></div>
</div>
</div>
</div>
</div>
<output class="bp__status" aria-live="polite"><span id="bpLabel">Rolling</span> <b id="bpNum"></b></output>
<button type="button" class="bp__btn" id="bpBtn">Roll</button>
<p class="bp__caption">Without any 3D math, squeezes one flat face's width and widens it again while swapping the pip layer and sliding the floor stripes one tile at a time, so a flat die rolls using keyframe style rules.</p>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
@include stage($stage-ink);
$tile: 128px;
$hop-height: 28px;
$die-size: 80px;
$hop-dur: .4s;
$felt-a: #a8e0c8;
$felt-b: #84cbae;
$die-face: #fff1f5;
$die-edge: #f2b4c8;
$pip-ink: #4b3a63;
$sky-top: #cdbfff;
$sky-bottom: #fbe3ec;
.bp {
position: relative;
width: min(92vw, calc(160vh - 12.8vw));
aspect-ratio: 480 / 300;
margin: 0 auto;
border-radius: $r-card;
overflow: hidden;
box-shadow: $shadow-raised;
background: $stage-ink;
}
.fd {
position: absolute;
inset: 0;
overflow: hidden;
background: linear-gradient($sky-top, $sky-bottom);
}
.fd__floor {
position: absolute;
left: 0;
bottom: 0;
width: calc(100% + 5 * #{$tile});
height: 34%;
background: repeating-linear-gradient(90deg, $felt-a 0 ($tile * .5), $felt-b ($tile * .5) $tile);
box-shadow: inset 0 4px 0 rgba(255, 255, 255, .5);
}
.fd__shadow {
position: absolute;
left: 50%;
bottom: calc(22% - 8px);
width: $die-size;
height: 16px;
margin-left: -($die-size * .5);
border-radius: 50%;
background: rgba(30, 60, 60, .5);
opacity: .5;
}
.fd__hop {
position: absolute;
left: 50%;
bottom: 22%;
width: $die-size;
height: $die-size;
margin-left: -($die-size * .5);
}
.fd__squeeze {
width: 100%;
height: 100%;
}
.fd__die {
position: relative;
width: 100%;
height: 100%;
border-radius: $r-card;
background: $die-face;
box-shadow: inset 0 -6px 0 rgba(200, 120, 160, .28), inset 0 0 0 3px $die-edge;
}
.fd__face {
position: absolute;
inset: 0;
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(3, 1fr);
padding: $sp-3;
opacity: 0;
}
.pip {
width: $sp-4;
height: $sp-4;
border-radius: $r-pill;
background: $pip-ink;
place-self: center;
}
.r1 { grid-row: 1; }
.r2 { grid-row: 2; }
.r3 { grid-row: 3; }
.c1 { grid-column: 1; }
.c2 { grid-column: 2; }
.c3 { grid-column: 3; }
[data-face="1"] .fd__face--1,
[data-face="2"] .fd__face--2,
[data-face="3"] .fd__face--3,
[data-face="4"] .fd__face--4,
[data-face="5"] .fd__face--5,
[data-face="6"] .fd__face--6 { opacity: 1; }
@keyframes fd-floor {
0% { transform: translateX(0); animation-timing-function: cubic-bezier(.45, 0, .2, 1); }
20% { transform: translateX(-$tile); animation-timing-function: cubic-bezier(.45, 0, .2, 1); }
40% { transform: translateX(-2 * $tile); animation-timing-function: cubic-bezier(.45, 0, .2, 1); }
60% { transform: translateX(-3 * $tile); animation-timing-function: cubic-bezier(.45, 0, .2, 1); }
80% { transform: translateX(-4 * $tile); animation-timing-function: cubic-bezier(.45, 0, .2, 1); }
100% { transform: translateX(-5 * $tile); }
}
@keyframes fd-hop {
0% { transform: translateY(0); animation-timing-function: ease-out; }
50% { transform: translateY(-$hop-height); animation-timing-function: ease-in; }
100% { transform: translateY(0); }
}
@keyframes fd-squeeze {
0% { transform: scaleX(1); animation-timing-function: ease-in; }
50% { transform: scaleX(.18); animation-timing-function: ease-out; }
100% { transform: scaleX(1); }
}
@keyframes fd-shadow {
0% { transform: scale(1); opacity: .5; animation-timing-function: ease-out; }
50% { transform: scale(.6); opacity: .25; animation-timing-function: ease-in; }
100% { transform: scale(1); opacity: .5; }
}
@keyframes fd-face-1 {
0% { opacity: 0; }
50% { opacity: 1; }
70% { opacity: 0; }
}
@keyframes fd-face-2 {
0% { opacity: 0; }
10% { opacity: 1; }
30% { opacity: 0; }
}
@keyframes fd-face-4 {
0% { opacity: 0; }
70% { opacity: 1; }
90% { opacity: 0; }
}
@keyframes fd-face-5 {
0% { opacity: 0; }
30% { opacity: 1; }
50% { opacity: 0; }
}
@keyframes fd-face-6 {
0% { opacity: 1; }
10% { opacity: 0; }
90% { opacity: 1; }
}
.is-demo .fd__floor { animation: fd-floor $dur-loop linear infinite; }
.is-demo .fd__hop { animation: fd-hop $hop-dur linear infinite; }
.is-demo .fd__squeeze { animation: fd-squeeze $hop-dur linear infinite; }
.is-demo .fd__shadow { animation: fd-shadow $hop-dur linear infinite; }
.is-demo .fd__face--2 { animation: fd-face-2 $dur-loop steps(1, end) infinite; }
.is-demo .fd__face--5 { animation: fd-face-5 $dur-loop steps(1, end) infinite; }
.is-demo .fd__face--1 { animation: fd-face-1 $dur-loop steps(1, end) infinite; }
.is-demo .fd__face--4 { animation: fd-face-4 $dur-loop steps(1, end) infinite; }
.is-demo .fd__face--6 { animation: fd-face-6 $dur-loop steps(1, end) infinite; }
.bp__status {
position: absolute;
left: $sp-3;
top: $sp-3;
display: flex;
align-items: baseline;
gap: $sp-2;
margin: 0;
padding: $sp-1 $sp-3;
border-radius: $r-pill;
background: rgba(255, 255, 255, .85);
color: $subject-ink;
font-weight: 500;
font-size: 12px;
line-height: 1.4;
}
.bp__status b {
min-width: 1ch;
font-weight: 700;
font-variant-numeric: tabular-nums;
}
.bp__btn {
position: absolute;
right: $sp-3;
top: $sp-3;
min-height: $sp-8;
padding: 0 $sp-4;
border: 0;
border-radius: $r-control;
background: $subject-cream;
color: $subject-ink;
font-weight: 700;
font-size: 13px;
line-height: 1;
cursor: pointer;
box-shadow: $shadow-press;
transition: transform $dur-instant $ease-out;
}
.bp__btn:hover { transform: translateY(-1px); }
.bp__btn:active { transform: translateY(1px); }
.bp__btn:focus-visible {
outline: 2px solid rgba(255, 255, 255, .9);
outline-offset: 2px;
}
.bp__caption {
position: absolute;
left: $sp-3;
bottom: $sp-3;
max-width: 80%;
margin: 0;
padding: $sp-1 $sp-3;
border-radius: $r-pill;
background: rgba(255, 255, 255, .85);
color: $subject-ink;
font-weight: 500;
font-size: 12px;
line-height: 1.4;
pointer-events: none;
}
@media (max-width: 340px) {
.bp__caption { font-size: 10px; padding: $sp-1 $sp-2; }
.bp__status { font-size: 10px; padding: $sp-1 $sp-2; }
.bp__btn { font-size: 11px; padding: 0 $sp-3; }
}
@media (prefers-reduced-motion: reduce) {
.bp { cursor: default; }
.bp__btn { transition: none; }
.fd__floor, .fd__hop, .fd__squeeze, .fd__shadow, .fd__face { animation: none !important; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #17141a;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.bp {
position: relative;
width: min(92vw, 160vh - 12.8vw);
aspect-ratio: 480/300;
margin: 0 auto;
border-radius: 16px;
overflow: hidden;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
background: #17141a;
}
.fd {
position: absolute;
inset: 0;
overflow: hidden;
background: linear-gradient(#cdbfff, #fbe3ec);
}
.fd__floor {
position: absolute;
left: 0;
bottom: 0;
width: calc(100% + 5 * 128px);
height: 34%;
background: repeating-linear-gradient(90deg, #a8e0c8 0 64px, #84cbae 64px 128px);
box-shadow: inset 0 4px 0 rgba(255, 255, 255, 0.5);
}
.fd__shadow {
position: absolute;
left: 50%;
bottom: calc(22% - 8px);
width: 80px;
height: 16px;
margin-left: -40px;
border-radius: 50%;
background: rgba(30, 60, 60, 0.5);
opacity: 0.5;
}
.fd__hop {
position: absolute;
left: 50%;
bottom: 22%;
width: 80px;
height: 80px;
margin-left: -40px;
}
.fd__squeeze {
width: 100%;
height: 100%;
}
.fd__die {
position: relative;
width: 100%;
height: 100%;
border-radius: 16px;
background: #fff1f5;
box-shadow: inset 0 -6px 0 rgba(200, 120, 160, 0.28), inset 0 0 0 3px #f2b4c8;
}
.fd__face {
position: absolute;
inset: 0;
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(3, 1fr);
padding: 12px;
opacity: 0;
}
.pip {
width: 16px;
height: 16px;
border-radius: 999px;
background: #4b3a63;
place-self: center;
}
.r1 {
grid-row: 1;
}
.r2 {
grid-row: 2;
}
.r3 {
grid-row: 3;
}
.c1 {
grid-column: 1;
}
.c2 {
grid-column: 2;
}
.c3 {
grid-column: 3;
}
[data-face="1"] .fd__face--1,
[data-face="2"] .fd__face--2,
[data-face="3"] .fd__face--3,
[data-face="4"] .fd__face--4,
[data-face="5"] .fd__face--5,
[data-face="6"] .fd__face--6 {
opacity: 1;
}
@keyframes fd-floor {
0% {
transform: translateX(0);
animation-timing-function: cubic-bezier(0.45, 0, 0.2, 1);
}
20% {
transform: translateX(-128px);
animation-timing-function: cubic-bezier(0.45, 0, 0.2, 1);
}
40% {
transform: translateX(-256px);
animation-timing-function: cubic-bezier(0.45, 0, 0.2, 1);
}
60% {
transform: translateX(-384px);
animation-timing-function: cubic-bezier(0.45, 0, 0.2, 1);
}
80% {
transform: translateX(-512px);
animation-timing-function: cubic-bezier(0.45, 0, 0.2, 1);
}
100% {
transform: translateX(-640px);
}
}
@keyframes fd-hop {
0% {
transform: translateY(0);
animation-timing-function: ease-out;
}
50% {
transform: translateY(-28px);
animation-timing-function: ease-in;
}
100% {
transform: translateY(0);
}
}
@keyframes fd-squeeze {
0% {
transform: scaleX(1);
animation-timing-function: ease-in;
}
50% {
transform: scaleX(0.18);
animation-timing-function: ease-out;
}
100% {
transform: scaleX(1);
}
}
@keyframes fd-shadow {
0% {
transform: scale(1);
opacity: 0.5;
animation-timing-function: ease-out;
}
50% {
transform: scale(0.6);
opacity: 0.25;
animation-timing-function: ease-in;
}
100% {
transform: scale(1);
opacity: 0.5;
}
}
@keyframes fd-face-1 {
0% {
opacity: 0;
}
50% {
opacity: 1;
}
70% {
opacity: 0;
}
}
@keyframes fd-face-2 {
0% {
opacity: 0;
}
10% {
opacity: 1;
}
30% {
opacity: 0;
}
}
@keyframes fd-face-4 {
0% {
opacity: 0;
}
70% {
opacity: 1;
}
90% {
opacity: 0;
}
}
@keyframes fd-face-5 {
0% {
opacity: 0;
}
30% {
opacity: 1;
}
50% {
opacity: 0;
}
}
@keyframes fd-face-6 {
0% {
opacity: 1;
}
10% {
opacity: 0;
}
90% {
opacity: 1;
}
}
.is-demo .fd__floor {
animation: fd-floor 2s linear infinite;
}
.is-demo .fd__hop {
animation: fd-hop 0.4s linear infinite;
}
.is-demo .fd__squeeze {
animation: fd-squeeze 0.4s linear infinite;
}
.is-demo .fd__shadow {
animation: fd-shadow 0.4s linear infinite;
}
.is-demo .fd__face--2 {
animation: fd-face-2 2s steps(1, end) infinite;
}
.is-demo .fd__face--5 {
animation: fd-face-5 2s steps(1, end) infinite;
}
.is-demo .fd__face--1 {
animation: fd-face-1 2s steps(1, end) infinite;
}
.is-demo .fd__face--4 {
animation: fd-face-4 2s steps(1, end) infinite;
}
.is-demo .fd__face--6 {
animation: fd-face-6 2s steps(1, end) infinite;
}
.bp__status {
position: absolute;
left: 12px;
top: 12px;
display: flex;
align-items: baseline;
gap: 8px;
margin: 0;
padding: 4px 12px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.85);
color: #17141a;
font-weight: 500;
font-size: 12px;
line-height: 1.4;
}
.bp__status b {
min-width: 1ch;
font-weight: 700;
font-variant-numeric: tabular-nums;
}
.bp__btn {
position: absolute;
right: 12px;
top: 12px;
min-height: 32px;
padding: 0 16px;
border: 0;
border-radius: 12px;
background: #fff7e6;
color: #17141a;
font-weight: 700;
font-size: 13px;
line-height: 1;
cursor: pointer;
box-shadow: 0 6px 12px rgba(23, 20, 26, 0.32);
transition: transform 140ms ease-out;
}
.bp__btn:hover {
transform: translateY(-1px);
}
.bp__btn:active {
transform: translateY(1px);
}
.bp__btn:focus-visible {
outline: 2px solid rgba(255, 255, 255, 0.9);
outline-offset: 2px;
}
.bp__caption {
position: absolute;
left: 12px;
bottom: 12px;
max-width: 80%;
margin: 0;
padding: 4px 12px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.85);
color: #17141a;
font-weight: 500;
font-size: 12px;
line-height: 1.4;
pointer-events: none;
}
@media (max-width: 340px) {
.bp__caption {
font-size: 10px;
padding: 4px 8px;
}
.bp__status {
font-size: 10px;
padding: 4px 8px;
}
.bp__btn {
font-size: 11px;
padding: 0 12px;
}
}
@media (prefers-reduced-motion: reduce) {
.bp {
cursor: default;
}
.bp__btn {
transition: none;
}
.fd__floor, .fd__hop, .fd__squeeze, .fd__shadow, .fd__face {
animation: none !important;
}
}(function () {
var root = document.getElementById('bpRoot');
var labelEl = document.getElementById('bpLabel');
var numEl = document.getElementById('bpNum');
var btn = document.getElementById('bpBtn');
var dieEl = document.getElementById('fdDie');
var floorEl = document.getElementById('fdFloor');
var hopEl = document.getElementById('fdHop');
var squeezeEl = document.getElementById('fdSqueeze');
var shadowEl = document.getElementById('fdShadow');
var DICE_SEED = 7;
var HOP_MS = 360;
var HOP_COUNT = 4;
var TILE_PX = 128;
var T_RESULT = 'Result';
var T_ROLLING = 'Rolling';
var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
var demo = !reduce;
if (!demo) root.classList.remove('is-demo');
function stopDemo() { demo = false; root.classList.remove('is-demo'); }
function seedRand(seed) {
var s = seed >>> 0;
return function () { s = (s * 1664525 + 1013904223) >>> 0; return s / 4294967296; };
}
var rng = seedRand(DICE_SEED + 8);
var timers = [];
var cleaners = [];
var stLabel = '', stNum = '';
function setStatus(label, num) {
if (label === stLabel && num === stNum) return;
stLabel = label; stNum = num;
labelEl.textContent = label;
numEl.textContent = num;
}
// 기법: 면 한 장의 가로 폭을 scaleX 로 줄였다 늘리는 동안(납작해지는 순간) 눈 수 레이어를 바꿔 끼운다.
// 바닥 줄무늬를 한 칸(TILE_PX)씩 밀면 주사위는 제자리에서 튀기고 바닥이 굴러 가는 것처럼 보인다.
function clearTimers() {
for (var i = 0; i < timers.length; i += 1) window.clearTimeout(timers[i]);
timers = [];
}
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;
}
function onRoll() {
stopDemo();
clearTimers();
var running = [floorEl, hopEl, squeezeEl, shadowEl];
for (var r = 0; r < running.length; r += 1) {
var an = running[r].getAnimations();
for (var q = 0; q < an.length; q += 1) an[q].cancel();
}
var seq = pickFaces();
var last = seq[seq.length - 1];
if (reduce) {
dieEl.setAttribute('data-face', String(last));
setStatus(T_RESULT, String(last));
return;
}
setStatus(T_ROLLING, '');
var span = HOP_MS * HOP_COUNT;
floorEl.animate([{ transform: 'translateX(0px)' }, { transform: 'translateX(-' + (TILE_PX * HOP_COUNT) + 'px)' }], { duration: span, easing: 'ease-in-out' });
hopEl.animate([{ transform: 'translateY(0px)' }, { transform: 'translateY(-28px)', offset: 0.5 }, { transform: 'translateY(0px)' }], { duration: HOP_MS, iterations: HOP_COUNT, easing: 'ease-out' });
squeezeEl.animate([{ transform: 'scaleX(1)' }, { transform: 'scaleX(0.18)', offset: 0.5 }, { transform: 'scaleX(1)' }], { duration: HOP_MS, iterations: HOP_COUNT, easing: 'ease-in-out' });
shadowEl.animate([{ transform: 'scale(1)', opacity: 0.5 }, { transform: 'scale(0.6)', opacity: 0.25, offset: 0.5 }, { transform: 'scale(1)', opacity: 0.5 }], { duration: HOP_MS, iterations: HOP_COUNT, easing: 'ease-out' });
for (var k = 0; k < HOP_COUNT; k += 1) {
(function (face, at) {
timers.push(window.setTimeout(function () { dieEl.setAttribute('data-face', String(face)); }, at));
})(seq[k], Math.round((k + 0.5) * HOP_MS));
}
timers.push(window.setTimeout(function () { setStatus(T_RESULT, String(last)); }, span + 20));
}
if (reduce) setStatus(T_RESULT, String(Number(dieEl.getAttribute('data-face')) || 2));
btn.addEventListener('click', onRoll);
cleaners.push(function () { btn.removeEventListener('click', onRoll); });
function destroy() {
clearTimers();
for (var i = 0; i < cleaners.length; i += 1) cleaners[i]();
}
})();import { useEffect, useRef } from "react";
import styles from "./SpinDiceSceneFlatFaceTumble.module.scss";
export default function SpinDiceSceneFlatFaceTumble() {
const rootRef = useRef<HTMLDivElement | null>(null);
const labelRef = useRef<HTMLSpanElement | null>(null);
const numRef = useRef<HTMLElement | null>(null);
const btnRef = useRef<HTMLButtonElement | null>(null);
const dieRef = useRef<HTMLDivElement | null>(null);
const floorRef = useRef<HTMLDivElement | null>(null);
const hopRef = useRef<HTMLDivElement | null>(null);
const squeezeRef = useRef<HTMLDivElement | null>(null);
const shadowRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
const r0 = rootRef.current, l0 = labelRef.current, n0 = numRef.current, b0 = btnRef.current;
const d0 = dieRef.current, f0 = floorRef.current, h0 = hopRef.current, s0 = squeezeRef.current, sh0 = shadowRef.current;
if (!r0 || !l0 || !n0 || !b0 || !d0 || !f0 || !h0 || !s0 || !sh0) return;
const root: HTMLDivElement = r0;
const labelEl: HTMLElement = l0;
const numEl: HTMLElement = n0;
const btn: HTMLButtonElement = b0;
const dieEl: HTMLDivElement = d0;
const floorEl: HTMLDivElement = f0;
const hopEl: HTMLDivElement = h0;
const squeezeEl: HTMLDivElement = s0;
const shadowEl: HTMLDivElement = sh0;
type Fn = () => void;
type RandFn = () => number;
var DICE_SEED = 7;
var HOP_MS = 360;
var HOP_COUNT = 4;
var TILE_PX = 128;
var T_RESULT = '결과';
var T_ROLLING = '굴리는 중';
var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
var demo = !reduce;
if (!demo) root.classList.remove('is-demo');
function stopDemo(): void { demo = false; root.classList.remove('is-demo'); }
function seedRand(seed: number): RandFn {
var s = seed >>> 0;
return function (): number { s = (s * 1664525 + 1013904223) >>> 0; return s / 4294967296; };
}
var rng = seedRand(DICE_SEED + 8);
var timers: number[] = [];
var cleaners: Fn[] = [];
var stLabel = '', stNum = '';
function setStatus(label: string, num: string): void {
if (label === stLabel && num === stNum) return;
stLabel = label; stNum = num;
labelEl.textContent = label;
numEl.textContent = num;
}
// 기법: 면 한 장의 가로 폭을 scaleX 로 줄였다 늘리는 동안(납작해지는 순간) 눈 수 레이어를 바꿔 끼운다.
// 바닥 줄무늬를 한 칸(TILE_PX)씩 밀면 주사위는 제자리에서 튀기고 바닥이 굴러 가는 것처럼 보인다.
function clearTimers(): void {
for (var i = 0; i < timers.length; i += 1) window.clearTimeout(timers[i]);
timers = [];
}
function pickFaces(): number[] {
var seq: number[] = [];
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;
}
function onRoll(): void {
stopDemo();
clearTimers();
var running = [floorEl, hopEl, squeezeEl, shadowEl];
for (var r = 0; r < running.length; r += 1) {
var an = running[r].getAnimations();
for (var q = 0; q < an.length; q += 1) an[q].cancel();
}
var seq = pickFaces();
var last = seq[seq.length - 1];
if (reduce) {
dieEl.setAttribute('data-face', String(last));
setStatus(T_RESULT, String(last));
return;
}
setStatus(T_ROLLING, '');
var span = HOP_MS * HOP_COUNT;
floorEl.animate([{ transform: 'translateX(0px)' }, { transform: 'translateX(-' + (TILE_PX * HOP_COUNT) + 'px)' }], { duration: span, easing: 'ease-in-out' });
hopEl.animate([{ transform: 'translateY(0px)' }, { transform: 'translateY(-28px)', offset: 0.5 }, { transform: 'translateY(0px)' }], { duration: HOP_MS, iterations: HOP_COUNT, easing: 'ease-out' });
squeezeEl.animate([{ transform: 'scaleX(1)' }, { transform: 'scaleX(0.18)', offset: 0.5 }, { transform: 'scaleX(1)' }], { duration: HOP_MS, iterations: HOP_COUNT, easing: 'ease-in-out' });
shadowEl.animate([{ transform: 'scale(1)', opacity: 0.5 }, { transform: 'scale(0.6)', opacity: 0.25, offset: 0.5 }, { transform: 'scale(1)', opacity: 0.5 }], { duration: HOP_MS, iterations: HOP_COUNT, easing: 'ease-out' });
for (var k = 0; k < HOP_COUNT; k += 1) {
(function (face: number, at: number): void {
timers.push(window.setTimeout(function (): void { dieEl.setAttribute('data-face', String(face)); }, at));
})(seq[k], Math.round((k + 0.5) * HOP_MS));
}
timers.push(window.setTimeout(function (): void { setStatus(T_RESULT, String(last)); }, span + 20));
}
if (reduce) setStatus(T_RESULT, String(Number(dieEl.getAttribute('data-face')) || 2));
btn.addEventListener('click', onRoll);
cleaners.push(function (): void { btn.removeEventListener('click', onRoll); });
function destroy(): void {
clearTimers();
for (var i = 0; i < cleaners.length; i += 1) cleaners[i]();
}
return destroy;
}, []);
return (
<main className={styles.stage}>
<div className={`${styles.bp} is-demo`} ref={rootRef}>
<div className={styles.fd} aria-hidden="true">
<div className={styles.fd__floor} ref={floorRef} />
<div className={styles.fd__shadow} ref={shadowRef} />
<div className={styles.fd__hop} ref={hopRef}>
<div className={styles.fd__squeeze} ref={squeezeRef}>
<div className={styles.fd__die} ref={dieRef} data-face="2">
<div className={`${styles.fd__face} ${styles["fd__face--1"]}`}><i className={`${styles.pip} ${styles.r2} ${styles.c2}`} /></div>
<div className={`${styles.fd__face} ${styles["fd__face--2"]}`}><i className={`${styles.pip} ${styles.r1} ${styles.c1}`} /><i className={`${styles.pip} ${styles.r3} ${styles.c3}`} /></div>
<div className={`${styles.fd__face} ${styles["fd__face--3"]}`}><i className={`${styles.pip} ${styles.r1} ${styles.c1}`} /><i className={`${styles.pip} ${styles.r2} ${styles.c2}`} /><i className={`${styles.pip} ${styles.r3} ${styles.c3}`} /></div>
<div className={`${styles.fd__face} ${styles["fd__face--4"]}`}><i className={`${styles.pip} ${styles.r1} ${styles.c1}`} /><i className={`${styles.pip} ${styles.r1} ${styles.c3}`} /><i className={`${styles.pip} ${styles.r3} ${styles.c1}`} /><i className={`${styles.pip} ${styles.r3} ${styles.c3}`} /></div>
<div className={`${styles.fd__face} ${styles["fd__face--5"]}`}><i className={`${styles.pip} ${styles.r1} ${styles.c1}`} /><i className={`${styles.pip} ${styles.r1} ${styles.c3}`} /><i className={`${styles.pip} ${styles.r2} ${styles.c2}`} /><i className={`${styles.pip} ${styles.r3} ${styles.c1}`} /><i className={`${styles.pip} ${styles.r3} ${styles.c3}`} /></div>
<div className={`${styles.fd__face} ${styles["fd__face--6"]}`}><i className={`${styles.pip} ${styles.r1} ${styles.c1}`} /><i className={`${styles.pip} ${styles.r2} ${styles.c1}`} /><i className={`${styles.pip} ${styles.r3} ${styles.c1}`} /><i className={`${styles.pip} ${styles.r1} ${styles.c3}`} /><i className={`${styles.pip} ${styles.r2} ${styles.c3}`} /><i className={`${styles.pip} ${styles.r3} ${styles.c3}`} /></div>
</div>
</div>
</div>
</div>
<output className={styles.bp__status} aria-live="polite"><span ref={labelRef}>굴리는 중</span> <b ref={numRef} /></output>
<button type="button" className={styles.bp__btn} ref={btnRef}>굴리기</button>
<p className={styles.bp__caption}>3D 계산 없이 면 한 장의 가로 폭을 줄였다 늘리며 눈 수를 바꿔 끼우고 바닥 줄무늬를 한 칸씩 밀어, 평면 주사위가 굴러가는 모습을 키프레임 스타일 규칙으로 만든다.</p>
</div>
</main>
);
}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 · Dice Roll Animation: 9 Scenes with Top-Face Math
The post this demo lives in
Dice Roll Animation: 9 Scenes with Top-Face MathThrow, bounce, settle — one roll is just three verbs, and scene 03 reads the top-face number out of the last one by calc See all demos in this post
If you like this effect - similar demos
Interactions01 A Coin That Squashes Flat and Flips Faces Around a Horizontal AxisCoin Flip: 9 Canvas Toss Scenes
Interactions03 Six-faced box you drag to spin3D Hover Effect CSS: 9 Pointer-Following Scenes
Interactions01 Ferris wheel scene — 16 spokes cycling through perspectivecss 3d transform Scenes: 9 Pointer Miniatures