@aethervtt/shard-dice 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +2 -2
- package/dist/attachments.d.ts +79 -0
- package/dist/attachments.js +43 -0
- package/dist/audio.d.ts +19 -0
- package/dist/audio.js +187 -0
- package/dist/builtins.d.ts +25 -0
- package/dist/builtins.js +335 -0
- package/dist/cells.d.ts +33 -0
- package/dist/cells.js +53 -0
- package/dist/components.d.ts +14 -0
- package/dist/components.js +27 -0
- package/dist/definition-asset.d.ts +19 -0
- package/dist/definition-asset.js +60 -0
- package/dist/definition.d.ts +98 -0
- package/dist/definition.js +320 -0
- package/dist/dice-worker.d.ts +1 -0
- package/dist/dice-worker.js +3 -0
- package/dist/effects.d.ts +99 -0
- package/dist/effects.js +200 -0
- package/dist/entrances.d.ts +48 -0
- package/dist/entrances.js +54 -0
- package/dist/families.d.ts +17 -0
- package/dist/families.js +131 -0
- package/dist/glyphs.d.ts +29 -0
- package/dist/glyphs.js +270 -0
- package/dist/golden.json +17 -0
- package/dist/hull.d.ts +32 -0
- package/dist/hull.js +154 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.js +324 -0
- package/dist/landing.d.ts +29 -0
- package/dist/landing.js +93 -0
- package/dist/layout.d.ts +75 -0
- package/dist/layout.js +137 -0
- package/dist/marks.d.ts +54 -0
- package/dist/marks.js +543 -0
- package/dist/material.d.ts +80 -0
- package/dist/material.js +343 -0
- package/dist/math.d.ts +39 -0
- package/dist/math.js +136 -0
- package/dist/mesh.d.ts +9 -0
- package/dist/mesh.js +105 -0
- package/dist/methods.d.ts +2 -0
- package/dist/methods.js +42 -0
- package/dist/numerals.d.ts +9 -0
- package/dist/numerals.js +47 -0
- package/dist/plugin.d.ts +41 -0
- package/dist/plugin.js +176 -0
- package/dist/preview.d.ts +32 -0
- package/dist/preview.js +135 -0
- package/dist/quality.d.ts +12 -0
- package/dist/quality.js +18 -0
- package/dist/resources.d.ts +85 -0
- package/dist/resources.js +191 -0
- package/dist/roll.d.ts +68 -0
- package/dist/roll.js +97 -0
- package/dist/settle.d.ts +53 -0
- package/dist/settle.js +96 -0
- package/dist/skin.d.ts +64 -0
- package/dist/skin.js +313 -0
- package/dist/sound.d.ts +33 -0
- package/dist/sound.js +296 -0
- package/dist/studio.d.ts +3 -0
- package/dist/studio.js +48 -0
- package/dist/table.d.ts +227 -0
- package/dist/table.js +1818 -0
- package/dist/test-worker.d.ts +1 -0
- package/dist/test-worker.js +12 -0
- package/dist/track.d.ts +70 -0
- package/dist/track.js +304 -0
- package/dist/tray.d.ts +2 -0
- package/dist/tray.js +43 -0
- package/dist/variants.d.ts +9 -0
- package/dist/variants.js +46 -0
- package/dist/windows.d.ts +25 -0
- package/dist/windows.js +77 -0
- package/dist/worker.d.ts +6 -0
- package/dist/worker.js +9 -0
- package/package.json +47 -4
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { ShardError } from "@aethervtt/shard-core";
|
|
2
|
+
import { DICE_BUDGETS } from "./attachments.js";
|
|
3
|
+
const MAX_ENTRANCES = 2;
|
|
4
|
+
const entrances = /* @__PURE__ */ new Map();
|
|
5
|
+
function budget(name, message) {
|
|
6
|
+
return new ShardError("dice/entrance-budget", `Entrance "${name}" ${message}`, {
|
|
7
|
+
hint: "Trim the scene, or raise DICE_BUDGETS with setDiceBudgets before defining it."
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
function defineDiceEntrance(name, def) {
|
|
11
|
+
if (!(def.vertices > 0) || def.vertices > DICE_BUDGETS.entranceVertices) {
|
|
12
|
+
throw budget(
|
|
13
|
+
name,
|
|
14
|
+
`declares ${def.vertices} vertices; the budget is ${DICE_BUDGETS.entranceVertices}`
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
if (!(def.durationMs > 0) || def.durationMs > DICE_BUDGETS.entranceMs) {
|
|
18
|
+
throw budget(name, `runs ${def.durationMs} ms; the budget is ${DICE_BUDGETS.entranceMs}`);
|
|
19
|
+
}
|
|
20
|
+
if (!(def.landAtMs >= 0 && def.landAtMs <= def.durationMs)) {
|
|
21
|
+
throw new ShardError(
|
|
22
|
+
"dice/invalid-entrance",
|
|
23
|
+
`Entrance "${name}" lands at ${def.landAtMs} ms, outside its ${def.durationMs} ms`,
|
|
24
|
+
{ hint: "landAtMs is when the die lands: between 0 and durationMs.", path: "landAtMs" }
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
const spot = def.spot;
|
|
28
|
+
if (spot && spot !== "center" && !(Math.abs(spot[0]) <= 1 && Math.abs(spot[1]) <= 1)) {
|
|
29
|
+
throw new ShardError("dice/invalid-entrance", `Entrance "${name}" wants a spot off the tray`, {
|
|
30
|
+
hint: "spot is 'center', or [u, v] with each in \u22121..1 of the tray.",
|
|
31
|
+
path: "spot"
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
const existing = entrances.get(name);
|
|
35
|
+
if (existing && existing !== def) {
|
|
36
|
+
throw new ShardError("dice/registry-conflict", `Entrance "${name}" is already defined`, {
|
|
37
|
+
hint: "Give the new entrance its own name."
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
entrances.set(name, def);
|
|
41
|
+
return def;
|
|
42
|
+
}
|
|
43
|
+
function findDiceEntrance(name) {
|
|
44
|
+
return entrances.get(name);
|
|
45
|
+
}
|
|
46
|
+
function allDiceEntrances() {
|
|
47
|
+
return [...entrances.keys()].sort();
|
|
48
|
+
}
|
|
49
|
+
export {
|
|
50
|
+
MAX_ENTRANCES,
|
|
51
|
+
allDiceEntrances,
|
|
52
|
+
defineDiceEntrance,
|
|
53
|
+
findDiceEntrance
|
|
54
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Opaque, one color, a few flecks in the pour. */
|
|
2
|
+
export declare const SOLID: import("./material.js").DiceFamily;
|
|
3
|
+
/**
|
|
4
|
+
* Translucent-looking resin without transparency: a tonal depth fake. Looking straight into a
|
|
5
|
+
* face sees deep color, swirled through the body; grazing angles and the chamfer catch light like
|
|
6
|
+
* thin resin does. The face on top glows a little once landed.
|
|
7
|
+
*/
|
|
8
|
+
export declare const RESIN: import("./material.js").DiceFamily;
|
|
9
|
+
/** Metal: tinted by baseColor, brushed, with polished worn edges and enamel marks. */
|
|
10
|
+
export declare const METAL: import("./material.js").DiceFamily;
|
|
11
|
+
/**
|
|
12
|
+
* Glass: blended, clear through the middle of a face and bright at grazing angles, with opaque
|
|
13
|
+
* marks. Blended dice cast no shadow, so they get a contact blob on the tray. Always see-through,
|
|
14
|
+
* but not a fixed blend: the large-pool tier draws it opaque, so its dice batch.
|
|
15
|
+
*/
|
|
16
|
+
export declare const GLASS: import("./material.js").DiceFamily;
|
|
17
|
+
export declare const BUILTIN_FAMILIES: readonly [import("./material.js").DiceFamily, import("./material.js").DiceFamily, import("./material.js").DiceFamily, import("./material.js").DiceFamily];
|
package/dist/families.js
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { t } from "@aethervtt/shard-core";
|
|
2
|
+
import { defineDiceFamily } from "./material.js";
|
|
3
|
+
const SOLID = defineDiceFamily("dice/SolidDice", {
|
|
4
|
+
fields: {
|
|
5
|
+
fleck: t.f32({
|
|
6
|
+
default: 0.25,
|
|
7
|
+
min: 0,
|
|
8
|
+
max: 1,
|
|
9
|
+
description: "How many flecks show in the body."
|
|
10
|
+
}),
|
|
11
|
+
fleckColor: t.color({ default: [1, 1, 1, 1], description: "Fleck color (linear)." }),
|
|
12
|
+
fleckScale: t.f32({
|
|
13
|
+
default: 16,
|
|
14
|
+
min: 1,
|
|
15
|
+
description: "Fleck density, per unit of die radius."
|
|
16
|
+
})
|
|
17
|
+
},
|
|
18
|
+
surface: `
|
|
19
|
+
let speck = smoothstep(0.8, 0.88, dice_noise(local * F.fleckScale));
|
|
20
|
+
p.base_color = mix(p.base_color, F.fleckColor.rgb, speck * F.fleck * m.face);`,
|
|
21
|
+
description: "Solid dice: an opaque body, optional flecks."
|
|
22
|
+
});
|
|
23
|
+
const RESIN = defineDiceFamily("dice/ResinDice", {
|
|
24
|
+
fields: {
|
|
25
|
+
resinDeep: t.color({
|
|
26
|
+
default: [0.02, 0.18, 0.2, 1],
|
|
27
|
+
description: "The deep tone, looking into the body."
|
|
28
|
+
}),
|
|
29
|
+
resinGlow: t.color({
|
|
30
|
+
default: [0.3, 0.9, 0.82, 1],
|
|
31
|
+
description: "The light tone, at thin edges."
|
|
32
|
+
}),
|
|
33
|
+
resinDepth: t.f32({
|
|
34
|
+
default: 0.85,
|
|
35
|
+
min: 0,
|
|
36
|
+
max: 1,
|
|
37
|
+
description: "How much of the tones replace baseColor."
|
|
38
|
+
}),
|
|
39
|
+
resinSwirl: t.f32({ default: 0.55, min: 0, max: 1, description: "Swirl through the pour." }),
|
|
40
|
+
resinScale: t.f32({
|
|
41
|
+
default: 2.4,
|
|
42
|
+
min: 0.1,
|
|
43
|
+
description: "Swirl size, per unit of die radius."
|
|
44
|
+
}),
|
|
45
|
+
resinLight: t.f32({
|
|
46
|
+
default: 2600,
|
|
47
|
+
min: 0,
|
|
48
|
+
unit: "cd/m\xB2",
|
|
49
|
+
description: "Light at thin edges."
|
|
50
|
+
}),
|
|
51
|
+
resinResultGlow: t.f32({
|
|
52
|
+
default: 3e3,
|
|
53
|
+
min: 0,
|
|
54
|
+
unit: "cd/m\xB2",
|
|
55
|
+
description: "Glow of the marks on top once landed."
|
|
56
|
+
}),
|
|
57
|
+
resinOpacity: t.f32({
|
|
58
|
+
default: 1,
|
|
59
|
+
min: 0.2,
|
|
60
|
+
max: 1,
|
|
61
|
+
description: "Below 1, the resin is see-through: its material blends (no shadow; a contact blob instead)."
|
|
62
|
+
})
|
|
63
|
+
},
|
|
64
|
+
surface: `
|
|
65
|
+
let nv = clamp(dot(p.normal, v), 0.0, 1.0);
|
|
66
|
+
let swirl = dice_fbm(local * F.resinScale + vec3f(value * 1.7));
|
|
67
|
+
let depth = clamp(pow(nv, 0.6) * (1.0 - 0.5 * F.resinSwirl) + swirl * F.resinSwirl * 0.6, 0.0, 1.0);
|
|
68
|
+
let tone = mix(F.resinGlow.rgb, F.resinDeep.rgb, depth);
|
|
69
|
+
p.base_color = mix(p.base_color, tone, F.resinDepth);
|
|
70
|
+
let thin = pow(1.0 - nv, 3.0) + (1.0 - m.face) * 0.45;
|
|
71
|
+
p.emissive += F.resinGlow.rgb * F.resinLight * thin;
|
|
72
|
+
p.emissive += look.mark_color.rgb * F.resinResultGlow * look.result * m.top * m.coverage;
|
|
73
|
+
p.roughness = min(p.roughness, 0.22);
|
|
74
|
+
p.alpha = F.resinOpacity;`,
|
|
75
|
+
translucent: (params) => typeof params.resinOpacity === "number" && params.resinOpacity < 1,
|
|
76
|
+
description: "Resin dice: a tonal depth fake, swirl, glowing thin edges, a lit result face."
|
|
77
|
+
});
|
|
78
|
+
const METAL = defineDiceFamily("dice/MetalDice", {
|
|
79
|
+
fields: {
|
|
80
|
+
metalWear: t.f32({ default: 0.6, min: 0, max: 1, description: "Polished, brighter edges." }),
|
|
81
|
+
metalGrain: t.f32({
|
|
82
|
+
default: 0.4,
|
|
83
|
+
min: 0,
|
|
84
|
+
max: 1,
|
|
85
|
+
description: "Brushed streaks in the roughness."
|
|
86
|
+
})
|
|
87
|
+
},
|
|
88
|
+
surface: `
|
|
89
|
+
p.metallic = 1.0;
|
|
90
|
+
let grain = dice_noise(local * vec3f(70.0, 2.5, 70.0));
|
|
91
|
+
p.roughness = clamp(p.roughness * (1.0 + (grain - 0.5) * F.metalGrain), 0.05, 1.0);
|
|
92
|
+
let edge = 1.0 - m.face;
|
|
93
|
+
p.roughness = mix(p.roughness, p.roughness * 0.35, edge * F.metalWear);
|
|
94
|
+
p.base_color = mix(p.base_color, min(p.base_color * 1.3 + vec3f(0.04), vec3f(1.0)), edge * F.metalWear);`,
|
|
95
|
+
description: "Metal dice: brushed, worn bright at the edges, enamel marks."
|
|
96
|
+
});
|
|
97
|
+
const GLASS = defineDiceFamily("dice/GlassDice", {
|
|
98
|
+
fields: {
|
|
99
|
+
glassClarity: t.f32({
|
|
100
|
+
default: 0.8,
|
|
101
|
+
min: 0,
|
|
102
|
+
max: 1,
|
|
103
|
+
description: "How clear the body is face-on."
|
|
104
|
+
}),
|
|
105
|
+
glassSparkle: t.f32({
|
|
106
|
+
default: 0.5,
|
|
107
|
+
min: 0,
|
|
108
|
+
max: 1,
|
|
109
|
+
description: "Inner glints as the die turns."
|
|
110
|
+
})
|
|
111
|
+
},
|
|
112
|
+
translucent: () => true,
|
|
113
|
+
surface: `
|
|
114
|
+
let nv = clamp(dot(p.normal, v), 0.0, 1.0);
|
|
115
|
+
let fresnel = pow(1.0 - nv, 2.5);
|
|
116
|
+
p.alpha = mix(1.0 - F.glassClarity, 0.9, fresnel) * m.face + (1.0 - m.face) * 0.55;
|
|
117
|
+
p.roughness = min(p.roughness, 0.08);
|
|
118
|
+
p.metallic = 0.0;
|
|
119
|
+
let glint = pow(dice_noise(local * 5.0 + v * 2.5), 10.0) * F.glassSparkle;
|
|
120
|
+
p.emissive += p.base_color * glint * 9000.0;
|
|
121
|
+
p.alpha = max(p.alpha, min(1.0, glint * 2.0));`,
|
|
122
|
+
description: "Glass dice: clear bodies, bright rims, opaque marks."
|
|
123
|
+
});
|
|
124
|
+
const BUILTIN_FAMILIES = [SOLID, RESIN, METAL, GLASS];
|
|
125
|
+
export {
|
|
126
|
+
BUILTIN_FAMILIES,
|
|
127
|
+
GLASS,
|
|
128
|
+
METAL,
|
|
129
|
+
RESIN,
|
|
130
|
+
SOLID
|
|
131
|
+
};
|
package/dist/glyphs.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { OutlineCommand } from '@aethervtt/shard-text';
|
|
2
|
+
export interface DiceGlyphDef {
|
|
3
|
+
/** SVG path data (M L H V C S Q T A Z, absolute or relative), in `viewBox` units, y down. */
|
|
4
|
+
path: string;
|
|
5
|
+
/** min-x, min-y, width, height. Default 0 0 100 100. */
|
|
6
|
+
viewBox?: [number, number, number, number];
|
|
7
|
+
}
|
|
8
|
+
export interface DiceGlyph {
|
|
9
|
+
name: string;
|
|
10
|
+
/** The outline in glyph units: y up, the viewBox's middle at the origin, its height 1. */
|
|
11
|
+
commands: OutlineCommand[];
|
|
12
|
+
/** Width over height of the viewBox. */
|
|
13
|
+
aspect: number;
|
|
14
|
+
}
|
|
15
|
+
/** SVG path data to outline commands (still y down, in path units). */
|
|
16
|
+
export declare function parseSvgPath(d: string, name?: string): OutlineCommand[];
|
|
17
|
+
/** Maps commands through (x, y) → (a·x + b, c·y + d). */
|
|
18
|
+
declare function transform(cmds: OutlineCommand[], a: number, b: number, c: number, d: number): OutlineCommand[];
|
|
19
|
+
/**
|
|
20
|
+
* Registers a glyph a face layout can name. The same path again is a no-op; a different one under
|
|
21
|
+
* a taken name throws `dice/registry-conflict`.
|
|
22
|
+
*/
|
|
23
|
+
export declare function defineDiceGlyph(name: string, def: DiceGlyphDef): DiceGlyph;
|
|
24
|
+
export declare function findDiceGlyph(name: string): DiceGlyph | undefined;
|
|
25
|
+
export declare function allDiceGlyphs(): string[];
|
|
26
|
+
export { transform as transformOutline };
|
|
27
|
+
/** Built-in glyphs, in a 100 × 100 box. */
|
|
28
|
+
export declare const BUILTIN_GLYPHS: Readonly<Record<string, DiceGlyphDef>>;
|
|
29
|
+
export declare function registerBuiltinGlyphs(): void;
|
package/dist/glyphs.js
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { ShardError } from "@aethervtt/shard-core";
|
|
2
|
+
const glyphs = /* @__PURE__ */ new Map();
|
|
3
|
+
function parseError(name, message) {
|
|
4
|
+
return new ShardError("dice/invalid-glyph", `Glyph "${name}": ${message}`, {
|
|
5
|
+
hint: "Glyph paths are SVG path data (M, L, H, V, C, S, Q, T, A, Z)."
|
|
6
|
+
});
|
|
7
|
+
}
|
|
8
|
+
function arcToCubics(out, x1, y1, rxIn, ryIn, angle, large, sweep, x2, y2) {
|
|
9
|
+
if (x1 === x2 && y1 === y2) return;
|
|
10
|
+
let rx = Math.abs(rxIn);
|
|
11
|
+
let ry = Math.abs(ryIn);
|
|
12
|
+
if (rx === 0 || ry === 0) {
|
|
13
|
+
out.push({ type: "L", x: x2, y: y2 });
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
const phi = angle * Math.PI / 180;
|
|
17
|
+
const cos = Math.cos(phi);
|
|
18
|
+
const sin = Math.sin(phi);
|
|
19
|
+
const dx = (x1 - x2) / 2;
|
|
20
|
+
const dy = (y1 - y2) / 2;
|
|
21
|
+
const xp = cos * dx + sin * dy;
|
|
22
|
+
const yp = -sin * dx + cos * dy;
|
|
23
|
+
const lambda = xp * xp / (rx * rx) + yp * yp / (ry * ry);
|
|
24
|
+
if (lambda > 1) {
|
|
25
|
+
rx *= Math.sqrt(lambda);
|
|
26
|
+
ry *= Math.sqrt(lambda);
|
|
27
|
+
}
|
|
28
|
+
const num = rx * rx * ry * ry - rx * rx * yp * yp - ry * ry * xp * xp;
|
|
29
|
+
const den = rx * rx * yp * yp + ry * ry * xp * xp;
|
|
30
|
+
const k = (large === sweep ? -1 : 1) * Math.sqrt(Math.max(0, num / den));
|
|
31
|
+
const cxp = k * rx * yp / ry;
|
|
32
|
+
const cyp = -k * ry * xp / rx;
|
|
33
|
+
const cx = cos * cxp - sin * cyp + (x1 + x2) / 2;
|
|
34
|
+
const cy = sin * cxp + cos * cyp + (y1 + y2) / 2;
|
|
35
|
+
const vAngle = (ux, uy, vx, vy) => Math.atan2(ux * vy - uy * vx, ux * vx + uy * vy);
|
|
36
|
+
const t1 = vAngle(1, 0, (xp - cxp) / rx, (yp - cyp) / ry);
|
|
37
|
+
let dt = vAngle((xp - cxp) / rx, (yp - cyp) / ry, (-xp - cxp) / rx, (-yp - cyp) / ry);
|
|
38
|
+
if (!sweep && dt > 0) dt -= Math.PI * 2;
|
|
39
|
+
if (sweep && dt < 0) dt += Math.PI * 2;
|
|
40
|
+
const segments = Math.ceil(Math.abs(dt) / (Math.PI / 2));
|
|
41
|
+
const step = dt / segments;
|
|
42
|
+
const kappa = 4 / 3 * Math.tan(step / 4);
|
|
43
|
+
const point = (t) => {
|
|
44
|
+
const ex = rx * Math.cos(t);
|
|
45
|
+
const ey = ry * Math.sin(t);
|
|
46
|
+
return [cos * ex - sin * ey + cx, sin * ex + cos * ey + cy];
|
|
47
|
+
};
|
|
48
|
+
const deriv = (t) => {
|
|
49
|
+
const ex = -rx * Math.sin(t);
|
|
50
|
+
const ey = ry * Math.cos(t);
|
|
51
|
+
return [cos * ex - sin * ey, sin * ex + cos * ey];
|
|
52
|
+
};
|
|
53
|
+
for (let i = 0; i < segments; i++) {
|
|
54
|
+
const a = t1 + i * step;
|
|
55
|
+
const b = a + step;
|
|
56
|
+
const [ax, ay] = point(a);
|
|
57
|
+
const [bx, by] = point(b);
|
|
58
|
+
const [dax, day] = deriv(a);
|
|
59
|
+
const [dbx, dby] = deriv(b);
|
|
60
|
+
out.push({
|
|
61
|
+
type: "C",
|
|
62
|
+
x1: ax + kappa * dax,
|
|
63
|
+
y1: ay + kappa * day,
|
|
64
|
+
x2: bx - kappa * dbx,
|
|
65
|
+
y2: by - kappa * dby,
|
|
66
|
+
x: bx,
|
|
67
|
+
y: by
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function parseSvgPath(d, name = "path") {
|
|
72
|
+
const tokens = d.match(/[a-zA-Z]|-?(?:\d+\.?\d*|\.\d+)(?:e[-+]?\d+)?/g) ?? [];
|
|
73
|
+
const out = [];
|
|
74
|
+
let i = 0;
|
|
75
|
+
let cmd = "";
|
|
76
|
+
let x = 0;
|
|
77
|
+
let y = 0;
|
|
78
|
+
let sx = 0;
|
|
79
|
+
let sy = 0;
|
|
80
|
+
let cx = 0;
|
|
81
|
+
let cy = 0;
|
|
82
|
+
let prev = "";
|
|
83
|
+
const num = () => {
|
|
84
|
+
const t = tokens[i++];
|
|
85
|
+
if (t === void 0 || /[a-zA-Z]/.test(t))
|
|
86
|
+
throw parseError(name, `expected a number after ${cmd}`);
|
|
87
|
+
return Number(t);
|
|
88
|
+
};
|
|
89
|
+
const flag = () => num() !== 0;
|
|
90
|
+
while (i < tokens.length) {
|
|
91
|
+
const t = tokens[i];
|
|
92
|
+
if (/[a-zA-Z]/.test(t)) {
|
|
93
|
+
cmd = t;
|
|
94
|
+
i++;
|
|
95
|
+
} else if (!cmd) throw parseError(name, "path data must start with a command");
|
|
96
|
+
const rel = cmd === cmd.toLowerCase();
|
|
97
|
+
const ox = rel ? x : 0;
|
|
98
|
+
const oy = rel ? y : 0;
|
|
99
|
+
switch (cmd.toUpperCase()) {
|
|
100
|
+
case "M": {
|
|
101
|
+
x = ox + num();
|
|
102
|
+
y = oy + num();
|
|
103
|
+
sx = x;
|
|
104
|
+
sy = y;
|
|
105
|
+
out.push({ type: "M", x, y });
|
|
106
|
+
cmd = rel ? "l" : "L";
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
case "L":
|
|
110
|
+
x = ox + num();
|
|
111
|
+
y = oy + num();
|
|
112
|
+
out.push({ type: "L", x, y });
|
|
113
|
+
break;
|
|
114
|
+
case "H":
|
|
115
|
+
x = ox + num();
|
|
116
|
+
out.push({ type: "L", x, y });
|
|
117
|
+
break;
|
|
118
|
+
case "V":
|
|
119
|
+
y = oy + num();
|
|
120
|
+
out.push({ type: "L", x, y });
|
|
121
|
+
break;
|
|
122
|
+
case "C": {
|
|
123
|
+
const x1 = ox + num();
|
|
124
|
+
const y1 = oy + num();
|
|
125
|
+
cx = ox + num();
|
|
126
|
+
cy = oy + num();
|
|
127
|
+
x = ox + num();
|
|
128
|
+
y = oy + num();
|
|
129
|
+
out.push({ type: "C", x1, y1, x2: cx, y2: cy, x, y });
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
case "S": {
|
|
133
|
+
const reflect = prev === "C" || prev === "S";
|
|
134
|
+
const x1 = reflect ? 2 * x - cx : x;
|
|
135
|
+
const y1 = reflect ? 2 * y - cy : y;
|
|
136
|
+
cx = ox + num();
|
|
137
|
+
cy = oy + num();
|
|
138
|
+
x = ox + num();
|
|
139
|
+
y = oy + num();
|
|
140
|
+
out.push({ type: "C", x1, y1, x2: cx, y2: cy, x, y });
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
case "Q":
|
|
144
|
+
cx = ox + num();
|
|
145
|
+
cy = oy + num();
|
|
146
|
+
x = ox + num();
|
|
147
|
+
y = oy + num();
|
|
148
|
+
out.push({ type: "Q", x1: cx, y1: cy, x, y });
|
|
149
|
+
break;
|
|
150
|
+
case "T": {
|
|
151
|
+
const reflect = prev === "Q" || prev === "T";
|
|
152
|
+
cx = reflect ? 2 * x - cx : x;
|
|
153
|
+
cy = reflect ? 2 * y - cy : y;
|
|
154
|
+
x = ox + num();
|
|
155
|
+
y = oy + num();
|
|
156
|
+
out.push({ type: "Q", x1: cx, y1: cy, x, y });
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
case "A": {
|
|
160
|
+
const rx = num();
|
|
161
|
+
const ry = num();
|
|
162
|
+
const angle = num();
|
|
163
|
+
const large = flag();
|
|
164
|
+
const sweep = flag();
|
|
165
|
+
const ex = ox + num();
|
|
166
|
+
const ey = oy + num();
|
|
167
|
+
arcToCubics(out, x, y, rx, ry, angle, large, sweep, ex, ey);
|
|
168
|
+
x = ex;
|
|
169
|
+
y = ey;
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
case "Z":
|
|
173
|
+
out.push({ type: "Z" });
|
|
174
|
+
x = sx;
|
|
175
|
+
y = sy;
|
|
176
|
+
break;
|
|
177
|
+
default:
|
|
178
|
+
throw parseError(name, `unknown command ${cmd}`);
|
|
179
|
+
}
|
|
180
|
+
prev = cmd.toUpperCase();
|
|
181
|
+
}
|
|
182
|
+
return out;
|
|
183
|
+
}
|
|
184
|
+
function transform(cmds, a, b, c, d) {
|
|
185
|
+
return cmds.map((k) => {
|
|
186
|
+
switch (k.type) {
|
|
187
|
+
case "M":
|
|
188
|
+
case "L":
|
|
189
|
+
return { type: k.type, x: a * k.x + b, y: c * k.y + d };
|
|
190
|
+
case "Q":
|
|
191
|
+
return { type: "Q", x1: a * k.x1 + b, y1: c * k.y1 + d, x: a * k.x + b, y: c * k.y + d };
|
|
192
|
+
case "C":
|
|
193
|
+
return {
|
|
194
|
+
type: "C",
|
|
195
|
+
x1: a * k.x1 + b,
|
|
196
|
+
y1: c * k.y1 + d,
|
|
197
|
+
x2: a * k.x2 + b,
|
|
198
|
+
y2: c * k.y2 + d,
|
|
199
|
+
x: a * k.x + b,
|
|
200
|
+
y: c * k.y + d
|
|
201
|
+
};
|
|
202
|
+
default:
|
|
203
|
+
return k;
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
function defineDiceGlyph(name, def) {
|
|
208
|
+
const [vx, vy, vw, vh] = def.viewBox ?? [0, 0, 100, 100];
|
|
209
|
+
const existing = glyphs.get(name);
|
|
210
|
+
const commands = transform(
|
|
211
|
+
parseSvgPath(def.path, name),
|
|
212
|
+
1 / vh,
|
|
213
|
+
-(vx + vw / 2) / vh,
|
|
214
|
+
-1 / vh,
|
|
215
|
+
(vy + vh / 2) / vh
|
|
216
|
+
);
|
|
217
|
+
const glyph = { name, commands, aspect: vw / vh };
|
|
218
|
+
if (existing) {
|
|
219
|
+
if (JSON.stringify(existing.commands) === JSON.stringify(commands)) return existing;
|
|
220
|
+
throw new ShardError(
|
|
221
|
+
"dice/registry-conflict",
|
|
222
|
+
`Glyph "${name}" is already defined, differently`,
|
|
223
|
+
{
|
|
224
|
+
hint: "Give the new glyph its own name."
|
|
225
|
+
}
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
glyphs.set(name, glyph);
|
|
229
|
+
return glyph;
|
|
230
|
+
}
|
|
231
|
+
function findDiceGlyph(name) {
|
|
232
|
+
return glyphs.get(name);
|
|
233
|
+
}
|
|
234
|
+
function allDiceGlyphs() {
|
|
235
|
+
return [...glyphs.keys()].sort();
|
|
236
|
+
}
|
|
237
|
+
const star = (points, outer, inner, turn = 0) => {
|
|
238
|
+
const out = [];
|
|
239
|
+
for (let i = 0; i < points * 2; i++) {
|
|
240
|
+
const a = -Math.PI / 2 + turn + i * Math.PI / points;
|
|
241
|
+
const r = i % 2 === 0 ? outer : inner;
|
|
242
|
+
out.push(`${(50 + Math.cos(a) * r).toFixed(2)} ${(50 + Math.sin(a) * r).toFixed(2)}`);
|
|
243
|
+
}
|
|
244
|
+
return `M${out.join("L")}Z`;
|
|
245
|
+
};
|
|
246
|
+
const BUILTIN_GLYPHS = {
|
|
247
|
+
star: { path: star(5, 48, 20) },
|
|
248
|
+
sun: { path: star(12, 48, 33) },
|
|
249
|
+
moon: { path: "M62 6A44 44 0 1 0 94 70A36 36 0 1 1 62 6Z" },
|
|
250
|
+
heart: {
|
|
251
|
+
path: "M50 90C22 68 4 52 4 32C4 16 16 6 29 6C39 6 46 12 50 20C54 12 61 6 71 6C84 6 96 16 96 32C96 52 78 68 50 90Z"
|
|
252
|
+
},
|
|
253
|
+
crown: { path: "M6 82L12 26L32 52L50 14L68 52L88 26L94 82Z" },
|
|
254
|
+
diamond: { path: "M50 2L90 50L50 98L10 50Z" }
|
|
255
|
+
};
|
|
256
|
+
let registered = false;
|
|
257
|
+
function registerBuiltinGlyphs() {
|
|
258
|
+
if (registered) return;
|
|
259
|
+
registered = true;
|
|
260
|
+
for (const [name, def] of Object.entries(BUILTIN_GLYPHS)) defineDiceGlyph(name, def);
|
|
261
|
+
}
|
|
262
|
+
export {
|
|
263
|
+
BUILTIN_GLYPHS,
|
|
264
|
+
allDiceGlyphs,
|
|
265
|
+
defineDiceGlyph,
|
|
266
|
+
findDiceGlyph,
|
|
267
|
+
parseSvgPath,
|
|
268
|
+
registerBuiltinGlyphs,
|
|
269
|
+
transform as transformOutline
|
|
270
|
+
};
|
package/dist/golden.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"note": "0054's golden dice track: this roll (default tray 5.4 x 3.15, seed = its id) records this trackHash inline, in a worker, in Node and in Chromium.",
|
|
3
|
+
"roll": {
|
|
4
|
+
"id": "golden-roll",
|
|
5
|
+
"dice": [
|
|
6
|
+
{ "kind": "d4", "value": 3 },
|
|
7
|
+
{ "kind": "d6", "value": 5 },
|
|
8
|
+
{ "kind": "d8", "value": 2 },
|
|
9
|
+
{ "kind": "d10", "value": 7 },
|
|
10
|
+
{ "kind": "d12", "value": 11 },
|
|
11
|
+
{ "kind": "d20", "value": 20, "dropped": true },
|
|
12
|
+
{ "kind": "percentile", "value": 42 },
|
|
13
|
+
{ "kind": "d100", "value": 63 }
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
"hash": "bf6ea022"
|
|
17
|
+
}
|
package/dist/hull.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type Q4, type V3 } from './math.js';
|
|
2
|
+
export interface Face {
|
|
3
|
+
/** Vertex indices, counter-clockwise seen from outside. */
|
|
4
|
+
vertices: number[];
|
|
5
|
+
normal: V3;
|
|
6
|
+
center: V3;
|
|
7
|
+
}
|
|
8
|
+
export interface Polytope {
|
|
9
|
+
points: V3[];
|
|
10
|
+
faces: Face[];
|
|
11
|
+
}
|
|
12
|
+
export declare function pointsOf(flat: ArrayLike<number>): V3[];
|
|
13
|
+
/**
|
|
14
|
+
* The faces of the convex hull of `points`: every plane through three points with all the others
|
|
15
|
+
* on one side, its coplanar points gathered into one polygon. Faces are sorted by normal, so the
|
|
16
|
+
* result doesn't depend on the order of `points` beyond their indices.
|
|
17
|
+
*/
|
|
18
|
+
export declare function convexFaces(points: readonly V3[]): Face[];
|
|
19
|
+
/**
|
|
20
|
+
* The polytope whose faces are the tangent planes at unit `normals` (distance 1 from the center):
|
|
21
|
+
* a ball die's flattened cells. Each face's vertices are the corners where three planes meet,
|
|
22
|
+
* one per triangle of the points' hull.
|
|
23
|
+
*/
|
|
24
|
+
export declare function tangentPolytope(normals: readonly V3[]): Polytope;
|
|
25
|
+
/** Whether rotating `points` by `q` gives the same set. */
|
|
26
|
+
export declare function isSymmetry(points: readonly V3[], q: Q4): boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Every proper rotation that maps the point set onto itself, sorted by their canonical form. An
|
|
29
|
+
* anchor triple is sent to every triple of matching pair distances; the rotations that also map
|
|
30
|
+
* every other point onto the set are the group.
|
|
31
|
+
*/
|
|
32
|
+
export declare function symmetryGroup(points: readonly V3[]): Q4[];
|