241 lines
7.2 KiB
HTML
241 lines
7.2 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Side Cards — Dice Motion Lab</title>
|
||
<style>
|
||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||
|
||
body {
|
||
min-height: 100vh;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 20px;
|
||
padding: 32px;
|
||
color: #fff;
|
||
background: radial-gradient(ellipse at center, #0b6b3a 0%, #074d28 100%);
|
||
font-family: Georgia, "Times New Roman", serif;
|
||
}
|
||
|
||
h1 {
|
||
font-size: 26px;
|
||
letter-spacing: 1px;
|
||
text-shadow: 0 2px 6px rgba(0,0,0,.5);
|
||
}
|
||
|
||
.dice-stage {
|
||
position: relative;
|
||
width: min(540px, calc(100vw - 32px));
|
||
aspect-ratio: 54 / 35;
|
||
}
|
||
|
||
#dice-canvas {
|
||
display: block;
|
||
width: 100%;
|
||
height: 100%;
|
||
outline: none;
|
||
}
|
||
|
||
.loading {
|
||
position: absolute;
|
||
inset: 0;
|
||
display: grid;
|
||
place-items: center;
|
||
color: rgba(255,255,255,.65);
|
||
font-size: 11px;
|
||
letter-spacing: 2px;
|
||
text-transform: uppercase;
|
||
pointer-events: none;
|
||
transition: opacity .18s;
|
||
}
|
||
|
||
.dice-stage.ready .loading { opacity: 0; }
|
||
|
||
.readout {
|
||
min-height: 62px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
text-align: center;
|
||
}
|
||
|
||
.total { font-size: 34px; letter-spacing: 2px; }
|
||
.detail { font-size: 13px; letter-spacing: 3px; text-transform: uppercase; opacity: .7; }
|
||
.tally { font-size: 11px; letter-spacing: 1.5px; opacity: .55; font-variant-numeric: tabular-nums; }
|
||
.controls { display: flex; gap: 12px; }
|
||
|
||
.motion-options {
|
||
width: min(540px, calc(100vw - 32px));
|
||
display: grid;
|
||
grid-template-columns: repeat(3, 1fr);
|
||
gap: 8px;
|
||
}
|
||
|
||
.motion-options button {
|
||
min-height: 58px;
|
||
padding: 8px 10px;
|
||
border: 1px solid rgba(255,255,255,.28);
|
||
border-radius: 8px;
|
||
color: rgba(255,255,255,.76);
|
||
background: rgba(0,0,0,.12);
|
||
font: 600 12px Georgia, "Times New Roman", serif;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.motion-options button span {
|
||
display: block;
|
||
margin-top: 4px;
|
||
font-size: 9px;
|
||
font-weight: 400;
|
||
letter-spacing: 1px;
|
||
opacity: .7;
|
||
}
|
||
|
||
.motion-options button.active {
|
||
border-color: rgba(255,255,255,.75);
|
||
color: #fff;
|
||
background: rgba(255,255,255,.16);
|
||
box-shadow: 0 0 0 1px rgba(255,255,255,.1) inset;
|
||
}
|
||
|
||
.motion-options button[disabled] { cursor: default; opacity: .5; }
|
||
.mode-note { min-height: 16px; font-size: 11px; letter-spacing: 1px; opacity: .65; }
|
||
|
||
.controls button {
|
||
padding: 11px 24px;
|
||
border: 1px solid rgba(255,255,255,.35);
|
||
border-radius: 8px;
|
||
color: #fff;
|
||
background: rgba(255,255,255,.12);
|
||
font: inherit;
|
||
font-size: 14px;
|
||
letter-spacing: 1px;
|
||
cursor: pointer;
|
||
transition: background .15s, transform .1s;
|
||
}
|
||
|
||
.controls button:hover { background: rgba(255,255,255,.22); }
|
||
.controls button:active { transform: translateY(1px); }
|
||
.controls button[disabled] { opacity: .45; cursor: default; transform: none; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<h1>Dice Motion Lab</h1>
|
||
|
||
<div class="motion-options" id="motion-options" aria-label="Motion preset">
|
||
<button class="active" data-preset="current">Current<span>g 24 · playback 1.70×</span></button>
|
||
<button data-preset="balanced">Balanced<span>g 96 · playback 1.15×</span></button>
|
||
<button data-preset="literal">Literal 20 mm<span>g 981 · playback 1.00×</span></button>
|
||
</div>
|
||
<div class="mode-note" id="mode-note">Approved motion — our control</div>
|
||
|
||
<div class="dice-stage" id="stage">
|
||
<canvas id="dice-canvas" aria-label="Two red dice"></canvas>
|
||
<div class="loading" id="loading">loading dice</div>
|
||
</div>
|
||
|
||
<div class="readout">
|
||
<div class="total" id="total">—</div>
|
||
<div class="detail" id="detail">ready</div>
|
||
<div class="tally" id="tally"></div>
|
||
</div>
|
||
|
||
<div class="controls">
|
||
<button id="throw" disabled>Throw</button>
|
||
<button id="reset">Reset Tally</button>
|
||
</div>
|
||
|
||
<script src="dice-renderer.bundle.js"></script>
|
||
<script>
|
||
const stage = document.getElementById("stage");
|
||
const canvas = document.getElementById("dice-canvas");
|
||
const total = document.getElementById("total");
|
||
const detail = document.getElementById("detail");
|
||
const tallyText = document.getElementById("tally");
|
||
const throwButton = document.getElementById("throw");
|
||
const presetButtons = [...document.querySelectorAll("[data-preset]")];
|
||
const modeNote = document.getElementById("mode-note");
|
||
const presetNotes = {
|
||
current: "Approved motion — our control",
|
||
balanced: "Heavier fall and impact, while keeping the tumble readable",
|
||
literal: "20 mm Earth-scale physics — intentionally fast",
|
||
};
|
||
const tally = new Array(7).fill(0);
|
||
let rolls = 0;
|
||
let tray;
|
||
|
||
function activatePreset(button) {
|
||
if (!tray || tray.isBusy() || !tray.setMotionPreset(button.dataset.preset)) return false;
|
||
presetButtons.forEach((candidate) => {
|
||
candidate.classList.toggle("active", candidate === button);
|
||
});
|
||
modeNote.textContent = presetNotes[button.dataset.preset];
|
||
detail.textContent = button.dataset.preset;
|
||
return true;
|
||
}
|
||
|
||
function updateTally(values) {
|
||
values.forEach((value) => tally[value]++);
|
||
rolls += values.length;
|
||
const parts = [];
|
||
for (let face = 1; face <= 6; face++) {
|
||
parts.push(`${face}: ${(100 * tally[face] / rolls).toFixed(1)}%`);
|
||
}
|
||
tallyText.textContent = `${rolls} faces — ${parts.join(" ")}`;
|
||
}
|
||
|
||
async function throwDice() {
|
||
if (!tray || tray.isBusy()) return;
|
||
throwButton.disabled = true;
|
||
presetButtons.forEach((button) => { button.disabled = true; });
|
||
detail.textContent = "rolling";
|
||
total.textContent = "··";
|
||
const values = await tray.roll();
|
||
if (values) {
|
||
total.textContent = values[0] + values[1];
|
||
detail.textContent = `${values[0]} + ${values[1]}`;
|
||
updateTally(values);
|
||
}
|
||
throwButton.disabled = false;
|
||
presetButtons.forEach((button) => { button.disabled = false; });
|
||
}
|
||
|
||
Dice3D.mount(canvas, 2).then((mounted) => {
|
||
tray = mounted;
|
||
stage.classList.add("ready");
|
||
throwButton.disabled = false;
|
||
const parameters = new URLSearchParams(location.search);
|
||
const requestedPreset = parameters.get("preset");
|
||
const requestedButton = presetButtons.find(
|
||
(button) => button.dataset.preset === requestedPreset,
|
||
);
|
||
if (requestedButton) activatePreset(requestedButton);
|
||
if (parameters.get("autoroll") === "1") setTimeout(throwDice, 100);
|
||
}).catch((error) => {
|
||
console.error(error);
|
||
document.getElementById("loading").textContent = "3D dice unavailable";
|
||
detail.textContent = "renderer error";
|
||
});
|
||
|
||
throwButton.addEventListener("click", throwDice);
|
||
presetButtons.forEach((button) => {
|
||
button.addEventListener("click", () => activatePreset(button));
|
||
});
|
||
document.getElementById("reset").addEventListener("click", () => {
|
||
tally.fill(0);
|
||
rolls = 0;
|
||
tallyText.textContent = "";
|
||
});
|
||
window.addEventListener("keydown", (event) => {
|
||
if (event.code === "Space" || event.code === "Enter") {
|
||
event.preventDefault();
|
||
throwDice();
|
||
}
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|