aether-dice
v0.3.0
Published
Standalone, framework-agnostic 3D dice roller that settles each die on a caller-supplied value.
Maintainers
Readme
AetherDice
A standalone, framework-agnostic 3D dice roller for the browser. Renders
d4 · d6 · d8 · d10 · d12 · d20 · d100 as a physics-driven, transparent canvas overlay
— and settles each die on a value you supply, so the visual always matches whatever
your app's logic already decided.
Built to be dropped into any web app (including no-build, vanilla-ESM ones) as a single prebuilt module + assets.
AetherDice is a presentation layer. It does not roll randomly or do dice math — your app owns the RNG and modifiers; AetherDice shows the given face values with a believable tumble.
Features
- All standard polyhedral dice +
d100(as tens/units d10s, with percentile mode). - Deterministic results — pass the exact value each die must land on.
- Physics-based tumble (Three.js + cannon-es) with a guaranteed final face.
- Dice never leave the frame — a hard containment guarantee (frustum-bounded tray +
per-step clamp), independent of
gravity/throwForce. - Mixed throws in one roll (e.g.
1d20 + 2d6). - Themeable with real HDR edge bloom —
aether(glowing cyan default),aether-neon,aether-molten,classic, or your own. - Respects
prefers-reduced-motion; instant/skip mode; WebGL-absent fallback signal. - Ships as one ESM bundle + assets; no framework dependency.
Install
npm install aether-diceOr load directly (no build step) via CDN:
import { AetherDice } from 'https://cdn.jsdelivr.net/npm/aether-dice/dist/aether-dice.esm.js';Quick start
import { AetherDice } from 'aether-dice';
const dice = new AetherDice({
container: document.getElementById('dice-overlay'), // a positioned element
assetPath: '/assets/aether-dice/', // where textures/wasm live
theme: 'aether',
});
await dice.init();
// The outcome is decided by YOUR app; AetherDice just renders it:
const result = await dice.roll(
[{ sides: 20 }, { sides: 6 }, { sides: 6 }],
{ results: [17, 4, 2] },
);
// → { dice: [{sides:20,value:17}, {sides:6,value:4}, {sides:6,value:2}], total: 23 }API
new AetherDice(options)
| Option | Type | Default | Description |
|---|---|---|---|
| container | HTMLElement | — | Element the canvas overlay mounts into (required). |
| assetPath | string | bundle-relative | Base URL for textures (env/normal maps). Defaults to the assets/ folder shipped next to the bundle, so npm/jsDelivr consumers need no config; override with your own hosted path. |
| theme | string \| object | 'aether' | Built-in theme name or a theme object. |
| scale | number | 6 | Die size. |
| gravity | number | 40 | Physics gravity. |
| throwForce | number | 8 | Initial impulse strength. |
| trayMargin | number | 0.5 | Extra inset of the containment tray from the frame edge (scene units). Larger keeps dice further from the edge. |
| reducedMotion | boolean | auto | true skips the tumble and shows the result instantly. Defaults to the user's prefers-reduced-motion. |
| maxDice | number | 20 | Safety cap on dice per roll. |
Methods
await dice.init()— load the engine + assets; resolves when ready.await dice.roll(dice, opts)— render a roll.diceis a notation string ('2d8') or an array ([{ sides: 20 }, …]).opts.resultsis the array of values each die must land on (always pass this from a game).opts.instantskips the tumble for this call. Resolves to{ dice: [{ sides, value }], total }.dice.clear()— remove dice from the scene.dice.destroy()— dispose all GPU resources (call on unmount).dice.on(event, handler)/dice.off(event, handler).
Events
| Event | Payload | When |
|---|---|---|
| rollStart | { dice } | A roll begins. |
| rollComplete | { dice, total, text } | Dice settle. text is the readable result (use it for logs / screen readers). |
| webglUnavailable | {} | WebGL can't initialize — fall back to a 2D presentation. |
Determinism (the important part)
If you omit results, AetherDice may roll randomly and return the landed values — handy
for demos. In a real game, always pass results so the rendered dice match your
authoritative roll. Internally the tumble runs with a random throw, then a corrective
rotation guarantees the requested face ends up. The result is also delivered as text
via rollComplete, so the visual is never the only source of truth.
Theming
Pass a built-in theme name or a theme object. A theme object is merged over the aether
default, so you only specify what you want to change:
new AetherDice({ theme: 'aether-neon' }); // built-in name
new AetherDice({ theme: { // custom (merged over aether)
dieColor: '#10131a', // body color
edgeGlow: '#39d0ff', // edge color (base hue of the glow)
numberColor: '#eaf6ff', // numeral color
numberFont: '600 220px "Trebuchet MS", system-ui, sans-serif',
metalness: 0.2,
roughness: 0.35,
opacity: 0.92, // <1 → translucent body
ambient: '#22303a', // ambient light color
keyLight: '#ffffff', // key (directional) light color
bloom: true, // enable the edge bloom post-process
edgeIntensity: 2.2, // HDR edge multiplier (>1 → brighter, punchier glow)
bloomStrength: 1.5, // overall bloom amount
bloomRadius: 0.6, // bloom spread
bloomThreshold:0, // luminance cutoff (0 = bloom all edge light)
envMap: 'env.png', // environment map for reflections (null to disable)
envIntensity: 1.0, // reflection strength
normalMap: 'surface-normal.png', // subtle surface detail (null to disable)
normalScale: 0.25, // normal map strength
}});envMap and normalMap are filenames resolved against assetPath. AetherDice ships
default env.png (a studio environment giving the metallic dice reflections) and
surface-normal.png (a faint cast-glass surface) in its assets/ folder; set either to
null to skip it. Textures load during init() and failures are non-fatal — the dice
just render without that map.
Built-in themes
| Preview | Name | Look |
|---|---|---|
| | aether (default) | Dark dice, glowing cyan edges, light numerals. |
| | aether-neon | Deep violet body, searing magenta-purple edge glow. |
| | aether-molten | Dark ember body, hot orange edge glow, warm numerals. |
| | classic | Ivory/black, no glow. |
Edge bloom (the glow is real)
The edge glow is a true bloom post-process, not a flat outline. It uses selective
bloom — only the die edges glow, never the body or numerals — and the edge color is pushed
into HDR (edgeIntensity > 1) so the brightest part of each edge blows out to a
white-hot core with a saturated colored halo, like neon. Two knobs shape it:
edgeIntensity— how over-bright the edge source is (character of the glow).bloomStrength/bloomRadius— how much the glow spreads (amount and softness).
Set bloom: false (as classic does) to skip the post-processing pipeline entirely — the
edges then render as a plain line, or are omitted if edgeGlow is transparent.
Supported dice
d4, d6, d8, d10, d12, d20, d100. The d4 is read by its apex/bottom convention; d100
renders as a tens die (00–90) + a units die (0–9) and can combine to a percentile.
Development
npm install
npm run dev # demo page with live reload
npm test # unit tests (value resolution, d4/d100, corrective orientation)
npm run build # emits dist/aether-dice.esm.js + assets/The /demo page rolls every die type, mixed throws, and lets you force a predetermined
result to verify determinism.
Distribution
Published to npm and consumable via jsDelivr. The public API above is the semver
stability contract. The build emits dist/aether-dice.esm.js + dist/assets/, and both
ship in the npm tarball — the bundle resolves its default assetPath relative to itself,
so textures load with no configuration. Override assetPath only if you host the assets
elsewhere (e.g. a different CDN).
Releasing
One command runs the whole flow — preflight, quality gate, version bump + tag, push, npm publish, and a GitHub release with the bundle attached:
npm run release # patch (0.1.0 -> 0.1.1)
npm run release -- minor # minor (0.1.0 -> 0.2.0)
npm run release -- major # major
npm run release -- 1.2.3 # explicit version
npm run release -- patch --dry-run # preflight + gate + pack preview, no changesIt aborts before mutating anything if the tree is dirty, the branch isn't main, the
branch is behind origin, npm isn't authenticated, or lint/tests/build fail.
License
MIT.
