@aethervtt/shard-text 0.0.1
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 +3 -0
- package/dist/build.d.ts +43 -0
- package/dist/build.js +106 -0
- package/dist/charset.d.ts +8 -0
- package/dist/charset.js +43 -0
- package/dist/components.d.ts +54 -0
- package/dist/components.js +118 -0
- package/dist/font.d.ts +193 -0
- package/dist/font.js +377 -0
- package/dist/importer.d.ts +40 -0
- package/dist/importer.js +193 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +140 -0
- package/dist/kerning.d.ts +8 -0
- package/dist/kerning.js +168 -0
- package/dist/layout.d.ts +84 -0
- package/dist/layout.js +297 -0
- package/dist/locale.d.ts +133 -0
- package/dist/locale.js +574 -0
- package/dist/msdf.d.ts +52 -0
- package/dist/msdf.js +1147 -0
- package/dist/opentype.d.ts +36 -0
- package/dist/plugin.d.ts +2 -0
- package/dist/plugin.js +43 -0
- package/dist/preview.d.ts +2 -0
- package/dist/preview.js +76 -0
- package/dist/render.d.ts +114 -0
- package/dist/render.js +544 -0
- package/dist/shaders.d.ts +2 -0
- package/dist/shaders.js +158 -0
- package/dist/shape.d.ts +77 -0
- package/dist/shape.js +404 -0
- package/dist/source.d.ts +44 -0
- package/dist/source.js +108 -0
- package/package.json +40 -0
package/dist/font.js
ADDED
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
import { SkylinePacker, Texture } from "@aethervtt/shard-texture";
|
|
2
|
+
import { FontSource, rasterizeGlyph } from "./source.js";
|
|
3
|
+
class FontPage {
|
|
4
|
+
font;
|
|
5
|
+
index;
|
|
6
|
+
width;
|
|
7
|
+
height;
|
|
8
|
+
/** The page's texture asset (`font.ttf#Atlas`), for imported fonts. */
|
|
9
|
+
ref;
|
|
10
|
+
/**
|
|
11
|
+
* The page's pixels, for runtime pages and fonts built in code. Its `version` bumps when glyphs
|
|
12
|
+
* are added, so the renderer re-uploads it.
|
|
13
|
+
*/
|
|
14
|
+
texture;
|
|
15
|
+
/** Made at runtime for glyphs outside the imported set. */
|
|
16
|
+
runtime;
|
|
17
|
+
/** @internal Free space, for runtime pages. */
|
|
18
|
+
packer;
|
|
19
|
+
constructor(font, index, init) {
|
|
20
|
+
this.font = font;
|
|
21
|
+
this.index = index;
|
|
22
|
+
this.width = init.width;
|
|
23
|
+
this.height = init.height;
|
|
24
|
+
this.ref = init.ref;
|
|
25
|
+
this.texture = init.texture;
|
|
26
|
+
this.runtime = init.runtime ?? false;
|
|
27
|
+
this.packer = void 0;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
class Glyph {
|
|
31
|
+
codepoint;
|
|
32
|
+
/** The font whose atlas holds the glyph (a fallback's, for fallback glyphs). */
|
|
33
|
+
font;
|
|
34
|
+
advance;
|
|
35
|
+
left;
|
|
36
|
+
bottom;
|
|
37
|
+
right;
|
|
38
|
+
top;
|
|
39
|
+
/** Index into `font.pages`. */
|
|
40
|
+
page;
|
|
41
|
+
/** Atlas box in pixels. */
|
|
42
|
+
x;
|
|
43
|
+
y;
|
|
44
|
+
w;
|
|
45
|
+
h;
|
|
46
|
+
/** Texture coordinates of the quad: (u0, v0) is its top-left. */
|
|
47
|
+
u0;
|
|
48
|
+
v0;
|
|
49
|
+
u1;
|
|
50
|
+
v1;
|
|
51
|
+
/** Generated at runtime rather than imported. */
|
|
52
|
+
runtime;
|
|
53
|
+
/** Whether it draws anything (spaces don't). */
|
|
54
|
+
visible;
|
|
55
|
+
/** @internal Range of this glyph's kerning pairs (as the left glyph) in its font's tables. */
|
|
56
|
+
kernStart = 0;
|
|
57
|
+
kernCount = 0;
|
|
58
|
+
constructor(font, json, runtime) {
|
|
59
|
+
this.font = font;
|
|
60
|
+
this.codepoint = json.u;
|
|
61
|
+
this.advance = json.a;
|
|
62
|
+
this.runtime = runtime;
|
|
63
|
+
const b = json.b;
|
|
64
|
+
const r = json.r;
|
|
65
|
+
this.page = json.p ?? 0;
|
|
66
|
+
if (b && r) {
|
|
67
|
+
this.left = b[0];
|
|
68
|
+
this.bottom = b[1];
|
|
69
|
+
this.right = b[2];
|
|
70
|
+
this.top = b[3];
|
|
71
|
+
this.x = r[0];
|
|
72
|
+
this.y = r[1];
|
|
73
|
+
this.w = r[2];
|
|
74
|
+
this.h = r[3];
|
|
75
|
+
const page = font.pages[this.page];
|
|
76
|
+
const pw = page?.width ?? 1;
|
|
77
|
+
const ph = page?.height ?? 1;
|
|
78
|
+
this.u0 = (r[0] + 0.5) / pw;
|
|
79
|
+
this.v0 = (r[1] + 0.5) / ph;
|
|
80
|
+
this.u1 = (r[0] + r[2] - 0.5) / pw;
|
|
81
|
+
this.v1 = (r[1] + r[3] - 0.5) / ph;
|
|
82
|
+
this.visible = true;
|
|
83
|
+
} else {
|
|
84
|
+
this.left = this.bottom = this.right = this.top = 0;
|
|
85
|
+
this.x = this.y = this.w = this.h = 0;
|
|
86
|
+
this.u0 = this.v0 = this.u1 = this.v1 = 0;
|
|
87
|
+
this.visible = false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const round6 = (v) => Math.round(v * 1e6) / 1e6;
|
|
92
|
+
const RUNTIME_PAGE_SIZE = 512;
|
|
93
|
+
const GLYPH_PADDING = 1;
|
|
94
|
+
class Font {
|
|
95
|
+
name;
|
|
96
|
+
size;
|
|
97
|
+
range;
|
|
98
|
+
unitsPerEm;
|
|
99
|
+
ascent;
|
|
100
|
+
descent;
|
|
101
|
+
lineGap;
|
|
102
|
+
charset;
|
|
103
|
+
pages = [];
|
|
104
|
+
/** Glyphs this font holds (imported and generated), by codepoint. */
|
|
105
|
+
glyphs = /* @__PURE__ */ new Map();
|
|
106
|
+
notdef;
|
|
107
|
+
/** Bumps when glyphs or pages are added (on-demand glyphs), or on hot reload. */
|
|
108
|
+
version = 0;
|
|
109
|
+
/** Glyphs generated at runtime so far. */
|
|
110
|
+
runtimeGlyphs = 0;
|
|
111
|
+
/** Codepoints nothing could draw (they show the missing-glyph box). */
|
|
112
|
+
missing = /* @__PURE__ */ new Set();
|
|
113
|
+
/** Fallback fonts from the import settings, before the `Fonts` store links them. */
|
|
114
|
+
fallbackRefs = [];
|
|
115
|
+
/** The metrics' fallback paths (project paths). */
|
|
116
|
+
fallbackPaths = [];
|
|
117
|
+
/** @internal The store that links this font's fallbacks (set by `FontStore`). */
|
|
118
|
+
linker;
|
|
119
|
+
/** @internal Kerning tables: right codepoints and em adjustments, grouped by left glyph. */
|
|
120
|
+
kernRight = new Int32Array(0);
|
|
121
|
+
kernValue = new Float32Array(0);
|
|
122
|
+
_fallbacks = [];
|
|
123
|
+
resolved = /* @__PURE__ */ new Map();
|
|
124
|
+
sourceBytes;
|
|
125
|
+
_source;
|
|
126
|
+
json;
|
|
127
|
+
constructor(json) {
|
|
128
|
+
this.json = json;
|
|
129
|
+
this.name = json.name;
|
|
130
|
+
this.size = json.size;
|
|
131
|
+
this.range = json.range;
|
|
132
|
+
this.unitsPerEm = json.unitsPerEm;
|
|
133
|
+
this.ascent = json.ascent;
|
|
134
|
+
this.descent = json.descent;
|
|
135
|
+
this.lineGap = json.lineGap;
|
|
136
|
+
this.charset = json.charset ?? "custom";
|
|
137
|
+
}
|
|
138
|
+
/** Builds a font from its metrics JSON and its atlas pages (pixels or texture refs). */
|
|
139
|
+
static fromMetrics(json, options = {}) {
|
|
140
|
+
const font = new Font(json);
|
|
141
|
+
font.pages = json.pages.map((p, i) => {
|
|
142
|
+
const pixels = options.pixels?.[i];
|
|
143
|
+
return new FontPage(font, i, {
|
|
144
|
+
width: p.width,
|
|
145
|
+
height: p.height,
|
|
146
|
+
ref: options.pageRefs?.[i],
|
|
147
|
+
texture: pixels ? Texture.create({
|
|
148
|
+
width: p.width,
|
|
149
|
+
height: p.height,
|
|
150
|
+
mips: [pixels],
|
|
151
|
+
usage: "data",
|
|
152
|
+
cpu: true
|
|
153
|
+
}) : void 0
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
for (const g of json.glyphs) font.glyphs.set(g.u, new Glyph(font, g, false));
|
|
157
|
+
font.notdef = new Glyph(font, json.notdef, false);
|
|
158
|
+
font.setKerning(json.kerning);
|
|
159
|
+
if (options.source instanceof FontSource) font._source = options.source;
|
|
160
|
+
else font.sourceBytes = options.source;
|
|
161
|
+
font.fallbackRefs = [...options.fallbackRefs ?? []];
|
|
162
|
+
font.fallbackPaths = [...json.fallback ?? []];
|
|
163
|
+
font.resolved = new Map(font.glyphs);
|
|
164
|
+
return font;
|
|
165
|
+
}
|
|
166
|
+
/** The metrics this font was loaded from. */
|
|
167
|
+
get metrics() {
|
|
168
|
+
return this.json;
|
|
169
|
+
}
|
|
170
|
+
/** Line height in em: ascent - descent + line gap. */
|
|
171
|
+
get lineHeight() {
|
|
172
|
+
return this.ascent - this.descent + this.lineGap;
|
|
173
|
+
}
|
|
174
|
+
get fallbacks() {
|
|
175
|
+
return this._fallbacks;
|
|
176
|
+
}
|
|
177
|
+
/** Sets the fallback fonts, tried in order. Clears the resolved-glyph cache. */
|
|
178
|
+
set fallbacks(fonts) {
|
|
179
|
+
this._fallbacks = [...fonts];
|
|
180
|
+
this.invalidate();
|
|
181
|
+
}
|
|
182
|
+
/** Forgets resolved fallback and missing glyphs (after fallbacks change). */
|
|
183
|
+
invalidate() {
|
|
184
|
+
this.resolved = new Map(this.glyphs);
|
|
185
|
+
this.missing.clear();
|
|
186
|
+
this.version++;
|
|
187
|
+
}
|
|
188
|
+
/** Whether on-demand glyphs can be generated (the font's outlines are available). */
|
|
189
|
+
get hasOutlines() {
|
|
190
|
+
return this._source !== void 0 || this.sourceBytes !== void 0;
|
|
191
|
+
}
|
|
192
|
+
/** The parsed font file, parsed on first use; undefined without outlines. */
|
|
193
|
+
get source() {
|
|
194
|
+
if (!this._source && this.sourceBytes)
|
|
195
|
+
this._source = new FontSource(this.sourceBytes, this.name);
|
|
196
|
+
return this._source;
|
|
197
|
+
}
|
|
198
|
+
/** This font's own glyph for a codepoint (imported or already generated), no fallbacks. */
|
|
199
|
+
glyph(codepoint) {
|
|
200
|
+
return this.glyphs.get(codepoint);
|
|
201
|
+
}
|
|
202
|
+
/** A glyph for any codepoint; never undefined (the missing-glyph box at worst). */
|
|
203
|
+
resolve(codepoint) {
|
|
204
|
+
const g = this.resolved.get(codepoint);
|
|
205
|
+
return g !== void 0 ? g : this.resolveSlow(codepoint);
|
|
206
|
+
}
|
|
207
|
+
/** Whether a codepoint resolves to a real glyph (here, on demand, or in a fallback). */
|
|
208
|
+
has(codepoint) {
|
|
209
|
+
return this.resolve(codepoint) !== this.notdef;
|
|
210
|
+
}
|
|
211
|
+
/** Kerning between two glyphs, in em (0 across fonts). */
|
|
212
|
+
kerning(left, right) {
|
|
213
|
+
if (left.font !== right.font || left.kernCount === 0) return 0;
|
|
214
|
+
const f = left.font;
|
|
215
|
+
const target = right.codepoint;
|
|
216
|
+
let lo = left.kernStart;
|
|
217
|
+
let hi = lo + left.kernCount - 1;
|
|
218
|
+
const keys = f.kernRight;
|
|
219
|
+
while (lo <= hi) {
|
|
220
|
+
const mid = lo + hi >> 1;
|
|
221
|
+
const k = keys[mid];
|
|
222
|
+
if (k === target) return f.kernValue[mid];
|
|
223
|
+
if (k < target) lo = mid + 1;
|
|
224
|
+
else hi = mid - 1;
|
|
225
|
+
}
|
|
226
|
+
return 0;
|
|
227
|
+
}
|
|
228
|
+
resolveSlow(codepoint) {
|
|
229
|
+
let g = this.find(codepoint, 0);
|
|
230
|
+
if (!g) {
|
|
231
|
+
g = this.notdef;
|
|
232
|
+
this.missing.add(codepoint);
|
|
233
|
+
}
|
|
234
|
+
this.resolved.set(codepoint, g);
|
|
235
|
+
return g;
|
|
236
|
+
}
|
|
237
|
+
find(codepoint, depth) {
|
|
238
|
+
const own = this.glyphs.get(codepoint);
|
|
239
|
+
if (own) return own;
|
|
240
|
+
const made = this.generate(codepoint);
|
|
241
|
+
if (made) return made;
|
|
242
|
+
if (depth < 4) {
|
|
243
|
+
for (const fb of this._fallbacks) {
|
|
244
|
+
if (fb === this) continue;
|
|
245
|
+
const g = fb.find(codepoint, depth + 1);
|
|
246
|
+
if (g) return g;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return void 0;
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Generates a glyph from this font's outlines into a runtime atlas page, if the font has the
|
|
253
|
+
* codepoint. Bumps `version` and the page texture's version.
|
|
254
|
+
*/
|
|
255
|
+
generate(codepoint) {
|
|
256
|
+
const source = this.source;
|
|
257
|
+
if (!source) return void 0;
|
|
258
|
+
const index = source.glyphIndex(codepoint);
|
|
259
|
+
if (index === 0) return void 0;
|
|
260
|
+
const bitmap = rasterizeGlyph(source.shape(index), source.unitsPerEm, this);
|
|
261
|
+
const json = { u: codepoint, a: round6(source.advance(index) / source.unitsPerEm) };
|
|
262
|
+
if (bitmap.plane) {
|
|
263
|
+
const spot = this.allocate(bitmap.width, bitmap.height);
|
|
264
|
+
const page = this.pages[spot.page];
|
|
265
|
+
const data = page.texture.levels[0];
|
|
266
|
+
for (let row = 0; row < bitmap.height; row++) {
|
|
267
|
+
data.set(
|
|
268
|
+
bitmap.pixels.subarray(row * bitmap.width * 4, (row + 1) * bitmap.width * 4),
|
|
269
|
+
((spot.y + row) * page.width + spot.x) * 4
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
page.texture.version++;
|
|
273
|
+
json.b = bitmap.plane.map(round6);
|
|
274
|
+
json.r = [spot.x, spot.y, bitmap.width, bitmap.height];
|
|
275
|
+
json.p = spot.page;
|
|
276
|
+
}
|
|
277
|
+
const glyph = new Glyph(this, json, true);
|
|
278
|
+
this.glyphs.set(codepoint, glyph);
|
|
279
|
+
this.resolved.set(codepoint, glyph);
|
|
280
|
+
this.runtimeGlyphs++;
|
|
281
|
+
this.version++;
|
|
282
|
+
return glyph;
|
|
283
|
+
}
|
|
284
|
+
/** Finds room for a w×h box on a runtime page, adding a page when the last one is full. */
|
|
285
|
+
allocate(w, h) {
|
|
286
|
+
const pad = GLYPH_PADDING;
|
|
287
|
+
const last = this.pages[this.pages.length - 1];
|
|
288
|
+
if (last?.runtime && last.packer) {
|
|
289
|
+
const spot2 = last.packer.insert(w + pad * 2, h + pad * 2);
|
|
290
|
+
if (spot2) return { page: last.index, x: spot2.x + pad, y: spot2.y + pad };
|
|
291
|
+
}
|
|
292
|
+
let side = RUNTIME_PAGE_SIZE;
|
|
293
|
+
while (side < Math.max(w, h) + pad * 2) side *= 2;
|
|
294
|
+
const pixels = new Uint8Array(side * side * 4);
|
|
295
|
+
for (let i = 3; i < pixels.length; i += 4) pixels[i] = 255;
|
|
296
|
+
const page = new FontPage(this, this.pages.length, {
|
|
297
|
+
width: side,
|
|
298
|
+
height: side,
|
|
299
|
+
runtime: true,
|
|
300
|
+
texture: Texture.create({
|
|
301
|
+
width: side,
|
|
302
|
+
height: side,
|
|
303
|
+
mips: [pixels],
|
|
304
|
+
usage: "data",
|
|
305
|
+
cpu: true
|
|
306
|
+
})
|
|
307
|
+
});
|
|
308
|
+
page.packer = new SkylinePacker(side, side);
|
|
309
|
+
this.pages.push(page);
|
|
310
|
+
const spot = page.packer.insert(w + pad * 2, h + pad * 2);
|
|
311
|
+
return { page: page.index, x: spot.x + pad, y: spot.y + pad };
|
|
312
|
+
}
|
|
313
|
+
setKerning(triples) {
|
|
314
|
+
const n = Math.floor(triples.length / 3);
|
|
315
|
+
const order = new Int32Array(n);
|
|
316
|
+
for (let i2 = 0; i2 < n; i2++) order[i2] = i2;
|
|
317
|
+
order.sort(
|
|
318
|
+
(a, b) => triples[a * 3] - triples[b * 3] || triples[a * 3 + 1] - triples[b * 3 + 1]
|
|
319
|
+
);
|
|
320
|
+
this.kernRight = new Int32Array(n);
|
|
321
|
+
this.kernValue = new Float32Array(n);
|
|
322
|
+
const inv = 1 / this.unitsPerEm;
|
|
323
|
+
let i = 0;
|
|
324
|
+
while (i < n) {
|
|
325
|
+
const left = triples[order[i] * 3];
|
|
326
|
+
const start = i;
|
|
327
|
+
while (i < n && triples[order[i] * 3] === left) {
|
|
328
|
+
this.kernRight[i] = triples[order[i] * 3 + 1];
|
|
329
|
+
this.kernValue[i] = triples[order[i] * 3 + 2] * inv;
|
|
330
|
+
i++;
|
|
331
|
+
}
|
|
332
|
+
const g = this.glyphs.get(left);
|
|
333
|
+
if (g) {
|
|
334
|
+
g.kernStart = start;
|
|
335
|
+
g.kernCount = i - start;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
/** Hot reload: takes over another font's data, keeping this object (and its fallbacks). */
|
|
340
|
+
copyFrom(next) {
|
|
341
|
+
this.json = next.json;
|
|
342
|
+
this.name = next.name;
|
|
343
|
+
this.size = next.size;
|
|
344
|
+
this.range = next.range;
|
|
345
|
+
this.unitsPerEm = next.unitsPerEm;
|
|
346
|
+
this.ascent = next.ascent;
|
|
347
|
+
this.descent = next.descent;
|
|
348
|
+
this.lineGap = next.lineGap;
|
|
349
|
+
this.charset = next.charset;
|
|
350
|
+
this.fallbackRefs = next.fallbackRefs;
|
|
351
|
+
this.fallbackPaths = next.fallbackPaths;
|
|
352
|
+
this.sourceBytes = next.sourceBytes;
|
|
353
|
+
this._source = next._source;
|
|
354
|
+
this.runtimeGlyphs = 0;
|
|
355
|
+
this.pages = next.pages.map(
|
|
356
|
+
(p) => new FontPage(this, p.index, {
|
|
357
|
+
width: p.width,
|
|
358
|
+
height: p.height,
|
|
359
|
+
ref: p.ref,
|
|
360
|
+
texture: p.texture,
|
|
361
|
+
runtime: p.runtime
|
|
362
|
+
})
|
|
363
|
+
);
|
|
364
|
+
this.glyphs = /* @__PURE__ */ new Map();
|
|
365
|
+
for (const g of next.json.glyphs) this.glyphs.set(g.u, new Glyph(this, g, false));
|
|
366
|
+
this.notdef = new Glyph(this, next.json.notdef, false);
|
|
367
|
+
this.setKerning(next.json.kerning);
|
|
368
|
+
this.invalidate();
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
export {
|
|
372
|
+
Font,
|
|
373
|
+
FontPage,
|
|
374
|
+
GLYPH_PADDING,
|
|
375
|
+
Glyph,
|
|
376
|
+
RUNTIME_PAGE_SIZE
|
|
377
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { AssetStore } from '@aethervtt/shard-assets';
|
|
2
|
+
import { type AssetRef } from '@aethervtt/shard-core';
|
|
3
|
+
import { Font } from './font.js';
|
|
4
|
+
export declare const FontImportSettings: import("@aethervtt/shard-core").ComponentDef<{
|
|
5
|
+
readonly charset: import("@aethervtt/shard-core").FieldType<"custom" | "latin" | "latin-extended", "u8">;
|
|
6
|
+
readonly customCharset: import("@aethervtt/shard-core").FieldType<string, "object">;
|
|
7
|
+
readonly size: import("@aethervtt/shard-core").FieldType<number, "u32">;
|
|
8
|
+
readonly range: import("@aethervtt/shard-core").FieldType<number, "f32">;
|
|
9
|
+
readonly fallback: import("@aethervtt/shard-core").FieldType<string[], "object">;
|
|
10
|
+
readonly kerning: import("@aethervtt/shard-core").FieldType<boolean, "u8">;
|
|
11
|
+
readonly outlines: import("@aethervtt/shard-core").FieldType<boolean, "u8">;
|
|
12
|
+
}>;
|
|
13
|
+
export interface FontImportSettingsValue {
|
|
14
|
+
charset: 'latin' | 'latin-extended' | 'custom';
|
|
15
|
+
customCharset: string;
|
|
16
|
+
size: number;
|
|
17
|
+
range: number;
|
|
18
|
+
fallback: string[];
|
|
19
|
+
kerning: boolean;
|
|
20
|
+
outlines: boolean;
|
|
21
|
+
}
|
|
22
|
+
/** Label of an atlas page's texture sub-asset: `Atlas`, then `Atlas/1`, `Atlas/2`, … */
|
|
23
|
+
export declare function atlasLabel(page: number): string;
|
|
24
|
+
export declare const FontImporter: import("@aethervtt/shard-assets").ImporterDef;
|
|
25
|
+
/**
|
|
26
|
+
* Loaded fonts by guid. Setting a font links fallback refs (from its import settings) to the
|
|
27
|
+
* fonts already loaded: the asset server loads fallbacks first, as dependencies.
|
|
28
|
+
*/
|
|
29
|
+
export declare class FontStore extends AssetStore<Font, 'Font'> {
|
|
30
|
+
private readonly fonts;
|
|
31
|
+
constructor();
|
|
32
|
+
add(item: Font, name?: string): AssetRef<'Font'>;
|
|
33
|
+
set(guid: string, item: Font): void;
|
|
34
|
+
delete(guid: string): boolean;
|
|
35
|
+
/** Points every font's `fallbacks` at its loaded fallback fonts. */
|
|
36
|
+
link(): void;
|
|
37
|
+
}
|
|
38
|
+
export declare const Fonts: import("@aethervtt/shard-core").ResourceDef<FontStore>;
|
|
39
|
+
/** Fonts load from their metrics JSON; atlas pages are `#Atlas` texture sub-assets. */
|
|
40
|
+
export declare const FontAssetType: import("@aethervtt/shard-assets").AssetTypeDef<Font>;
|
package/dist/importer.js
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AssetStore,
|
|
3
|
+
defineAssetType,
|
|
4
|
+
defineImporter
|
|
5
|
+
} from "@aethervtt/shard-assets";
|
|
6
|
+
import {
|
|
7
|
+
defineResource,
|
|
8
|
+
defineSchema,
|
|
9
|
+
ShardError,
|
|
10
|
+
t
|
|
11
|
+
} from "@aethervtt/shard-core";
|
|
12
|
+
import { writeKtx2 } from "@aethervtt/shard-texture";
|
|
13
|
+
import { buildFont } from "./build.js";
|
|
14
|
+
import { Font } from "./font.js";
|
|
15
|
+
import { FontSource } from "./source.js";
|
|
16
|
+
const FontImportSettings = defineSchema(
|
|
17
|
+
"text/FontImportSettings",
|
|
18
|
+
{
|
|
19
|
+
charset: t.enum(["latin", "latin-extended", "custom"], {
|
|
20
|
+
description: "Glyphs to put in the atlas. latin: ASCII, Latin-1, and typographic punctuation. latin-extended: plus Latin Extended-A. custom: the characters of customCharset."
|
|
21
|
+
}),
|
|
22
|
+
customCharset: t.string({ description: "The characters to include when charset is custom." }),
|
|
23
|
+
size: t.u32({ default: 48, min: 8, max: 512, description: "Atlas pixels per em." }),
|
|
24
|
+
range: t.f32({
|
|
25
|
+
default: 4,
|
|
26
|
+
min: 1,
|
|
27
|
+
max: 32,
|
|
28
|
+
description: "Distance range in atlas pixels. Larger allows wider outlines and glows, at the cost of atlas space."
|
|
29
|
+
}),
|
|
30
|
+
fallback: t.list(t.string, {
|
|
31
|
+
description: "Fonts (paths) tried in order for characters this font lacks."
|
|
32
|
+
}),
|
|
33
|
+
kerning: t.bool({ default: true, description: "Import kerning pairs." }),
|
|
34
|
+
outlines: t.bool({
|
|
35
|
+
default: true,
|
|
36
|
+
description: "Keep the font file in the artifact, so characters outside the charset are generated at runtime."
|
|
37
|
+
})
|
|
38
|
+
},
|
|
39
|
+
{ description: "Import settings for fonts (.ttf, .otf): an MSDF atlas and metrics." }
|
|
40
|
+
);
|
|
41
|
+
function atlasLabel(page) {
|
|
42
|
+
return page === 0 ? "Atlas" : `Atlas/${page}`;
|
|
43
|
+
}
|
|
44
|
+
const FontImporter = defineImporter({
|
|
45
|
+
name: "font",
|
|
46
|
+
version: 1,
|
|
47
|
+
extensions: [".ttf", ".otf"],
|
|
48
|
+
settings: FontImportSettings,
|
|
49
|
+
async import(source, ctx) {
|
|
50
|
+
const s = ctx.settings;
|
|
51
|
+
let charset = s.charset;
|
|
52
|
+
if (s.charset === "custom") {
|
|
53
|
+
if (!s.customCharset) {
|
|
54
|
+
ctx.warn('charset is "custom" but customCharset is empty; importing the latin charset.');
|
|
55
|
+
charset = "latin";
|
|
56
|
+
} else charset = s.customCharset;
|
|
57
|
+
}
|
|
58
|
+
const fallback = (s.fallback ?? []).map((p) => ctx.resolve(p));
|
|
59
|
+
let build;
|
|
60
|
+
try {
|
|
61
|
+
build = buildFont(new FontSource(source.bytes, source.path), {
|
|
62
|
+
charset,
|
|
63
|
+
size: s.size,
|
|
64
|
+
range: s.range,
|
|
65
|
+
kerning: s.kerning,
|
|
66
|
+
fallback
|
|
67
|
+
});
|
|
68
|
+
} catch (err) {
|
|
69
|
+
if (err instanceof ShardError) throw err;
|
|
70
|
+
throw new ShardError("text/import-failed", `${source.path}: ${err.message}`, {
|
|
71
|
+
path: source.path,
|
|
72
|
+
cause: err
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
if (build.missing.length > 0) {
|
|
76
|
+
const list = build.missing.slice(0, 12).map((c) => `U+${c.toString(16).toUpperCase().padStart(4, "0")}`).join(", ");
|
|
77
|
+
ctx.warn(
|
|
78
|
+
`The font lacks ${build.missing.length} charset characters (${list}${build.missing.length > 12 ? ", \u2026" : ""}); they use fallbacks or the missing-glyph box.`
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
const metrics = build.metrics;
|
|
82
|
+
const labels = build.pages.map((_, i) => atlasLabel(i));
|
|
83
|
+
const info = {
|
|
84
|
+
name: metrics.name,
|
|
85
|
+
glyphs: metrics.glyphs.length,
|
|
86
|
+
pages: build.pages.length,
|
|
87
|
+
atlas: build.pages.map((p) => `${p.width}x${p.height}`).join(", "),
|
|
88
|
+
kerningPairs: metrics.kerning.length / 3,
|
|
89
|
+
size: metrics.size,
|
|
90
|
+
range: metrics.range,
|
|
91
|
+
charset: metrics.charset ?? "custom",
|
|
92
|
+
missing: build.missing.length,
|
|
93
|
+
outlines: s.outlines !== false
|
|
94
|
+
};
|
|
95
|
+
const main = {
|
|
96
|
+
label: "",
|
|
97
|
+
type: "Font",
|
|
98
|
+
json: metrics,
|
|
99
|
+
dependencies: [...labels.map((l) => `#${l}`), ...fallback],
|
|
100
|
+
info
|
|
101
|
+
};
|
|
102
|
+
if (s.outlines !== false) main.bytes = source.bytes;
|
|
103
|
+
const assets = [main];
|
|
104
|
+
build.pages.forEach((page, i) => {
|
|
105
|
+
assets.push({
|
|
106
|
+
label: labels[i],
|
|
107
|
+
type: "Texture",
|
|
108
|
+
bytes: writeKtx2({ width: page.width, height: page.height, levels: [page.pixels] }, "data"),
|
|
109
|
+
info: {
|
|
110
|
+
width: page.width,
|
|
111
|
+
height: page.height,
|
|
112
|
+
usage: "data",
|
|
113
|
+
mips: 1,
|
|
114
|
+
font: metrics.name
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
return { assets };
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
class FontStore extends AssetStore {
|
|
122
|
+
fonts = /* @__PURE__ */ new Map();
|
|
123
|
+
constructor() {
|
|
124
|
+
super("Font");
|
|
125
|
+
}
|
|
126
|
+
add(item, name) {
|
|
127
|
+
const ref = super.add(item, name);
|
|
128
|
+
this.fonts.set(ref.guid, item);
|
|
129
|
+
item.linker = this;
|
|
130
|
+
this.link();
|
|
131
|
+
return ref;
|
|
132
|
+
}
|
|
133
|
+
set(guid, item) {
|
|
134
|
+
super.set(guid, item);
|
|
135
|
+
this.fonts.set(guid, item);
|
|
136
|
+
item.linker = this;
|
|
137
|
+
this.link();
|
|
138
|
+
}
|
|
139
|
+
delete(guid) {
|
|
140
|
+
this.fonts.delete(guid);
|
|
141
|
+
return super.delete(guid);
|
|
142
|
+
}
|
|
143
|
+
/** Points every font's `fallbacks` at its loaded fallback fonts. */
|
|
144
|
+
link() {
|
|
145
|
+
for (const font of this.fonts.values()) {
|
|
146
|
+
if (font.fallbackRefs.length === 0) continue;
|
|
147
|
+
const next = [];
|
|
148
|
+
for (const ref of font.fallbackRefs) {
|
|
149
|
+
const fb = ref.guid ? this.fonts.get(ref.guid) : void 0;
|
|
150
|
+
if (fb && fb !== font) next.push(fb);
|
|
151
|
+
}
|
|
152
|
+
const current = font.fallbacks;
|
|
153
|
+
if (next.length !== current.length || next.some((f, i) => f !== current[i])) {
|
|
154
|
+
font.fallbacks = next;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const Fonts = defineResource("text/Fonts", {
|
|
160
|
+
description: "Loaded fonts by guid.",
|
|
161
|
+
init: () => new FontStore()
|
|
162
|
+
});
|
|
163
|
+
const FontAssetType = defineAssetType("Font", {
|
|
164
|
+
store: Fonts,
|
|
165
|
+
load: (artifact, ctx) => {
|
|
166
|
+
const json = artifact.json;
|
|
167
|
+
if (json?.version !== 1 || !Array.isArray(json.glyphs)) {
|
|
168
|
+
throw new ShardError("text/invalid-metrics", `${ctx.path}: not font metrics`, {
|
|
169
|
+
path: ctx.path,
|
|
170
|
+
hint: "Re-import the font (`shard import`)."
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
const pageRefs = json.pages.map((_, i) => ctx.resolve(`#${atlasLabel(i)}`));
|
|
174
|
+
const fallbackRefs = [];
|
|
175
|
+
for (const p of json.fallback ?? []) {
|
|
176
|
+
const ref = ctx.resolve(p);
|
|
177
|
+
if (ref) fallbackRefs.push(ref);
|
|
178
|
+
}
|
|
179
|
+
return Font.fromMetrics(json, { pageRefs, source: artifact.bytes, fallbackRefs });
|
|
180
|
+
},
|
|
181
|
+
update: (existing, next) => {
|
|
182
|
+
existing.copyFrom(next);
|
|
183
|
+
existing.linker?.link();
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
export {
|
|
187
|
+
FontAssetType,
|
|
188
|
+
FontImportSettings,
|
|
189
|
+
FontImporter,
|
|
190
|
+
FontStore,
|
|
191
|
+
Fonts,
|
|
192
|
+
atlasLabel
|
|
193
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export { type PackItem, type PackResult, packRects, SkylinePacker } from '@aethervtt/shard-texture';
|
|
2
|
+
export { buildFont, type FontBuild, type FontBuildOptions, fontFromBytes } from './build.js';
|
|
3
|
+
export { type CharsetName, charsetCodepoints } from './charset.js';
|
|
4
|
+
export { Localized, SCREEN_CORNERS, ScreenText, TEXT_ALIGNS, Text } from './components.js';
|
|
5
|
+
export { Font, type FontLoadOptions, type FontMetricsJson, FontPage, GLYPH_PADDING, Glyph, type GlyphJson, RUNTIME_PAGE_SIZE, } from './font.js';
|
|
6
|
+
export { atlasLabel, FontAssetType, FontImporter, FontImportSettings, type FontImportSettingsValue, FontStore, Fonts, } from './importer.js';
|
|
7
|
+
export { hasGposKerning, readGposKerning } from './kerning.js';
|
|
8
|
+
export { isCjk, layoutText, measureText, type TextAlign, TextLayout, type TextLayoutOptions, type TextLine, type TextMetrics, } from './layout.js';
|
|
9
|
+
export { installLocalization, KEYED_COMPONENTS, Locale, LocaleState, LocaleStore, type LocaleValue, type LocalizationReport, loadStringTables, localeChain, localeMethods, localeOfPath, localizationKeysIn, localizeComponent, localizeText, PLURAL_CATEGORIES, type PluralCategory, placeholdersOf, type StringEntry, StringTable, StringTableAssetType, StringTableImporter, StringTables, setLocale, tr, validateLocalization, validateStringTable, } from './locale.js';
|
|
10
|
+
export { blitMsdf, generateMsdf, type MsdfBox, type MsdfOptions, msdfBox, prepareShape, } from './msdf.js';
|
|
11
|
+
export { textPlugin } from './plugin.js';
|
|
12
|
+
export { describeText, prepareTexts, TextRenderer, TextStore } from './render.js';
|
|
13
|
+
export { TEXT_SHADERS } from './shaders.js';
|
|
14
|
+
export { type Contour, colorEdgesSimple, type Edge, EdgeColor, type EdgeKind, type OutlineCommand, type Shape, shapeBounds, shapeFromCommands, } from './shape.js';
|
|
15
|
+
export { FontSource, type GlyphBitmap, type GlyphRaster, rasterizeGlyph, TOFU_ADVANCE, tofuShape, } from './source.js';
|