Files
2026-08-11 09:53:42 -04:00

241 lines
7.2 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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">&mdash;</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>