@aethervtt/shard-dice 0.0.0-stage → 0.1.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/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 +97 -0
- package/dist/effects.js +196 -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 +31 -0
- package/dist/index.js +322 -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 +174 -0
- package/dist/preview.d.ts +32 -0
- package/dist/preview.js +123 -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 +61 -0
- package/dist/skin.js +303 -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 +225 -0
- package/dist/table.js +1757 -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
package/dist/marks.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { AssetRef } from '@aethervtt/shard-core';
|
|
2
|
+
import { type Font } from '@aethervtt/shard-text';
|
|
3
|
+
import { type CellLayout } from './cells.js';
|
|
4
|
+
import type { DieGeometry } from './definition.js';
|
|
5
|
+
import { type FaceLayoutValue, type ResolvedMark } from './layout.js';
|
|
6
|
+
export interface MarkAtlas {
|
|
7
|
+
/** What it was baked from: definition hash, layout, fonts. */
|
|
8
|
+
key: string;
|
|
9
|
+
width: number;
|
|
10
|
+
height: number;
|
|
11
|
+
layout: CellLayout;
|
|
12
|
+
/** Distance range in pixels. */
|
|
13
|
+
range: number;
|
|
14
|
+
pixels: Uint8Array;
|
|
15
|
+
/** Values drawn, per face (d4 faces carry three). */
|
|
16
|
+
values: number[][];
|
|
17
|
+
/** How long the bake took, ms. */
|
|
18
|
+
ms: number;
|
|
19
|
+
}
|
|
20
|
+
export interface BakeMarksOptions {
|
|
21
|
+
/** Loaded fonts for text marks that name one. */
|
|
22
|
+
font?: (ref: AssetRef) => Font | undefined;
|
|
23
|
+
}
|
|
24
|
+
export interface Placement {
|
|
25
|
+
mark: ResolvedMark;
|
|
26
|
+
/** Center in cell pixels (y up). */
|
|
27
|
+
x: number;
|
|
28
|
+
y: number;
|
|
29
|
+
/** Mark height in pixels. */
|
|
30
|
+
height: number;
|
|
31
|
+
/** Up direction angle, radians counter-clockwise from +y. */
|
|
32
|
+
angle: number;
|
|
33
|
+
/** The flat area the mark keeps inside, in cell pixels: [x0, y0, x1, y1, …]. */
|
|
34
|
+
region: number[];
|
|
35
|
+
/** The largest height the mark's ink can take there, keeping the margin clear. */
|
|
36
|
+
fit: number;
|
|
37
|
+
}
|
|
38
|
+
/** The share of a region's inscribed radius a mark's ink keeps clear of its edges. */
|
|
39
|
+
export declare const MARK_MARGIN = 0.12;
|
|
40
|
+
/**
|
|
41
|
+
* Every mark of a die under a layout, where the bake draws it: at the definition's height, shrunk
|
|
42
|
+
* (one scale for the whole die, so its numbers match) until every mark's ink fits its region.
|
|
43
|
+
*/
|
|
44
|
+
export declare function markPlacements(g: DieGeometry, layout: FaceLayoutValue, options?: BakeMarksOptions): Placement[][];
|
|
45
|
+
/** The cache key of a bake: the definition's hash, the layout's content, and its fonts' guids. */
|
|
46
|
+
export declare function markAtlasKey(g: DieGeometry, layout: FaceLayoutValue): string;
|
|
47
|
+
/**
|
|
48
|
+
* Bakes a die's marks under a layout into one MSDF atlas: glyph outlines from the font (or the
|
|
49
|
+
* built-in numerals), pips as analytic circles, glyphs from their registered paths. Cached by
|
|
50
|
+
* (definition, layout).
|
|
51
|
+
*/
|
|
52
|
+
export declare function bakeMarks(g: DieGeometry, layout: FaceLayoutValue, options?: BakeMarksOptions): MarkAtlas;
|
|
53
|
+
/** Drops every cached bake (tests, or a host changing fonts under the same refs). */
|
|
54
|
+
export declare function clearMarkCache(): void;
|
package/dist/marks.js
ADDED
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
import {
|
|
2
|
+
generateMsdf,
|
|
3
|
+
prepareShape,
|
|
4
|
+
shapeBounds,
|
|
5
|
+
shapeFromCommands
|
|
6
|
+
} from "@aethervtt/shard-text";
|
|
7
|
+
import { cellLayout, cellPixel, MARK_RANGE } from "./cells.js";
|
|
8
|
+
import { findDiceGlyph, parseSvgPath } from "./glyphs.js";
|
|
9
|
+
import { resolveMark } from "./layout.js";
|
|
10
|
+
import { add, scale, sub } from "./math.js";
|
|
11
|
+
import { DICE_NUMERALS, NUMERAL_HEIGHT } from "./numerals.js";
|
|
12
|
+
const PIPS = {
|
|
13
|
+
1: [[0, 0]],
|
|
14
|
+
2: [
|
|
15
|
+
[-1, 1],
|
|
16
|
+
[1, -1]
|
|
17
|
+
],
|
|
18
|
+
3: [
|
|
19
|
+
[-1, 1],
|
|
20
|
+
[0, 0],
|
|
21
|
+
[1, -1]
|
|
22
|
+
],
|
|
23
|
+
4: [
|
|
24
|
+
[-1, -1],
|
|
25
|
+
[1, -1],
|
|
26
|
+
[-1, 1],
|
|
27
|
+
[1, 1]
|
|
28
|
+
],
|
|
29
|
+
5: [
|
|
30
|
+
[-1, -1],
|
|
31
|
+
[1, -1],
|
|
32
|
+
[0, 0],
|
|
33
|
+
[-1, 1],
|
|
34
|
+
[1, 1]
|
|
35
|
+
],
|
|
36
|
+
6: [
|
|
37
|
+
[-1, -1],
|
|
38
|
+
[1, -1],
|
|
39
|
+
[-1, 0],
|
|
40
|
+
[1, 0],
|
|
41
|
+
[-1, 1],
|
|
42
|
+
[1, 1]
|
|
43
|
+
],
|
|
44
|
+
7: [
|
|
45
|
+
[-1, -1],
|
|
46
|
+
[1, -1],
|
|
47
|
+
[-1, 0],
|
|
48
|
+
[0, 0],
|
|
49
|
+
[1, 0],
|
|
50
|
+
[-1, 1],
|
|
51
|
+
[1, 1]
|
|
52
|
+
],
|
|
53
|
+
8: [
|
|
54
|
+
[-1, -1],
|
|
55
|
+
[0, -1],
|
|
56
|
+
[1, -1],
|
|
57
|
+
[-1, 0],
|
|
58
|
+
[1, 0],
|
|
59
|
+
[-1, 1],
|
|
60
|
+
[0, 1],
|
|
61
|
+
[1, 1]
|
|
62
|
+
],
|
|
63
|
+
9: [
|
|
64
|
+
[-1, -1],
|
|
65
|
+
[0, -1],
|
|
66
|
+
[1, -1],
|
|
67
|
+
[-1, 0],
|
|
68
|
+
[0, 0],
|
|
69
|
+
[1, 0],
|
|
70
|
+
[-1, 1],
|
|
71
|
+
[0, 1],
|
|
72
|
+
[1, 1]
|
|
73
|
+
]
|
|
74
|
+
};
|
|
75
|
+
const numeralShapes = /* @__PURE__ */ new Map();
|
|
76
|
+
function numeral(ch) {
|
|
77
|
+
let o = numeralShapes.get(ch);
|
|
78
|
+
if (o) return o;
|
|
79
|
+
const n = DICE_NUMERALS[ch];
|
|
80
|
+
if (!n) return void 0;
|
|
81
|
+
const cmds = parseSvgPath(n.path, `numeral ${ch}`);
|
|
82
|
+
const shape = shapeFromCommands(cmds);
|
|
83
|
+
for (const c of shape.contours)
|
|
84
|
+
for (const e of c.edges) for (let i = 0; i < e.p.length; i++) e.p[i] /= 1e3;
|
|
85
|
+
o = { shape, advance: n.advance / 1e3 };
|
|
86
|
+
numeralShapes.set(ch, o);
|
|
87
|
+
return o;
|
|
88
|
+
}
|
|
89
|
+
function fontGlyph(font, ch) {
|
|
90
|
+
const source = font.source;
|
|
91
|
+
if (!source) return void 0;
|
|
92
|
+
const index = source.glyphIndex(ch.codePointAt(0));
|
|
93
|
+
if (index === 0) return void 0;
|
|
94
|
+
const shape = source.shape(index);
|
|
95
|
+
const upem = source.unitsPerEm;
|
|
96
|
+
for (const c of shape.contours)
|
|
97
|
+
for (const e of c.edges) for (let i = 0; i < e.p.length; i++) e.p[i] /= upem;
|
|
98
|
+
return { shape, advance: source.advance(index) / upem };
|
|
99
|
+
}
|
|
100
|
+
function place(out, shape, m, tx, ty) {
|
|
101
|
+
for (const c of shape.contours) {
|
|
102
|
+
out.push({
|
|
103
|
+
edges: c.edges.map((e) => {
|
|
104
|
+
const p = e.p.slice();
|
|
105
|
+
for (let i = 0; i < p.length; i += 2) {
|
|
106
|
+
const x = p[i];
|
|
107
|
+
const y = p[i + 1];
|
|
108
|
+
p[i] = m[0] * x + m[1] * y + tx;
|
|
109
|
+
p[i + 1] = m[2] * x + m[3] * y + ty;
|
|
110
|
+
}
|
|
111
|
+
return { kind: e.kind, p, color: e.color };
|
|
112
|
+
})
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function area(c) {
|
|
117
|
+
let a = 0;
|
|
118
|
+
for (const e of c.edges) {
|
|
119
|
+
const p = e.p;
|
|
120
|
+
const n = p.length;
|
|
121
|
+
a += p[0] * p[n - 1] - p[n - 2] * p[1];
|
|
122
|
+
}
|
|
123
|
+
return a / 2;
|
|
124
|
+
}
|
|
125
|
+
function rect(l, b, r, t, ccw) {
|
|
126
|
+
const pts = ccw ? [l, b, r, b, r, t, l, t] : [l, b, l, t, r, t, r, b];
|
|
127
|
+
const edges = [];
|
|
128
|
+
for (let i = 0; i < 4; i++) {
|
|
129
|
+
const j = (i + 1) % 4;
|
|
130
|
+
edges.push({
|
|
131
|
+
kind: 1,
|
|
132
|
+
p: [pts[i * 2], pts[i * 2 + 1], pts[j * 2], pts[j * 2 + 1]],
|
|
133
|
+
color: 7
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
return { edges };
|
|
137
|
+
}
|
|
138
|
+
function scaled(shape, sx, sy, dx, dy) {
|
|
139
|
+
const out = [];
|
|
140
|
+
place(out, shape, [sx, 0, 0, sy], dx, dy);
|
|
141
|
+
return out;
|
|
142
|
+
}
|
|
143
|
+
function markPieces(p, options, weightPx) {
|
|
144
|
+
const m = p.mark;
|
|
145
|
+
const turn = p.angle - m.rotation;
|
|
146
|
+
const cos = Math.cos(turn);
|
|
147
|
+
const sin = Math.sin(turn);
|
|
148
|
+
if (m.kind === "glyph") {
|
|
149
|
+
const glyph = findDiceGlyph(m.glyph);
|
|
150
|
+
if (!glyph) return [];
|
|
151
|
+
const s = p.height;
|
|
152
|
+
const contours2 = [];
|
|
153
|
+
place(
|
|
154
|
+
contours2,
|
|
155
|
+
shapeFromCommands(glyph.commands),
|
|
156
|
+
[cos * s, -sin * s, sin * s, cos * s],
|
|
157
|
+
p.x,
|
|
158
|
+
p.y
|
|
159
|
+
);
|
|
160
|
+
return [{ contours: contours2, ox: 0, oy: 0, key: void 0 }];
|
|
161
|
+
}
|
|
162
|
+
const font = m.font ? options.font?.(m.font) : void 0;
|
|
163
|
+
const chars = [];
|
|
164
|
+
for (const ch of m.text) {
|
|
165
|
+
const outline = font ? fontGlyph(font, ch) : numeral(ch);
|
|
166
|
+
if (outline) chars.push({ ch, outline });
|
|
167
|
+
}
|
|
168
|
+
if (chars.length === 0) return [];
|
|
169
|
+
const capHeight = font ? capOf(chars.map((c) => c.outline)) : NUMERAL_HEIGHT / 1e3;
|
|
170
|
+
let width = 0;
|
|
171
|
+
for (const c of chars) width += c.outline.advance;
|
|
172
|
+
const sy = p.height / capHeight;
|
|
173
|
+
const sx = sy * Math.min(1, 1.35 * p.height / (width * sy));
|
|
174
|
+
const baseline = -capHeight / 2;
|
|
175
|
+
const local = [];
|
|
176
|
+
const fontKey = m.font?.guid ?? m.font?.path ?? "numerals";
|
|
177
|
+
const size = `${sx.toFixed(3)}:${sy.toFixed(3)}:${weightPx.toFixed(2)}`;
|
|
178
|
+
let pen = -width / 2;
|
|
179
|
+
for (const c of chars) {
|
|
180
|
+
local.push({ contours: c.outline.shape.contours, x: pen, key: `${fontKey}:${c.ch}:${size}` });
|
|
181
|
+
pen += c.outline.advance;
|
|
182
|
+
}
|
|
183
|
+
let bar;
|
|
184
|
+
if (m.underline) {
|
|
185
|
+
const all = chars.flatMap((c) => c.outline.shape.contours);
|
|
186
|
+
const outer = all.reduce((a, c) => Math.abs(area(c)) > Math.abs(area(a)) ? c : a, all[0]);
|
|
187
|
+
const w = Math.min(width * 0.72, capHeight * 0.95);
|
|
188
|
+
const y = baseline - capHeight * 0.2;
|
|
189
|
+
bar = rect(-w / 2, y - capHeight * 0.09, w / 2, y, area(outer) > 0);
|
|
190
|
+
}
|
|
191
|
+
if (Math.abs(turn) < 1e-6) {
|
|
192
|
+
const pieces = local.map((l) => {
|
|
193
|
+
const ox = Math.round(p.x + l.x * sx);
|
|
194
|
+
const oy = Math.round(p.y + baseline * sy);
|
|
195
|
+
return { contours: scaled({ contours: l.contours }, sx, sy, 0, 0), ox, oy, key: l.key };
|
|
196
|
+
});
|
|
197
|
+
if (bar) {
|
|
198
|
+
const contours2 = [];
|
|
199
|
+
place(contours2, { contours: [bar] }, [sx, 0, 0, sy], 0, 0);
|
|
200
|
+
pieces.push({ contours: contours2, ox: Math.round(p.x), oy: Math.round(p.y), key: void 0 });
|
|
201
|
+
}
|
|
202
|
+
return pieces;
|
|
203
|
+
}
|
|
204
|
+
const contours = [];
|
|
205
|
+
const r = [cos * sx, -sin * sy, sin * sx, cos * sy];
|
|
206
|
+
for (const l of local) {
|
|
207
|
+
for (const c of l.contours) {
|
|
208
|
+
place(
|
|
209
|
+
contours,
|
|
210
|
+
{ contours: [c] },
|
|
211
|
+
r,
|
|
212
|
+
p.x + r[0] * (l.x + 0) + r[1] * baseline,
|
|
213
|
+
p.y + r[2] * l.x + r[3] * baseline
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (bar) place(contours, { contours: [bar] }, r, p.x, p.y);
|
|
218
|
+
return [{ contours, ox: 0, oy: 0, key: void 0 }];
|
|
219
|
+
}
|
|
220
|
+
function capOf(outlines) {
|
|
221
|
+
let top = 0;
|
|
222
|
+
for (const o of outlines) {
|
|
223
|
+
const b = shapeBounds(o.shape);
|
|
224
|
+
if (b) top = Math.max(top, b[3]);
|
|
225
|
+
}
|
|
226
|
+
return top || 0.7;
|
|
227
|
+
}
|
|
228
|
+
const tiles = /* @__PURE__ */ new Map();
|
|
229
|
+
const TILE_CACHE = 1024;
|
|
230
|
+
function bakeTile(piece, weightPx) {
|
|
231
|
+
const shape = { contours: piece.contours };
|
|
232
|
+
prepareShape(shape);
|
|
233
|
+
const bounds = shapeBounds(shape);
|
|
234
|
+
if (!bounds) return void 0;
|
|
235
|
+
const half = MARK_RANGE / 2 + 1;
|
|
236
|
+
const l = Math.floor(bounds[0] - half);
|
|
237
|
+
const b = Math.floor(bounds[1] - half);
|
|
238
|
+
const r = Math.ceil(bounds[2] + half);
|
|
239
|
+
const t = Math.ceil(bounds[3] + half);
|
|
240
|
+
const box = { width: r - l, height: t - b, tx: -l, ty: -b, plane: [l, b, r, t] };
|
|
241
|
+
const sdf = generateMsdf(shape, box, { scale: 1, range: MARK_RANGE });
|
|
242
|
+
const data = new Float32Array(box.width * box.height * 3);
|
|
243
|
+
const bias = weightPx / MARK_RANGE;
|
|
244
|
+
for (let i = 0; i < data.length; i++) data[i] = sdf[i] + bias;
|
|
245
|
+
return { l, b, width: box.width, height: box.height, data };
|
|
246
|
+
}
|
|
247
|
+
function tileOf(piece, weightPx) {
|
|
248
|
+
if (!piece.key) return bakeTile(piece, weightPx);
|
|
249
|
+
const hit = tiles.get(piece.key);
|
|
250
|
+
if (hit) return hit;
|
|
251
|
+
const tile = bakeTile(piece, weightPx);
|
|
252
|
+
if (!tile) return void 0;
|
|
253
|
+
tiles.set(piece.key, tile);
|
|
254
|
+
if (tiles.size > TILE_CACHE) tiles.delete(tiles.keys().next().value);
|
|
255
|
+
return tile;
|
|
256
|
+
}
|
|
257
|
+
const med = (a, b, c) => Math.max(Math.min(a, b), Math.min(Math.max(a, b), c));
|
|
258
|
+
const byte = (v) => v <= 0 ? 0 : v >= 1 ? 255 : Math.round(v * 255);
|
|
259
|
+
function blitTile(atlas, owner, piece, atlasWidth, cellX, cellY, cell, tile, ox, oy) {
|
|
260
|
+
for (let row = 0; row < tile.height; row++) {
|
|
261
|
+
const y = oy + tile.b + row;
|
|
262
|
+
if (y < 0 || y >= cell) continue;
|
|
263
|
+
const atlasRow = cellY + (cell - 1 - y);
|
|
264
|
+
for (let col = 0; col < tile.width; col++) {
|
|
265
|
+
const x = ox + tile.l + col;
|
|
266
|
+
if (x < 0 || x >= cell) continue;
|
|
267
|
+
const s = (row * tile.width + col) * 3;
|
|
268
|
+
const r = tile.data[s];
|
|
269
|
+
const g = tile.data[s + 1];
|
|
270
|
+
const b = tile.data[s + 2];
|
|
271
|
+
const t = atlasRow * atlasWidth + cellX + x;
|
|
272
|
+
const o = t * 4;
|
|
273
|
+
const was = owner[t];
|
|
274
|
+
owner[t] = piece;
|
|
275
|
+
if (was < 0 || was === piece) {
|
|
276
|
+
atlas[o] = byte(r);
|
|
277
|
+
atlas[o + 1] = byte(g);
|
|
278
|
+
atlas[o + 2] = byte(b);
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
const m = Math.max(byte(med(r, g, b)), med(atlas[o], atlas[o + 1], atlas[o + 2]));
|
|
282
|
+
atlas[o] = atlas[o + 1] = atlas[o + 2] = m;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
function drawPips(atlas, atlasWidth, cellX, cellY, cell, p) {
|
|
287
|
+
const layout = PIPS[p.mark.count];
|
|
288
|
+
if (!layout) return;
|
|
289
|
+
const spread = p.height * 0.62;
|
|
290
|
+
const radius = p.height * 0.21 + p.mark.weight * p.height;
|
|
291
|
+
const cos = Math.cos(p.angle - p.mark.rotation);
|
|
292
|
+
const sin = Math.sin(p.angle - p.mark.rotation);
|
|
293
|
+
const centers = layout.map(([gx, gy]) => [
|
|
294
|
+
p.x + (cos * gx - sin * gy) * spread,
|
|
295
|
+
p.y + (sin * gx + cos * gy) * spread
|
|
296
|
+
]);
|
|
297
|
+
for (let y = 0; y < cell; y++) {
|
|
298
|
+
const atlasRow = cellY + (cell - 1 - y);
|
|
299
|
+
const py = y + 0.5;
|
|
300
|
+
for (let x = 0; x < cell; x++) {
|
|
301
|
+
const px = x + 0.5;
|
|
302
|
+
let d = Number.NEGATIVE_INFINITY;
|
|
303
|
+
for (const c of centers)
|
|
304
|
+
d = Math.max(d, radius - Math.sqrt((px - c[0]) ** 2 + (py - c[1]) ** 2));
|
|
305
|
+
const v = d / MARK_RANGE + 0.5;
|
|
306
|
+
const byte2 = v <= 0 ? 0 : v >= 1 ? 255 : Math.round(v * 255);
|
|
307
|
+
const o = (atlasRow * atlasWidth + cellX + x) * 4;
|
|
308
|
+
if (byte2 > atlas[o]) atlas[o] = atlas[o + 1] = atlas[o + 2] = byte2;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
const MARK_MARGIN = 0.12;
|
|
313
|
+
function regions(g, layout, faceIndex) {
|
|
314
|
+
const face = layout.faces[faceIndex];
|
|
315
|
+
const poly = g.polytope.faces[faceIndex];
|
|
316
|
+
const bevel = g.definition.bevel;
|
|
317
|
+
const flat = poly.vertices.map((v) => {
|
|
318
|
+
const p = g.polytope.points[v];
|
|
319
|
+
return cellPixel(layout, face, add(p, scale(sub(face.center, p), bevel)));
|
|
320
|
+
});
|
|
321
|
+
if (face.value !== void 0) return [{ value: face.value, poly: flat.flat(), angle: 0 }];
|
|
322
|
+
const c = layout.cell / 2;
|
|
323
|
+
const n = flat.length;
|
|
324
|
+
return poly.vertices.map((v, i) => {
|
|
325
|
+
const [x, y] = flat[i];
|
|
326
|
+
const [px, py] = flat[(i + n - 1) % n];
|
|
327
|
+
const [nx, ny] = flat[(i + 1) % n];
|
|
328
|
+
return {
|
|
329
|
+
value: g.vertexValues[v],
|
|
330
|
+
poly: [c, c, (x + nx) / 2, (y + ny) / 2, x, y, (x + px) / 2, (y + py) / 2],
|
|
331
|
+
angle: Math.atan2(-(x - c), y - c)
|
|
332
|
+
};
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
function edgesOf(poly) {
|
|
336
|
+
const n = poly.length / 2;
|
|
337
|
+
let twice = 0;
|
|
338
|
+
for (let i = 0; i < n; i++) {
|
|
339
|
+
const j = (i + 1) % n;
|
|
340
|
+
twice += poly[i * 2] * poly[j * 2 + 1] - poly[j * 2] * poly[i * 2 + 1];
|
|
341
|
+
}
|
|
342
|
+
const sign = twice > 0 ? 1 : -1;
|
|
343
|
+
const out = [];
|
|
344
|
+
for (let i = 0; i < n; i++) {
|
|
345
|
+
const j = (i + 1) % n;
|
|
346
|
+
const dx = poly[j * 2] - poly[i * 2];
|
|
347
|
+
const dy = poly[j * 2 + 1] - poly[i * 2 + 1];
|
|
348
|
+
const len = Math.sqrt(dx * dx + dy * dy);
|
|
349
|
+
if (len < 1e-9) continue;
|
|
350
|
+
const nx = sign * dy / len;
|
|
351
|
+
const ny = -sign * dx / len;
|
|
352
|
+
out.push(nx, ny, nx * poly[i * 2] + ny * poly[i * 2 + 1]);
|
|
353
|
+
}
|
|
354
|
+
return out;
|
|
355
|
+
}
|
|
356
|
+
function clearance(edges, x, y) {
|
|
357
|
+
let m = Number.POSITIVE_INFINITY;
|
|
358
|
+
for (let e = 0; e < edges.length; e += 3)
|
|
359
|
+
m = Math.min(m, edges[e + 2] - edges[e] * x - edges[e + 1] * y);
|
|
360
|
+
return m;
|
|
361
|
+
}
|
|
362
|
+
function golden(f, a, b) {
|
|
363
|
+
const k = (Math.sqrt(5) - 1) / 2;
|
|
364
|
+
let lo = a;
|
|
365
|
+
let hi = b;
|
|
366
|
+
for (let i = 0; i < 28; i++) {
|
|
367
|
+
const m1 = hi - k * (hi - lo);
|
|
368
|
+
const m2 = lo + k * (hi - lo);
|
|
369
|
+
if (f(m1) < f(m2)) lo = m1;
|
|
370
|
+
else hi = m2;
|
|
371
|
+
}
|
|
372
|
+
return (lo + hi) / 2;
|
|
373
|
+
}
|
|
374
|
+
function deepest(edges, x, y, span) {
|
|
375
|
+
let bx = x;
|
|
376
|
+
let by = y;
|
|
377
|
+
for (let round = 0; round < 3; round++) {
|
|
378
|
+
by = golden((t) => clearance(edges, bx, t), by - span, by + span);
|
|
379
|
+
bx = golden((t) => clearance(edges, t, by), bx - span, bx + span);
|
|
380
|
+
}
|
|
381
|
+
return clearance(edges, bx, by) > clearance(edges, x, y) * 1.02 ? [bx, by] : [x, y];
|
|
382
|
+
}
|
|
383
|
+
function inkOf(m, options) {
|
|
384
|
+
if (m.kind === "blank") return void 0;
|
|
385
|
+
if (m.kind === "pips") {
|
|
386
|
+
const layout = PIPS[m.count];
|
|
387
|
+
if (!layout) return void 0;
|
|
388
|
+
return { points: layout.flatMap(([x, y]) => [x * 0.62, y * 0.62]), grow: 0.21 + m.weight };
|
|
389
|
+
}
|
|
390
|
+
if (m.kind === "glyph") {
|
|
391
|
+
const glyph = findDiceGlyph(m.glyph);
|
|
392
|
+
if (!glyph) return void 0;
|
|
393
|
+
const points2 = [];
|
|
394
|
+
for (const c of shapeFromCommands(glyph.commands).contours)
|
|
395
|
+
for (const e of c.edges) for (let i = 0; i < e.p.length; i++) points2.push(e.p[i]);
|
|
396
|
+
return { points: points2, grow: 0 };
|
|
397
|
+
}
|
|
398
|
+
const font = m.font ? options.font?.(m.font) : void 0;
|
|
399
|
+
const outlines = [];
|
|
400
|
+
for (const ch of m.text) {
|
|
401
|
+
const o = font ? fontGlyph(font, ch) : numeral(ch);
|
|
402
|
+
if (o) outlines.push(o);
|
|
403
|
+
}
|
|
404
|
+
if (outlines.length === 0) return void 0;
|
|
405
|
+
const cap = font ? capOf(outlines) : NUMERAL_HEIGHT / 1e3;
|
|
406
|
+
let width = 0;
|
|
407
|
+
for (const o of outlines) width += o.advance;
|
|
408
|
+
const sy = 1 / cap;
|
|
409
|
+
const sx = sy * Math.min(1, 1.35 / (width * sy));
|
|
410
|
+
const points = [];
|
|
411
|
+
let pen = -width / 2;
|
|
412
|
+
for (const o of outlines) {
|
|
413
|
+
for (const c of o.shape.contours) {
|
|
414
|
+
for (const e of c.edges) {
|
|
415
|
+
for (let i = 0; i < e.p.length; i += 2)
|
|
416
|
+
points.push((e.p[i] + pen) * sx, (e.p[i + 1] - cap / 2) * sy);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
pen += o.advance;
|
|
420
|
+
}
|
|
421
|
+
if (m.underline) {
|
|
422
|
+
const w = Math.min(width * 0.72, cap * 0.95) / 2;
|
|
423
|
+
const bottom = -0.5 - 0.29;
|
|
424
|
+
points.push(-w * sx, bottom, w * sx, bottom);
|
|
425
|
+
}
|
|
426
|
+
return { points, grow: m.weight / cap };
|
|
427
|
+
}
|
|
428
|
+
function fitHeight(edges, ink, x, y, turn, margin) {
|
|
429
|
+
const c = Math.cos(turn);
|
|
430
|
+
const s = Math.sin(turn);
|
|
431
|
+
let h = Number.POSITIVE_INFINITY;
|
|
432
|
+
for (let e = 0; e < edges.length; e += 3) {
|
|
433
|
+
const nx = edges[e];
|
|
434
|
+
const ny = edges[e + 1];
|
|
435
|
+
let reach = Number.NEGATIVE_INFINITY;
|
|
436
|
+
for (let i = 0; i < ink.points.length; i += 2) {
|
|
437
|
+
const px = ink.points[i];
|
|
438
|
+
const py = ink.points[i + 1];
|
|
439
|
+
reach = Math.max(reach, nx * (c * px - s * py) + ny * (s * px + c * py));
|
|
440
|
+
}
|
|
441
|
+
reach += ink.grow;
|
|
442
|
+
const room = edges[e + 2] - nx * x - ny * y - margin;
|
|
443
|
+
if (reach > 1e-9) h = Math.min(h, room / reach);
|
|
444
|
+
}
|
|
445
|
+
return Math.max(0, h);
|
|
446
|
+
}
|
|
447
|
+
function placements(g, layout, faceIndex, marks, options) {
|
|
448
|
+
const def = g.definition;
|
|
449
|
+
const inner = layout.cell * (1 - 2 * layout.pad);
|
|
450
|
+
const height = (def.markScale ?? 0.4) * inner;
|
|
451
|
+
return regions(g, layout, faceIndex).map((r) => {
|
|
452
|
+
const mark = resolveMark(marks, def, r.value);
|
|
453
|
+
const edges = edgesOf(r.poly);
|
|
454
|
+
let cx = 0;
|
|
455
|
+
let cy = 0;
|
|
456
|
+
for (let i = 0; i < r.poly.length; i += 2) {
|
|
457
|
+
cx += r.poly[i];
|
|
458
|
+
cy += r.poly[i + 1];
|
|
459
|
+
}
|
|
460
|
+
cx /= r.poly.length / 2;
|
|
461
|
+
cy /= r.poly.length / 2;
|
|
462
|
+
const [x, y] = deepest(edges, cx, cy, inner * 0.35);
|
|
463
|
+
const ink = inkOf(mark, options);
|
|
464
|
+
const margin = MARK_MARGIN * clearance(edges, x, y);
|
|
465
|
+
const fit = ink ? fitHeight(edges, ink, x, y, r.angle - mark.rotation, margin) : Infinity;
|
|
466
|
+
return { mark, x, y, height: height * mark.size, angle: r.angle, region: r.poly, fit };
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
function markPlacements(g, layout, options = {}) {
|
|
470
|
+
const cells = cellLayout(g);
|
|
471
|
+
const all = cells.faces.map((face) => placements(g, cells, face.face, layout, options));
|
|
472
|
+
let k = 1;
|
|
473
|
+
for (const face of all) for (const p of face) if (p.height > 0) k = Math.min(k, p.fit / p.height);
|
|
474
|
+
if (k < 1) for (const face of all) for (const p of face) p.height *= k;
|
|
475
|
+
return all;
|
|
476
|
+
}
|
|
477
|
+
const cache = /* @__PURE__ */ new Map();
|
|
478
|
+
const CACHE_SIZE = 24;
|
|
479
|
+
function markAtlasKey(g, layout) {
|
|
480
|
+
return `${g.hash.toString(16)}:${JSON.stringify(layout)}`;
|
|
481
|
+
}
|
|
482
|
+
function bakeMarks(g, layout, options = {}) {
|
|
483
|
+
const key = markAtlasKey(g, layout);
|
|
484
|
+
const hit = cache.get(key);
|
|
485
|
+
if (hit) {
|
|
486
|
+
cache.delete(key);
|
|
487
|
+
cache.set(key, hit);
|
|
488
|
+
return hit;
|
|
489
|
+
}
|
|
490
|
+
const start = performance.now();
|
|
491
|
+
const cells = cellLayout(g);
|
|
492
|
+
const width = cells.cols * cells.cell;
|
|
493
|
+
const height = cells.rows * cells.cell;
|
|
494
|
+
const pixels = new Uint8Array(width * height * 4);
|
|
495
|
+
for (let i = 3; i < pixels.length; i += 4) pixels[i] = 255;
|
|
496
|
+
const values = [];
|
|
497
|
+
const placed = markPlacements(g, layout, options);
|
|
498
|
+
const owner = new Int32Array(width * height).fill(-1);
|
|
499
|
+
let pieces = 0;
|
|
500
|
+
for (const face of cells.faces) {
|
|
501
|
+
const cellX = face.col * cells.cell;
|
|
502
|
+
const cellY = face.row * cells.cell;
|
|
503
|
+
for (const p of placed[face.face]) {
|
|
504
|
+
if (p.mark.kind === "blank") continue;
|
|
505
|
+
if (p.mark.kind === "pips") {
|
|
506
|
+
drawPips(pixels, width, cellX, cellY, cells.cell, p);
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
const weightPx = p.mark.weight * (p.height / (NUMERAL_HEIGHT / 1e3));
|
|
510
|
+
for (const piece of markPieces(p, options, weightPx)) {
|
|
511
|
+
const tile = tileOf(piece, weightPx);
|
|
512
|
+
if (!tile) continue;
|
|
513
|
+
blitTile(pixels, owner, pieces++, width, cellX, cellY, cells.cell, tile, piece.ox, piece.oy);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
values.push(
|
|
517
|
+
face.value !== void 0 ? [face.value] : g.polytope.faces[face.face].vertices.map((v) => g.vertexValues[v])
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
const atlas = {
|
|
521
|
+
key,
|
|
522
|
+
width,
|
|
523
|
+
height,
|
|
524
|
+
layout: cells,
|
|
525
|
+
range: MARK_RANGE,
|
|
526
|
+
pixels,
|
|
527
|
+
values,
|
|
528
|
+
ms: performance.now() - start
|
|
529
|
+
};
|
|
530
|
+
cache.set(key, atlas);
|
|
531
|
+
if (cache.size > CACHE_SIZE) cache.delete(cache.keys().next().value);
|
|
532
|
+
return atlas;
|
|
533
|
+
}
|
|
534
|
+
function clearMarkCache() {
|
|
535
|
+
cache.clear();
|
|
536
|
+
}
|
|
537
|
+
export {
|
|
538
|
+
MARK_MARGIN,
|
|
539
|
+
bakeMarks,
|
|
540
|
+
clearMarkCache,
|
|
541
|
+
markAtlasKey,
|
|
542
|
+
markPlacements
|
|
543
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { type Fields } from '@aethervtt/shard-core';
|
|
2
|
+
import { type BlendMode, findMaterialType, type MaterialType } from '@aethervtt/shard-render';
|
|
3
|
+
/** Fields every dice family has, before its own. */
|
|
4
|
+
export declare const DICE_FIELDS: {
|
|
5
|
+
marks: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").AssetRef<"Texture"> | null, "object">;
|
|
6
|
+
markColor: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").Color, "f32">;
|
|
7
|
+
markEmissive: import("@aethervtt/shard-core").FieldType<number, "f32">;
|
|
8
|
+
markRoughness: import("@aethervtt/shard-core").FieldType<number, "f32">;
|
|
9
|
+
markMetallic: import("@aethervtt/shard-core").FieldType<number, "f32">;
|
|
10
|
+
markDepth: import("@aethervtt/shard-core").FieldType<number, "f32">;
|
|
11
|
+
edgeColor: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").Color, "f32">;
|
|
12
|
+
edgeMix: import("@aethervtt/shard-core").FieldType<number, "f32">;
|
|
13
|
+
droppedColor: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").Color, "f32">;
|
|
14
|
+
dropped: import("@aethervtt/shard-core").FieldType<number, "f32">;
|
|
15
|
+
fade: import("@aethervtt/shard-core").FieldType<number, "f32">;
|
|
16
|
+
result: import("@aethervtt/shard-core").FieldType<number, "f32">;
|
|
17
|
+
resultTime: import("@aethervtt/shard-core").FieldType<number, "f32">;
|
|
18
|
+
};
|
|
19
|
+
/** The shared WGSL: marks from the atlas, the engraving, the chamfer, dropped and fade. */
|
|
20
|
+
export declare const DICE_SHADERS: Record<string, string>;
|
|
21
|
+
/** What a family adds to the dice fields. */
|
|
22
|
+
export interface DiceFamilyOptions {
|
|
23
|
+
/** The family's own fields (names can't clash with the dice or standard fields). */
|
|
24
|
+
fields?: Fields;
|
|
25
|
+
/**
|
|
26
|
+
* WGSL shading the body, run inside the family's generated `pbr_input` with `p` (a PbrInput from
|
|
27
|
+
* the standard fields), `m` (DiceMark), `look` (DiceLook), `in`, `local` (object-space
|
|
28
|
+
* position), `value` (the die's value), `v` (toward the camera) and `F` (this family's uniform)
|
|
29
|
+
* in scope, and `globals.time` (seconds) to animate by. Helpers: `dice_hash`, `dice_noise` and
|
|
30
|
+
* `dice_fbm` (object-space noise), `dice_luma`, and `dice_local_ray(in, local)`, the view ray
|
|
31
|
+
* inside the die. The shared `dice_finish` runs after it. Leave it out and give `shader` instead
|
|
32
|
+
* to write the whole module.
|
|
33
|
+
*/
|
|
34
|
+
surface?: string;
|
|
35
|
+
/**
|
|
36
|
+
* The body moves with `globals.time` (a vortex turning, a pulsar sweeping): while its dice are on
|
|
37
|
+
* the table, frames keep coming, at the display's rate (`true`) or at most `fps`. Without it the
|
|
38
|
+
* table draws nothing once dice rest, and a time-driven body holds still. Reduced motion stops it.
|
|
39
|
+
*/
|
|
40
|
+
animated?: boolean | {
|
|
41
|
+
fps: number;
|
|
42
|
+
};
|
|
43
|
+
/** A shader module with the family's own hook overrides (instead of `surface`). */
|
|
44
|
+
shader?: string;
|
|
45
|
+
/**
|
|
46
|
+
* A fixed blend mode, for families that only work blended (an additive glow). Their dice blend in
|
|
47
|
+
* every tier, so they don't batch in large pools. Omitted, dice follow their material's
|
|
48
|
+
* alphaMode: see-through families (`translucent`) blend, but draw opaque in large pools.
|
|
49
|
+
*/
|
|
50
|
+
blend?: BlendMode;
|
|
51
|
+
/**
|
|
52
|
+
* Whether a skin's parameters make these dice see-through (their material blends, so they cast
|
|
53
|
+
* no shadow and get a contact blob instead). Families with a fixed transparent blend always are.
|
|
54
|
+
*/
|
|
55
|
+
translucent?: (params: Record<string, unknown>) => boolean;
|
|
56
|
+
description?: string;
|
|
57
|
+
}
|
|
58
|
+
export interface DiceFamily {
|
|
59
|
+
name: string;
|
|
60
|
+
type: MaterialType;
|
|
61
|
+
/** The generated module (families defined with `surface`), for the dice plugin to register. */
|
|
62
|
+
module: {
|
|
63
|
+
path: string;
|
|
64
|
+
source: string;
|
|
65
|
+
} | undefined;
|
|
66
|
+
translucent: ((params: Record<string, unknown>) => boolean) | undefined;
|
|
67
|
+
/** Frames per second the body animates at while shown: 0 still, Infinity the display's rate. */
|
|
68
|
+
fps: number;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Declares a dice family: `defineMaterial` extending the standard material with the dice fields
|
|
72
|
+
* required. Skins name it by `name`. Aether's nebula, plasma, stained glass and black hole are
|
|
73
|
+
* such families, defined in Aether.
|
|
74
|
+
*/
|
|
75
|
+
export declare function defineDiceFamily(name: string, options: DiceFamilyOptions): DiceFamily;
|
|
76
|
+
export declare function findDiceFamily(name: string): DiceFamily | undefined;
|
|
77
|
+
export declare function allDiceFamilies(): DiceFamily[];
|
|
78
|
+
/** Whether a material type has every dice field (a family defined some other way still renders). */
|
|
79
|
+
export declare function isDiceMaterial(type: MaterialType): boolean;
|
|
80
|
+
export { findMaterialType };
|