Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ stash
public-stash
notes.txt
stats.html
.history
.history
.claude
40 changes: 40 additions & 0 deletions drag-demo/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
<title>Dice Drag Throw</title>
<style>
html, body { margin: 0; height: 100%; overflow: hidden; background: #111; color: #eee; font-family: system-ui, sans-serif; }
#dice-canvas { position: fixed; inset: 0; width: 100vw; height: 100vh; display: block; touch-action: none; }
#hud {
position: fixed; top: 12px; left: 12px; z-index: 10;
background: rgba(0,0,0,.5); padding: 10px 12px; border-radius: 8px;
display: flex; gap: 8px; align-items: center;
}
#hud input { width: 80px; padding: 4px 6px; border-radius: 4px; border: 1px solid #444; background: #222; color: #eee; }
#hud .hint { font-size: 12px; opacity: .7; margin-left: 6px; }
#debug { position: fixed; bottom: 12px; left: 12px; font-size: 12px; opacity: .7; z-index: 10; white-space: pre; }
#arrow {
position: fixed; top: 0; left: 0; pointer-events: none; z-index: 9;
width: 100vw; height: 100vh;
}
</style>
</head>
<body>
<div id="hud">
<label>Roll: <input id="notation" type="text" value="4d6" /></label>
<span class="hint">drag anywhere on the canvas to fling the dice</span>
</div>
<svg id="arrow">
<defs>
<marker id="arrowhead" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#4af" />
</marker>
</defs>
<line id="arrow-line" x1="0" y1="0" x2="0" y2="0" stroke="#4af" stroke-width="3" marker-end="url(#arrowhead)" />
</svg>
<div id="debug"></div>
<script type="module" src="./main.js"></script>
</body>
</html>
125 changes: 125 additions & 0 deletions drag-demo/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import DiceBox from '../src/index.js'

const Dice = new DiceBox({
id: 'dice-canvas',
assetPath: '/assets/dice-box/',
throwForce: 9,
scale: 6,
})

await Dice.init()

const notationInput = document.getElementById('notation')
const debugEl = document.getElementById('debug')
const arrowLine = document.getElementById('arrow-line')
const canvas = document.getElementById('dice-canvas')

// Screen → world mapping. With a top-down camera at (0, h, 0) the X axis
// ends up inverted versus screen space (empirically: screen-right == world -X,
// screen-down == world +Z). Normalize to [-1, 1] then flip x.
function screenToWorld(sx, sy) {
const rect = canvas.getBoundingClientRect()
const size = Dice.config.size ?? 9.5
const aspect = rect.width / rect.height
const u = (sx - rect.left) / rect.width * 2 - 1
const v = (sy - rect.top) / rect.height * 2 - 1
const xHalf = size * aspect / 2 - 0.5
const zHalf = size / 2 - 0.5
const clamp = (n, lim) => Math.max(-lim, Math.min(lim, n))
return {
x: clamp(-u * size * aspect / 2, xHalf),
z: clamp(v * size / 2, zHalf),
}
}

let dragging = null

function setDebug(obj) {
debugEl.textContent = Object.entries(obj)
.map(([k, v]) => `${k}: ${typeof v === 'number' ? v.toFixed(2) : JSON.stringify(v)}`)
.join('\n')
}

function drawArrow(x1, y1, x2, y2) {
arrowLine.setAttribute('x1', x1)
arrowLine.setAttribute('y1', y1)
arrowLine.setAttribute('x2', x2)
arrowLine.setAttribute('y2', y2)
}

function clearArrow() {
drawArrow(0, 0, 0, 0)
}

canvas.addEventListener('pointerdown', (e) => {
// ignore drags originating on the HUD
if (e.target.closest('#hud')) return
canvas.setPointerCapture(e.pointerId)
dragging = { id: e.pointerId, startX: e.clientX, startY: e.clientY, x: e.clientX, y: e.clientY }
drawArrow(e.clientX, e.clientY, e.clientX, e.clientY)
})

canvas.addEventListener('pointermove', (e) => {
if (!dragging || e.pointerId !== dragging.id) return
dragging.x = e.clientX
dragging.y = e.clientY
drawArrow(dragging.startX, dragging.startY, dragging.x, dragging.y)
})

canvas.addEventListener('pointerup', (e) => {
if (!dragging || e.pointerId !== dragging.id) return
const { startX, startY } = dragging
const endX = e.clientX
const endY = e.clientY
dragging = null
clearArrow()

const dxScreen = endX - startX
const dyScreen = endY - startY
const dragPx = Math.hypot(dxScreen, dyScreen)

const notation = notationInput.value || '1d6'
const MIN_DRAG = 8 // treat small drags as taps → use default random throw

if (dragPx < MIN_DRAG) {
setDebug({ note: 'tap — default random throw', notation })
Dice.roll(notation)
return
}

// origin = drag start, projected into the world plane
const origin = screenToWorld(startX, startY)
// direction: drag delta in screen space → world x/z
// screen-right == world -X, so flip dx; screen-down == world +Z, leave dy.
const throwDirection = [-dxScreen, 0, dyScreen]

// strength scales with drag length, normalized against a reference pixel
// distance so the mapping is resolution-independent. Clamped in the worker.
const refPx = Math.min(window.innerWidth, window.innerHeight) * 0.4
const throwStrength = Math.min(dragPx / refPx, 2.5)

setDebug({
notation,
startPx: [startX, startY],
endPx: [endX, endY],
dragPx: dragPx,
throwOrigin: [origin.x, origin.z],
throwDirection,
throwStrength,
})

Dice.roll(notation, {
throwOrigin: [origin.x, undefined, origin.z],
throwDirection,
throwStrength,
})
})

canvas.addEventListener('pointercancel', () => {
dragging = null
clearArrow()
})

Dice.onRollComplete = (results) => {
console.log('results', results)
}
18 changes: 18 additions & 0 deletions drag-demo/vite.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import path from 'path'
import { fileURLToPath } from 'url'
import { defineConfig } from 'vite'

const here = path.dirname(fileURLToPath(import.meta.url))

// Standalone dev config for the drag demo. Serves this folder and uses
// ../public as the static asset root so /assets/dice-box/... resolves.
export default defineConfig({
root: here,
publicDir: path.resolve(here, '../public'),
server: {
fs: {
// allow importing from ../src
allow: [path.resolve(here, '..')],
},
},
})
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"dev": "vite",
"build": "vite build",
"serve": "vite preview",
"demo:drag": "vite --config drag-demo/vite.config.js",
"postinstall": "node copyAssets.js"
},
"dependencies": {
Expand Down
39 changes: 31 additions & 8 deletions src/WorldFacade.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,18 @@ const defaultOptions = {
assetPath: '/assets/dice-box/', // path to 'ammo', 'themes' folders and web workers
// origin: location.origin,
origin: typeof window !== "undefined" ? window.location.origin : "",
suspendSimulation: false
suspendSimulation: false,
// throwOrigin: where dice spawn from. Accepts a named zone
// ('random' | 'center' | 'top' | 'bottom' | 'left' | 'right' | 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight')
// or an explicit [x, y, z] world position. Defaults to 'random' (existing behavior).
throwOrigin: 'random',
// throwDirection: optional [x, y, z] vector indicating the general direction to throw.
// When omitted, dice are thrown toward the opposite side of the origin (existing behavior).
throwDirection: null,
// throwStrength: per-roll multiplier on throw velocity. 1 = default, clamped in the physics worker.
throwStrength: 1,
// maxThrowStrength: ceiling applied to throwStrength in the physics worker.
maxThrowStrength: 3
}

class WorldFacade {
Expand Down Expand Up @@ -460,18 +471,21 @@ class WorldFacade {
}

// TODO: pass data with roll - such as roll name. Passed back at the end in the results
roll(notation, {theme = this.config.theme, themeColor = this.config.themeColor, newStartPoint = true} = {}) {
roll(notation, {theme = this.config.theme, themeColor = this.config.themeColor, newStartPoint = true, throwOrigin = this.config.throwOrigin, throwDirection = this.config.throwDirection, throwStrength = this.config.throwStrength} = {}) {
// note: to add to a roll on screen use .add method
// reset the offscreen worker and physics worker with each new roll
this.clear()
const collectionId = this.#collectionIndex++

this.rollCollectionData[collectionId] = new Collection({
id: collectionId,
notation,
theme,
themeColor,
newStartPoint
newStartPoint,
throwOrigin,
throwDirection,
throwStrength
})

const parsedNotation = this.createNotationArray(notation, this.themesLoadedData[theme].diceAvailable)
Expand All @@ -481,7 +495,7 @@ class WorldFacade {
return this.rollCollectionData[collectionId].promise
}

add(notation, {theme = this.config.theme, themeColor = this.config.themeColor, newStartPoint = true} = {}) {
add(notation, {theme = this.config.theme, themeColor = this.config.themeColor, newStartPoint = true, throwOrigin = this.config.throwOrigin, throwDirection = this.config.throwDirection, throwStrength = this.config.throwStrength} = {}) {

const collectionId = this.#collectionIndex++

Expand All @@ -490,7 +504,10 @@ class WorldFacade {
notation,
theme,
themeColor,
newStartPoint
newStartPoint,
throwOrigin,
throwDirection,
throwStrength
})

const parsedNotation = this.createNotationArray(notation, this.themesLoadedData[theme].diceAvailable)
Expand All @@ -500,7 +517,7 @@ class WorldFacade {
return this.rollCollectionData[collectionId].promise
}

reroll(notation, {remove = false, hide = false, newStartPoint = true} = {}) {
reroll(notation, {remove = false, hide = false, newStartPoint = true, throwOrigin, throwDirection, throwStrength} = {}) {
// TODO: add hide if you want to keep the die result for an external parser

// ensure notation is an array
Expand All @@ -514,7 +531,7 @@ class WorldFacade {
}

// .add will return a promise that will then be returned here
return this.add(cleanNotation, {newStartPoint})
return this.add(cleanNotation, {newStartPoint, throwOrigin, throwDirection, throwStrength})
}

remove(notation, {hide = false} = {}) {
Expand Down Expand Up @@ -554,6 +571,9 @@ class WorldFacade {

const collection = this.rollCollectionData[collectionId]
let newStartPoint = collection.newStartPoint
const throwOrigin = collection.throwOrigin
const throwDirection = collection.throwDirection
const throwStrength = collection.throwStrength

// loop through the number of dice in the group and roll each one
parsedNotation.forEach(async notation => {
Expand Down Expand Up @@ -655,6 +675,9 @@ class WorldFacade {
this.#DiceWorld.add({
...roll,
newStartPoint,
throwOrigin,
throwDirection,
throwStrength,
theme: extendedTheme?.systemName || theme,
meshName: extendedTheme?.meshName || meshName,
colorSuffix
Expand Down
Loading