@weasel-js/font 1.9.3 → 1.10.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/README.md +13 -3
- package/dist/{chunk-OYLAXZOV.js → chunk-NM6K6M4Y.js} +529 -250
- package/dist/chunk-NM6K6M4Y.js.map +1 -0
- package/dist/index.d.ts +142 -77
- package/dist/index.js +153 -5
- package/dist/index.js.map +1 -1
- package/dist/{test-seams-Df2Zqwk8.d.ts → test-seams-BvULFsbo.d.ts} +171 -230
- package/dist/test-seams.d.ts +1 -1
- package/dist/test-seams.js +1 -1
- package/package.json +2 -2
- package/dist/chunk-OYLAXZOV.js.map +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createReflectable } from '@weasel-js/registry';
|
|
1
|
+
import { createEmitter, createReflectable } from '@weasel-js/registry';
|
|
2
2
|
|
|
3
3
|
// src/faceMetrics.ts
|
|
4
4
|
var finite = (n) => typeof n === "number" && Number.isFinite(n);
|
|
@@ -97,6 +97,19 @@ function parseBmFont(raw) {
|
|
|
97
97
|
if (!Array.isArray(r.chars)) throw new Error("parseBmFont: chars must be an array");
|
|
98
98
|
const info = r.info;
|
|
99
99
|
const common = r.common;
|
|
100
|
+
const positive = [
|
|
101
|
+
["info.size", info.size],
|
|
102
|
+
["common.scaleW", common.scaleW],
|
|
103
|
+
["common.scaleH", common.scaleH]
|
|
104
|
+
];
|
|
105
|
+
for (const [name, value] of positive) {
|
|
106
|
+
if (typeof value !== "number" || !(value > 0) || !Number.isFinite(value)) {
|
|
107
|
+
throw new Error(`parseBmFont: ${name} must be a positive number`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (typeof common.base !== "number" || !Number.isFinite(common.base)) {
|
|
111
|
+
throw new Error("parseBmFont: common.base must be a number");
|
|
112
|
+
}
|
|
100
113
|
const chars = r.chars;
|
|
101
114
|
const kernings = Array.isArray(r.kernings) ? r.kernings : [];
|
|
102
115
|
const charMap = /* @__PURE__ */ new Map();
|
|
@@ -123,25 +136,29 @@ function parseBmFont(raw) {
|
|
|
123
136
|
...typeof range === "number" && range > 0 ? { distanceRange: range } : {}
|
|
124
137
|
};
|
|
125
138
|
}
|
|
126
|
-
|
|
127
|
-
// src/glyphReady.ts
|
|
128
|
-
var subscribers = /* @__PURE__ */ new Set();
|
|
139
|
+
var ready = createEmitter();
|
|
129
140
|
var generation = 0;
|
|
130
141
|
function glyphGeneration() {
|
|
131
142
|
return generation;
|
|
132
143
|
}
|
|
133
144
|
function subscribeGlyphReady(cb) {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
145
|
+
return ready.subscribe(cb);
|
|
146
|
+
}
|
|
147
|
+
var faces = 0;
|
|
148
|
+
function faceGeneration() {
|
|
149
|
+
return faces;
|
|
138
150
|
}
|
|
139
151
|
function notifyGlyphReady() {
|
|
140
152
|
generation++;
|
|
141
|
-
|
|
153
|
+
faces++;
|
|
154
|
+
ready.emit();
|
|
155
|
+
}
|
|
156
|
+
function notifyGlyphBaked() {
|
|
157
|
+
generation++;
|
|
158
|
+
ready.emit();
|
|
142
159
|
}
|
|
143
160
|
function _clearGlyphReadySubscribers() {
|
|
144
|
-
|
|
161
|
+
ready = createEmitter();
|
|
145
162
|
}
|
|
146
163
|
|
|
147
164
|
// src/fallback.ts
|
|
@@ -169,6 +186,9 @@ function claimFallbackWarning(key) {
|
|
|
169
186
|
warnedFallbacks.add(key);
|
|
170
187
|
return true;
|
|
171
188
|
}
|
|
189
|
+
function fallbackWarningClaimed(key) {
|
|
190
|
+
return warnedFallbacks.has(key);
|
|
191
|
+
}
|
|
172
192
|
function _clearFallbackWarnings() {
|
|
173
193
|
warnedFallbacks.clear();
|
|
174
194
|
}
|
|
@@ -178,26 +198,115 @@ function _resetFallbackForTests() {
|
|
|
178
198
|
warnedFallbacks.clear();
|
|
179
199
|
}
|
|
180
200
|
|
|
181
|
-
// src/
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
201
|
+
// src/dynamic/dynamicPages.ts
|
|
202
|
+
var PAGE_SIZE = 1024;
|
|
203
|
+
var MAX_PAGES = 4;
|
|
204
|
+
var PageSet = class {
|
|
205
|
+
constructor(format) {
|
|
206
|
+
this.format = format;
|
|
207
|
+
}
|
|
208
|
+
format;
|
|
209
|
+
pages = [];
|
|
210
|
+
// Per-GL-cache upload progress: last page version each cache has seen.
|
|
211
|
+
// WeakMap-keyed so disposed renderers don't pin anything.
|
|
212
|
+
uploaded = /* @__PURE__ */ new WeakMap();
|
|
213
|
+
textureId(page) {
|
|
214
|
+
return `${this.format.idPrefix}${page}`;
|
|
215
|
+
}
|
|
216
|
+
/** Copy a `w`×`h` block of texels into page `pageIndex` at `(x, y)`. */
|
|
217
|
+
blit(pageIndex, x, y, w, h, bytes) {
|
|
218
|
+
const n = this.format.channels;
|
|
219
|
+
while (this.pages.length <= pageIndex) {
|
|
220
|
+
this.pages.push({ data: new Uint8Array(PAGE_SIZE * PAGE_SIZE * n), version: 0, patches: [] });
|
|
221
|
+
}
|
|
222
|
+
const page = this.pages[pageIndex];
|
|
223
|
+
for (let row = 0; row < h; row++) {
|
|
224
|
+
page.data.set(bytes.subarray(row * w * n, (row + 1) * w * n), ((y + row) * PAGE_SIZE + x) * n);
|
|
225
|
+
}
|
|
226
|
+
page.version++;
|
|
227
|
+
page.patches.push({ seq: page.version, x, y, w, h });
|
|
228
|
+
}
|
|
229
|
+
/** Zero every page. One whole-page patch stands in for all the patches before it. */
|
|
230
|
+
clear() {
|
|
231
|
+
for (const page of this.pages) {
|
|
232
|
+
page.data.fill(0);
|
|
233
|
+
page.version++;
|
|
234
|
+
page.patches = [{ seq: page.version, x: 0, y: 0, w: PAGE_SIZE, h: PAGE_SIZE }];
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
/** Bring `cache`'s copy of page `pageIndex` up to date: a full upload the
|
|
238
|
+
* first time, patches after. Returns false if the page doesn't exist yet. */
|
|
239
|
+
sync(cache, pageIndex) {
|
|
240
|
+
const page = this.pages[pageIndex];
|
|
241
|
+
if (!page) return false;
|
|
242
|
+
const n = this.format.channels;
|
|
243
|
+
const id = this.textureId(pageIndex);
|
|
244
|
+
let seen = this.uploaded.get(cache);
|
|
245
|
+
if (!seen) {
|
|
246
|
+
seen = /* @__PURE__ */ new Map();
|
|
247
|
+
this.uploaded.set(cache, seen);
|
|
248
|
+
}
|
|
249
|
+
if (!cache.has(id)) {
|
|
250
|
+
this.format.upload(cache, id, page.data);
|
|
251
|
+
seen.set(pageIndex, page.version);
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
const last = seen.get(pageIndex) ?? 0;
|
|
255
|
+
if (last >= page.version) return true;
|
|
256
|
+
for (const patch of page.patches) {
|
|
257
|
+
if (patch.seq <= last) continue;
|
|
258
|
+
const tight = new Uint8Array(patch.w * patch.h * n);
|
|
259
|
+
for (let row = 0; row < patch.h; row++) {
|
|
260
|
+
const src = ((patch.y + row) * PAGE_SIZE + patch.x) * n;
|
|
261
|
+
tight.set(page.data.subarray(src, src + patch.w * n), row * patch.w * n);
|
|
262
|
+
}
|
|
263
|
+
this.format.patch(cache, id, patch.x, patch.y, patch.w, patch.h, tight);
|
|
264
|
+
}
|
|
265
|
+
seen.set(pageIndex, page.version);
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
var fields = new PageSet({
|
|
270
|
+
channels: 1,
|
|
271
|
+
idPrefix: "weasel-dyn-sdf-page-",
|
|
272
|
+
upload: (cache, id, data) => cache.uploadR8(id, PAGE_SIZE, PAGE_SIZE, data),
|
|
273
|
+
patch: (cache, id, x, y, w, h, data) => cache.subImageR8(id, x, y, w, h, data)
|
|
274
|
+
});
|
|
275
|
+
var colors = new PageSet({
|
|
276
|
+
channels: 4,
|
|
277
|
+
idPrefix: "weasel-dyn-color-page-",
|
|
278
|
+
upload: (cache, id, data) => cache.uploadRGBA(id, PAGE_SIZE, PAGE_SIZE, data),
|
|
279
|
+
patch: (cache, id, x, y, w, h, data) => cache.subImageRGBA(id, x, y, w, h, data)
|
|
280
|
+
});
|
|
281
|
+
function blitToPage(pageIndex, x, y, w, h, bytes) {
|
|
282
|
+
fields.blit(pageIndex, x, y, w, h, bytes);
|
|
283
|
+
}
|
|
284
|
+
function blitToColorPage(pageIndex, x, y, w, h, rgba) {
|
|
285
|
+
colors.blit(pageIndex, x, y, w, h, rgba);
|
|
286
|
+
}
|
|
287
|
+
function clearPages() {
|
|
288
|
+
fields.clear();
|
|
289
|
+
colors.clear();
|
|
290
|
+
}
|
|
291
|
+
function dynamicPageTextureId(page) {
|
|
292
|
+
return fields.textureId(page);
|
|
293
|
+
}
|
|
294
|
+
function colorPageTextureId(page) {
|
|
295
|
+
return colors.textureId(page);
|
|
296
|
+
}
|
|
297
|
+
function syncDynamicPageTexture(cache, pageIndex) {
|
|
298
|
+
return fields.sync(cache, pageIndex);
|
|
299
|
+
}
|
|
300
|
+
function syncColorPageTexture(cache, pageIndex) {
|
|
301
|
+
return colors.sync(cache, pageIndex);
|
|
302
|
+
}
|
|
303
|
+
function _getPagesForTests() {
|
|
304
|
+
return fields.pages;
|
|
305
|
+
}
|
|
306
|
+
function _resetPagesForTests() {
|
|
307
|
+
fields.pages = [];
|
|
308
|
+
colors.pages = [];
|
|
185
309
|
}
|
|
186
|
-
var GENERIC_FAMILIES = /* @__PURE__ */ new Set([
|
|
187
|
-
"serif",
|
|
188
|
-
"sans-serif",
|
|
189
|
-
"monospace",
|
|
190
|
-
"cursive",
|
|
191
|
-
"fantasy",
|
|
192
|
-
"system-ui",
|
|
193
|
-
"ui-serif",
|
|
194
|
-
"ui-sans-serif",
|
|
195
|
-
"ui-monospace",
|
|
196
|
-
"ui-rounded",
|
|
197
|
-
"math",
|
|
198
|
-
"emoji",
|
|
199
|
-
"fangsong"
|
|
200
|
-
]);
|
|
201
310
|
|
|
202
311
|
// src/dynamic/shelfPack.ts
|
|
203
312
|
var ShelfPacker = class {
|
|
@@ -208,13 +317,16 @@ var ShelfPacker = class {
|
|
|
208
317
|
pageSize;
|
|
209
318
|
maxPages;
|
|
210
319
|
pages = [];
|
|
211
|
-
warned = false;
|
|
212
320
|
get pageCount() {
|
|
213
321
|
return this.pages.length;
|
|
214
322
|
}
|
|
215
|
-
/**
|
|
323
|
+
/** Whether an empty page could hold a w×h rect. */
|
|
324
|
+
fits(w, h) {
|
|
325
|
+
return w <= this.pageSize && h <= this.pageSize;
|
|
326
|
+
}
|
|
327
|
+
/** Allocate a w×h rect. Returns null when capacity is out. */
|
|
216
328
|
alloc(w, h) {
|
|
217
|
-
if (
|
|
329
|
+
if (!this.fits(w, h)) return null;
|
|
218
330
|
for (let p = 0; p < this.pages.length; p++) {
|
|
219
331
|
const spot = this.allocInPage(this.pages[p], w, h);
|
|
220
332
|
if (spot) return { page: p, ...spot };
|
|
@@ -225,7 +337,7 @@ var ShelfPacker = class {
|
|
|
225
337
|
const spot = this.allocInPage(page, w, h);
|
|
226
338
|
if (spot) return { page: this.pages.length - 1, ...spot };
|
|
227
339
|
}
|
|
228
|
-
return
|
|
340
|
+
return null;
|
|
229
341
|
}
|
|
230
342
|
allocInPage(page, w, h) {
|
|
231
343
|
for (const shelf of page.shelves) {
|
|
@@ -243,15 +355,6 @@ var ShelfPacker = class {
|
|
|
243
355
|
}
|
|
244
356
|
return null;
|
|
245
357
|
}
|
|
246
|
-
fail() {
|
|
247
|
-
if (!this.warned) {
|
|
248
|
-
this.warned = true;
|
|
249
|
-
console.warn(
|
|
250
|
-
`weasel DynamicGlyphAtlas: glyph pages full (${this.maxPages} \xD7 ${this.pageSize}\xB2); further dynamic glyphs will not render.`
|
|
251
|
-
);
|
|
252
|
-
}
|
|
253
|
-
return null;
|
|
254
|
-
}
|
|
255
358
|
};
|
|
256
359
|
|
|
257
360
|
// src/dynamic/distanceTransform.ts
|
|
@@ -315,9 +418,179 @@ function alphaToSdf(alpha, width, height, radius, cutoff) {
|
|
|
315
418
|
return out;
|
|
316
419
|
}
|
|
317
420
|
|
|
421
|
+
// src/dynamic/bakeQueue.ts
|
|
422
|
+
var SDF_RADIUS = 8;
|
|
423
|
+
var SDF_CUTOFF = 0.5;
|
|
424
|
+
var DEFAULT_BAKE_BUDGET = 16;
|
|
425
|
+
var newPacker = () => new ShelfPacker(PAGE_SIZE, MAX_PAGES);
|
|
426
|
+
var fieldPacker = newPacker();
|
|
427
|
+
var colorPacker = newPacker();
|
|
428
|
+
var pending = [];
|
|
429
|
+
var flushScheduled = false;
|
|
430
|
+
var budget = DEFAULT_BAKE_BUDGET;
|
|
431
|
+
var asked = 0;
|
|
432
|
+
var settled = true;
|
|
433
|
+
var reclaimScheduled = false;
|
|
434
|
+
var forgetGlyphs = () => {
|
|
435
|
+
};
|
|
436
|
+
var warned = /* @__PURE__ */ new Set();
|
|
437
|
+
function onPagesEmptied(forget) {
|
|
438
|
+
forgetGlyphs = forget;
|
|
439
|
+
}
|
|
440
|
+
function requestBake(char, raster) {
|
|
441
|
+
asked++;
|
|
442
|
+
if (raster.width === 0 || raster.height === 0) {
|
|
443
|
+
char.page = 0;
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
if (budget > 0) {
|
|
447
|
+
budget--;
|
|
448
|
+
bake(char, raster);
|
|
449
|
+
} else {
|
|
450
|
+
pending.push({ char, raster });
|
|
451
|
+
scheduleFlush();
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
function dropBakes(chars) {
|
|
455
|
+
pending = pending.filter((job) => !chars.has(job.char));
|
|
456
|
+
}
|
|
457
|
+
function bake(char, raster) {
|
|
458
|
+
const color = "rgba" in raster;
|
|
459
|
+
const packer = color ? colorPacker : fieldPacker;
|
|
460
|
+
const spot = packer.alloc(raster.width, raster.height);
|
|
461
|
+
if (!spot) {
|
|
462
|
+
noRoom(packer, raster);
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
if (color) {
|
|
466
|
+
blitToColorPage(spot.page, spot.x, spot.y, raster.width, raster.height, raster.rgba);
|
|
467
|
+
} else {
|
|
468
|
+
const sdf = alphaToSdf(raster.alpha, raster.width, raster.height, SDF_RADIUS, SDF_CUTOFF);
|
|
469
|
+
blitToPage(spot.page, spot.x, spot.y, raster.width, raster.height, sdf);
|
|
470
|
+
}
|
|
471
|
+
char.x = spot.x;
|
|
472
|
+
char.y = spot.y;
|
|
473
|
+
char.width = raster.width;
|
|
474
|
+
char.height = raster.height;
|
|
475
|
+
char.page = spot.page;
|
|
476
|
+
}
|
|
477
|
+
function noRoom(packer, raster) {
|
|
478
|
+
if (!packer.fits(raster.width, raster.height)) {
|
|
479
|
+
warnOnce("weasel DynamicGlyphAtlas: a glyph is larger than a page and will not render.");
|
|
480
|
+
} else if (!settled) {
|
|
481
|
+
warnOnce(
|
|
482
|
+
`weasel DynamicGlyphAtlas: more glyphs are in use at once than its pages hold (${MAX_PAGES} \xD7 ${PAGE_SIZE}\xB2); the rest will not render.`
|
|
483
|
+
);
|
|
484
|
+
} else if (!reclaimScheduled) {
|
|
485
|
+
reclaimScheduled = true;
|
|
486
|
+
setTimeout(reclaim, 0);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
function warnOnce(message) {
|
|
490
|
+
if (warned.has(message)) return;
|
|
491
|
+
warned.add(message);
|
|
492
|
+
console.warn(message);
|
|
493
|
+
}
|
|
494
|
+
function reclaim() {
|
|
495
|
+
reclaimScheduled = false;
|
|
496
|
+
settled = false;
|
|
497
|
+
fieldPacker = newPacker();
|
|
498
|
+
colorPacker = newPacker();
|
|
499
|
+
pending = [];
|
|
500
|
+
clearPages();
|
|
501
|
+
forgetGlyphs();
|
|
502
|
+
notifyGlyphReady();
|
|
503
|
+
}
|
|
504
|
+
function scheduleFlush() {
|
|
505
|
+
if (flushScheduled) return;
|
|
506
|
+
flushScheduled = true;
|
|
507
|
+
setTimeout(flushPending, 0);
|
|
508
|
+
}
|
|
509
|
+
function flushPending() {
|
|
510
|
+
flushScheduled = false;
|
|
511
|
+
let n = 0;
|
|
512
|
+
while (pending.length > 0 && n < DEFAULT_BAKE_BUDGET) {
|
|
513
|
+
const job = pending.shift();
|
|
514
|
+
if (job.char.page !== -1) continue;
|
|
515
|
+
bake(job.char, job.raster);
|
|
516
|
+
n++;
|
|
517
|
+
}
|
|
518
|
+
if (pending.length > 0) scheduleFlush();
|
|
519
|
+
notifyGlyphBaked();
|
|
520
|
+
}
|
|
521
|
+
function resetBakeBudget(n = DEFAULT_BAKE_BUDGET) {
|
|
522
|
+
budget = n;
|
|
523
|
+
if (asked === 0 && pending.length === 0) settled = true;
|
|
524
|
+
asked = 0;
|
|
525
|
+
}
|
|
526
|
+
function _resetBakeQueueForTests() {
|
|
527
|
+
fieldPacker = newPacker();
|
|
528
|
+
colorPacker = newPacker();
|
|
529
|
+
pending = [];
|
|
530
|
+
flushScheduled = false;
|
|
531
|
+
asked = 0;
|
|
532
|
+
settled = true;
|
|
533
|
+
reclaimScheduled = false;
|
|
534
|
+
warned.clear();
|
|
535
|
+
budget = DEFAULT_BAKE_BUDGET;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// src/cssFamily.ts
|
|
539
|
+
function cssFamilyName(family) {
|
|
540
|
+
if (/[,"']/.test(family)) return family;
|
|
541
|
+
return GENERIC_FAMILIES.has(family.trim().toLowerCase()) ? family.trim() : JSON.stringify(family);
|
|
542
|
+
}
|
|
543
|
+
var GENERIC_FAMILIES = /* @__PURE__ */ new Set([
|
|
544
|
+
"serif",
|
|
545
|
+
"sans-serif",
|
|
546
|
+
"monospace",
|
|
547
|
+
"cursive",
|
|
548
|
+
"fantasy",
|
|
549
|
+
"system-ui",
|
|
550
|
+
"ui-serif",
|
|
551
|
+
"ui-sans-serif",
|
|
552
|
+
"ui-monospace",
|
|
553
|
+
"ui-rounded",
|
|
554
|
+
"math",
|
|
555
|
+
"emoji",
|
|
556
|
+
"fangsong"
|
|
557
|
+
]);
|
|
558
|
+
|
|
559
|
+
// src/dynamic/fontLoads.ts
|
|
560
|
+
function familyKey(family) {
|
|
561
|
+
return family.trim().replace(/^(["'])(.*)\1$/, "$2").toLowerCase();
|
|
562
|
+
}
|
|
563
|
+
function watchFontLoads(onLoaded) {
|
|
564
|
+
const fonts = typeof document === "undefined" ? void 0 : document.fonts;
|
|
565
|
+
if (!fonts?.addEventListener) return () => {
|
|
566
|
+
};
|
|
567
|
+
const listener = (event) => {
|
|
568
|
+
const faces4 = event.fontfaces ?? [];
|
|
569
|
+
if (faces4.length > 0) onLoaded(faces4.map((face) => face.family));
|
|
570
|
+
};
|
|
571
|
+
fonts.addEventListener("loadingdone", listener);
|
|
572
|
+
return () => fonts.removeEventListener("loadingdone", listener);
|
|
573
|
+
}
|
|
574
|
+
|
|
318
575
|
// src/dynamic/glyphRasterizer.ts
|
|
319
576
|
var BAKE_SIZE = 48;
|
|
320
577
|
var PAD = 8;
|
|
578
|
+
var COLORED_BELOW = 240;
|
|
579
|
+
function premultipliedColor(data) {
|
|
580
|
+
const out = new Uint8ClampedArray(data.length);
|
|
581
|
+
let colored = false;
|
|
582
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
583
|
+
const a = data[i + 3];
|
|
584
|
+
if (a === 0) continue;
|
|
585
|
+
const r = data[i], g = data[i + 1], b = data[i + 2];
|
|
586
|
+
if (a >= 128 && Math.min(r, g, b) < COLORED_BELOW) colored = true;
|
|
587
|
+
out[i] = Math.round(r * a / 255);
|
|
588
|
+
out[i + 1] = Math.round(g * a / 255);
|
|
589
|
+
out[i + 2] = Math.round(b * a / 255);
|
|
590
|
+
out[i + 3] = a;
|
|
591
|
+
}
|
|
592
|
+
return colored ? out : null;
|
|
593
|
+
}
|
|
321
594
|
var METRICS_SIZE = 1e3;
|
|
322
595
|
function cssFontString(weight, style, family, size = BAKE_SIZE) {
|
|
323
596
|
return `${style === "italic" ? "italic " : ""}${weight} ${size}px ${cssFamilyName(family)}`;
|
|
@@ -340,6 +613,38 @@ function createCanvasRasterizer() {
|
|
|
340
613
|
ctx.textBaseline = "alphabetic";
|
|
341
614
|
ctx.textAlign = "left";
|
|
342
615
|
}
|
|
616
|
+
function draw(family, weight, style, text) {
|
|
617
|
+
setFont(family, weight, style);
|
|
618
|
+
const m = ctx.measureText(text);
|
|
619
|
+
const advance = m.width;
|
|
620
|
+
const inkLeft = Math.ceil(m.actualBoundingBoxLeft ?? 0);
|
|
621
|
+
const inkRight = Math.ceil(m.actualBoundingBoxRight ?? advance);
|
|
622
|
+
const inkAscent = Math.ceil(m.actualBoundingBoxAscent ?? BAKE_SIZE * 0.8);
|
|
623
|
+
const inkDescent = Math.ceil(m.actualBoundingBoxDescent ?? BAKE_SIZE * 0.2);
|
|
624
|
+
const inkW = inkLeft + inkRight;
|
|
625
|
+
const inkH = inkAscent + inkDescent;
|
|
626
|
+
if (inkW <= 0 || inkH <= 0) {
|
|
627
|
+
return { data: new Uint8ClampedArray(0), width: 0, height: 0, left: 0, top: 0, advance };
|
|
628
|
+
}
|
|
629
|
+
const w = inkW + 2 * PAD;
|
|
630
|
+
const h = inkH + 2 * PAD;
|
|
631
|
+
if (canvas.width < w || canvas.height < h) {
|
|
632
|
+
canvas.width = Math.max(canvas.width, w);
|
|
633
|
+
canvas.height = Math.max(canvas.height, h);
|
|
634
|
+
setFont(family, weight, style);
|
|
635
|
+
}
|
|
636
|
+
ctx.clearRect(0, 0, w, h);
|
|
637
|
+
ctx.fillStyle = "#fff";
|
|
638
|
+
ctx.fillText(text, PAD + inkLeft, PAD + inkAscent);
|
|
639
|
+
return {
|
|
640
|
+
data: ctx.getImageData(0, 0, w, h).data,
|
|
641
|
+
width: w,
|
|
642
|
+
height: h,
|
|
643
|
+
left: -(inkLeft + PAD),
|
|
644
|
+
top: inkAscent + PAD,
|
|
645
|
+
advance
|
|
646
|
+
};
|
|
647
|
+
}
|
|
343
648
|
return {
|
|
344
649
|
faceMetrics(family, weight, style) {
|
|
345
650
|
setFont(family, weight, style, METRICS_SIZE);
|
|
@@ -351,59 +656,33 @@ function createCanvasRasterizer() {
|
|
|
351
656
|
};
|
|
352
657
|
},
|
|
353
658
|
rasterize(family, weight, style, codepoint) {
|
|
354
|
-
|
|
355
|
-
const
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
const
|
|
361
|
-
const
|
|
362
|
-
|
|
363
|
-
const inkH = inkAscent + inkDescent;
|
|
364
|
-
if (inkW <= 0 || inkH <= 0) {
|
|
365
|
-
return { width: 0, height: 0, alpha: new Uint8ClampedArray(0), left: 0, top: 0, advance };
|
|
366
|
-
}
|
|
367
|
-
const w = inkW + 2 * PAD;
|
|
368
|
-
const h = inkH + 2 * PAD;
|
|
369
|
-
if (canvas.width < w || canvas.height < h) {
|
|
370
|
-
canvas.width = Math.max(canvas.width, w);
|
|
371
|
-
canvas.height = Math.max(canvas.height, h);
|
|
372
|
-
setFont(family, weight, style);
|
|
373
|
-
}
|
|
374
|
-
ctx.clearRect(0, 0, w, h);
|
|
375
|
-
ctx.fillStyle = "#fff";
|
|
376
|
-
ctx.fillText(chStr, PAD + inkLeft, PAD + inkAscent);
|
|
377
|
-
const img = ctx.getImageData(0, 0, w, h);
|
|
378
|
-
const alpha = new Uint8ClampedArray(w * h);
|
|
379
|
-
for (let i = 0; i < alpha.length; i++) alpha[i] = img.data[i * 4 + 3];
|
|
380
|
-
return {
|
|
381
|
-
width: w,
|
|
382
|
-
height: h,
|
|
383
|
-
alpha,
|
|
384
|
-
left: -(inkLeft + PAD),
|
|
385
|
-
top: inkAscent + PAD,
|
|
386
|
-
advance
|
|
387
|
-
};
|
|
659
|
+
const { data, ...box } = draw(family, weight, style, String.fromCodePoint(codepoint));
|
|
660
|
+
const alpha = new Uint8ClampedArray(box.width * box.height);
|
|
661
|
+
for (let i = 0; i < alpha.length; i++) alpha[i] = data[i * 4 + 3];
|
|
662
|
+
return { ...box, alpha };
|
|
663
|
+
},
|
|
664
|
+
rasterizeColor(family, weight, style, text) {
|
|
665
|
+
const { data, ...box } = draw(family, weight, style, text);
|
|
666
|
+
const rgba = premultipliedColor(data);
|
|
667
|
+
return rgba && { ...box, rgba };
|
|
388
668
|
}
|
|
389
669
|
};
|
|
390
670
|
}
|
|
391
671
|
|
|
392
672
|
// src/dynamic/dynamicAtlas.ts
|
|
393
|
-
var PAGE_SIZE = 1024;
|
|
394
|
-
var MAX_PAGES = 4;
|
|
395
|
-
var SDF_RADIUS = 8;
|
|
396
|
-
var SDF_CUTOFF = 0.5;
|
|
397
|
-
var DEFAULT_BAKE_BUDGET = 16;
|
|
398
673
|
var canvasFamilies = /* @__PURE__ */ new Set();
|
|
399
674
|
var autoEnrolledFamilies = /* @__PURE__ */ new Set();
|
|
400
|
-
var
|
|
401
|
-
var pages = [];
|
|
402
|
-
var packer = new ShelfPacker(PAGE_SIZE, MAX_PAGES);
|
|
403
|
-
var pending = [];
|
|
404
|
-
var flushScheduled = false;
|
|
405
|
-
var budget = DEFAULT_BAKE_BUDGET;
|
|
675
|
+
var faces2 = /* @__PURE__ */ new Map();
|
|
406
676
|
var rasterizer = null;
|
|
677
|
+
var stopWatchingLoads = null;
|
|
678
|
+
function forgetGlyphs2(face) {
|
|
679
|
+
face.font.charMap.clear();
|
|
680
|
+
face.font.chars.length = 0;
|
|
681
|
+
face.clusters.clear();
|
|
682
|
+
}
|
|
683
|
+
onPagesEmptied(() => {
|
|
684
|
+
for (const face of faces2.values()) forgetGlyphs2(face);
|
|
685
|
+
});
|
|
407
686
|
function getRasterizer() {
|
|
408
687
|
if (!rasterizer) rasterizer = createCanvasRasterizer();
|
|
409
688
|
return rasterizer;
|
|
@@ -438,162 +717,105 @@ function unregisterCanvasFont(family) {
|
|
|
438
717
|
if (!canvasFamilies.has(family)) return;
|
|
439
718
|
canvasFamilies.delete(family);
|
|
440
719
|
autoEnrolledFamilies.delete(family);
|
|
441
|
-
for (const key of [...
|
|
442
|
-
if (
|
|
720
|
+
for (const key of [...faces2.keys()]) {
|
|
721
|
+
if (faces2.get(key).family === family) faces2.delete(key);
|
|
443
722
|
}
|
|
444
723
|
notifyGlyphReady();
|
|
445
724
|
}
|
|
446
725
|
function getDynamicFace(family, weight, style) {
|
|
447
726
|
const key = `${family}|${weight}|${style}`;
|
|
448
|
-
const existing =
|
|
727
|
+
const existing = faces2.get(key);
|
|
449
728
|
if (existing) return existing;
|
|
450
729
|
const r = getRasterizer();
|
|
451
|
-
const metrics = r.faceMetrics(family, weight, style);
|
|
452
|
-
const base = Math.round(metrics.ascent);
|
|
453
730
|
const font = {
|
|
454
731
|
info: { face: family, size: BAKE_SIZE },
|
|
455
|
-
common: {
|
|
456
|
-
lineHeight: Math.round(metrics.ascent + metrics.descent),
|
|
457
|
-
base,
|
|
458
|
-
scaleW: PAGE_SIZE,
|
|
459
|
-
scaleH: PAGE_SIZE
|
|
460
|
-
},
|
|
732
|
+
common: { lineHeight: 0, base: 0, scaleW: PAGE_SIZE, scaleH: PAGE_SIZE },
|
|
461
733
|
chars: [],
|
|
462
734
|
kernings: [],
|
|
463
735
|
// no kerning in v1 (measured-pair kerning is future work)
|
|
464
736
|
charMap: /* @__PURE__ */ new Map(),
|
|
465
|
-
kerningMap: /* @__PURE__ */ new Map()
|
|
466
|
-
// Unrounded, unlike `common`: layout places the baseline from these.
|
|
467
|
-
faceMetrics: { ascent: metrics.ascent / BAKE_SIZE, descent: metrics.descent / BAKE_SIZE }
|
|
737
|
+
kerningMap: /* @__PURE__ */ new Map()
|
|
468
738
|
};
|
|
739
|
+
const record = (id, raster) => ({
|
|
740
|
+
id,
|
|
741
|
+
x: 0,
|
|
742
|
+
y: 0,
|
|
743
|
+
width: 0,
|
|
744
|
+
height: 0,
|
|
745
|
+
xoffset: raster.left,
|
|
746
|
+
yoffset: font.common.base - raster.top,
|
|
747
|
+
xadvance: raster.advance,
|
|
748
|
+
page: -1
|
|
749
|
+
});
|
|
750
|
+
const clusters = /* @__PURE__ */ new Map();
|
|
469
751
|
const face = {
|
|
470
752
|
family,
|
|
471
753
|
weight,
|
|
472
754
|
style,
|
|
473
755
|
font,
|
|
756
|
+
clusters,
|
|
474
757
|
requestGlyph(cp) {
|
|
475
758
|
const cached = font.charMap.get(cp);
|
|
476
759
|
if (cached) return cached;
|
|
477
760
|
const raster = r.rasterize(family, weight, style, cp);
|
|
478
|
-
const char =
|
|
479
|
-
id: cp,
|
|
480
|
-
x: 0,
|
|
481
|
-
y: 0,
|
|
482
|
-
width: 0,
|
|
483
|
-
height: 0,
|
|
484
|
-
xoffset: raster.left,
|
|
485
|
-
yoffset: base - raster.top,
|
|
486
|
-
xadvance: raster.advance,
|
|
487
|
-
page: -1
|
|
488
|
-
};
|
|
761
|
+
const char = record(cp, raster);
|
|
489
762
|
font.charMap.set(cp, char);
|
|
490
763
|
font.chars.push(char);
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
}
|
|
764
|
+
requestBake(char, raster);
|
|
765
|
+
return char;
|
|
766
|
+
},
|
|
767
|
+
requestCluster(text) {
|
|
768
|
+
const cached = clusters.get(text);
|
|
769
|
+
if (cached !== void 0) return cached;
|
|
770
|
+
const raster = r.rasterizeColor?.(family, weight, style, text) ?? null;
|
|
771
|
+
const char = raster && { ...record(text.codePointAt(0), raster), color: true };
|
|
772
|
+
clusters.set(text, char);
|
|
773
|
+
if (char && raster) requestBake(char, raster);
|
|
502
774
|
return char;
|
|
503
775
|
}
|
|
504
776
|
};
|
|
505
|
-
|
|
777
|
+
measureFace(face);
|
|
778
|
+
faces2.set(key, face);
|
|
779
|
+
stopWatchingLoads ??= watchFontLoads(refreshFamilies);
|
|
506
780
|
return face;
|
|
507
781
|
}
|
|
508
|
-
function
|
|
509
|
-
const
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
flushScheduled = true;
|
|
533
|
-
setTimeout(flushPending, 0);
|
|
534
|
-
}
|
|
535
|
-
function flushPending() {
|
|
536
|
-
flushScheduled = false;
|
|
537
|
-
let n = 0;
|
|
538
|
-
while (pending.length > 0 && n < DEFAULT_BAKE_BUDGET) {
|
|
539
|
-
const job = pending.shift();
|
|
540
|
-
if (job.char.page !== -1) continue;
|
|
541
|
-
bake(job.char, job.raster);
|
|
542
|
-
n++;
|
|
543
|
-
}
|
|
544
|
-
if (pending.length > 0) scheduleFlush();
|
|
782
|
+
function measureFace(face) {
|
|
783
|
+
const { font } = face;
|
|
784
|
+
const metrics = getRasterizer().faceMetrics(face.family, face.weight, face.style);
|
|
785
|
+
font.common.lineHeight = Math.round(metrics.ascent + metrics.descent);
|
|
786
|
+
font.common.base = Math.round(metrics.ascent);
|
|
787
|
+
font.faceMetrics = { ascent: metrics.ascent / BAKE_SIZE, descent: metrics.descent / BAKE_SIZE };
|
|
788
|
+
forgetGlyphs2(face);
|
|
789
|
+
}
|
|
790
|
+
function refreshCanvasFont(family) {
|
|
791
|
+
refreshFamilies(family === void 0 ? null : [family]);
|
|
792
|
+
}
|
|
793
|
+
function refreshFamilies(families) {
|
|
794
|
+
const keys = families && new Set(families.map(familyKey));
|
|
795
|
+
const dropped = /* @__PURE__ */ new Set();
|
|
796
|
+
let measured = 0;
|
|
797
|
+
for (const face of faces2.values()) {
|
|
798
|
+
if (keys && !face.family.split(",").some((name) => keys.has(familyKey(name)))) continue;
|
|
799
|
+
for (const char of face.font.chars) dropped.add(char);
|
|
800
|
+
for (const char of face.clusters.values()) if (char) dropped.add(char);
|
|
801
|
+
measureFace(face);
|
|
802
|
+
measured++;
|
|
803
|
+
}
|
|
804
|
+
if (measured === 0) return;
|
|
805
|
+
dropBakes(dropped);
|
|
545
806
|
notifyGlyphReady();
|
|
546
807
|
}
|
|
547
|
-
function resetBakeBudget(n = DEFAULT_BAKE_BUDGET) {
|
|
548
|
-
budget = n;
|
|
549
|
-
}
|
|
550
|
-
function dynamicPageTextureId(page) {
|
|
551
|
-
return `weasel-dyn-sdf-page-${page}`;
|
|
552
|
-
}
|
|
553
|
-
var uploadedVersions = /* @__PURE__ */ new WeakMap();
|
|
554
|
-
function syncDynamicPageTexture(cache, pageIndex) {
|
|
555
|
-
const page = pages[pageIndex];
|
|
556
|
-
if (!page) return false;
|
|
557
|
-
const id = dynamicPageTextureId(pageIndex);
|
|
558
|
-
let seen = uploadedVersions.get(cache);
|
|
559
|
-
if (!seen) {
|
|
560
|
-
seen = /* @__PURE__ */ new Map();
|
|
561
|
-
uploadedVersions.set(cache, seen);
|
|
562
|
-
}
|
|
563
|
-
if (!cache.has(id)) {
|
|
564
|
-
cache.uploadR8(id, PAGE_SIZE, PAGE_SIZE, page.data);
|
|
565
|
-
seen.set(pageIndex, page.version);
|
|
566
|
-
return true;
|
|
567
|
-
}
|
|
568
|
-
const last = seen.get(pageIndex) ?? 0;
|
|
569
|
-
if (last >= page.version) return true;
|
|
570
|
-
for (const patch of page.patches) {
|
|
571
|
-
if (patch.seq <= last) continue;
|
|
572
|
-
const tight = new Uint8Array(patch.w * patch.h);
|
|
573
|
-
for (let row = 0; row < patch.h; row++) {
|
|
574
|
-
const src = (patch.y + row) * PAGE_SIZE + patch.x;
|
|
575
|
-
tight.set(page.data.subarray(src, src + patch.w), row * patch.w);
|
|
576
|
-
}
|
|
577
|
-
cache.subImageR8(id, patch.x, patch.y, patch.w, patch.h, tight);
|
|
578
|
-
}
|
|
579
|
-
seen.set(pageIndex, page.version);
|
|
580
|
-
return true;
|
|
581
|
-
}
|
|
582
808
|
function __setGlyphRasterizerForTests(r) {
|
|
583
809
|
rasterizer = r;
|
|
584
810
|
}
|
|
585
|
-
function _getPagesForTests() {
|
|
586
|
-
return pages;
|
|
587
|
-
}
|
|
588
811
|
function _resetDynamicFontsForTests() {
|
|
589
812
|
canvasFamilies.clear();
|
|
590
813
|
autoEnrolledFamilies.clear();
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
budget = DEFAULT_BAKE_BUDGET;
|
|
814
|
+
faces2 = /* @__PURE__ */ new Map();
|
|
815
|
+
_resetPagesForTests();
|
|
816
|
+
_resetBakeQueueForTests();
|
|
817
|
+
stopWatchingLoads?.();
|
|
818
|
+
stopWatchingLoads = null;
|
|
597
819
|
_clearGlyphReadySubscribers();
|
|
598
820
|
rasterizer = null;
|
|
599
821
|
}
|
|
@@ -601,9 +823,7 @@ function _resetDynamicFontsForTests() {
|
|
|
601
823
|
// src/outline/OutlineFace.ts
|
|
602
824
|
var OUTLINE_PRECISION = 5;
|
|
603
825
|
|
|
604
|
-
// src/outline/
|
|
605
|
-
var SFNT_HEADER_BYTES = 12;
|
|
606
|
-
var TABLE_RECORD_BYTES = 16;
|
|
826
|
+
// src/outline/tag.ts
|
|
607
827
|
function tagAt(view, offset) {
|
|
608
828
|
return String.fromCharCode(
|
|
609
829
|
view.getUint8(offset),
|
|
@@ -612,10 +832,11 @@ function tagAt(view, offset) {
|
|
|
612
832
|
view.getUint8(offset + 3)
|
|
613
833
|
);
|
|
614
834
|
}
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
835
|
+
|
|
836
|
+
// src/outline/dataFork.ts
|
|
837
|
+
var RESOURCE_HEADER_BYTES = 16;
|
|
838
|
+
var TYPE_ENTRY_BYTES = 8;
|
|
839
|
+
var REFERENCE_BYTES = 12;
|
|
619
840
|
var FONT_SIGNATURES = /* @__PURE__ */ new Set([
|
|
620
841
|
"\0\0\0",
|
|
621
842
|
"true",
|
|
@@ -625,7 +846,6 @@ var FONT_SIGNATURES = /* @__PURE__ */ new Set([
|
|
|
625
846
|
"wOFF",
|
|
626
847
|
"wOF2"
|
|
627
848
|
]);
|
|
628
|
-
var RESOURCE_HEADER_BYTES = 16;
|
|
629
849
|
function isDataForkFont(bytes) {
|
|
630
850
|
if (bytes.byteLength < RESOURCE_HEADER_BYTES) return false;
|
|
631
851
|
const view = new DataView(bytes);
|
|
@@ -636,6 +856,46 @@ function isDataForkFont(bytes) {
|
|
|
636
856
|
const mapLength = view.getUint32(12);
|
|
637
857
|
return dataOffset >= RESOURCE_HEADER_BYTES && dataOffset + dataLength === mapOffset && mapOffset + mapLength <= bytes.byteLength;
|
|
638
858
|
}
|
|
859
|
+
var truncated = () => new Error("Datafork TrueType (.dfont) is truncated: a resource runs past the end of its segment.");
|
|
860
|
+
function sfntResources(bytes) {
|
|
861
|
+
const view = new DataView(bytes);
|
|
862
|
+
const dataOffset = view.getUint32(0);
|
|
863
|
+
const mapOffset = view.getUint32(4);
|
|
864
|
+
const dataLength = view.getUint32(8);
|
|
865
|
+
const mapEnd = mapOffset + view.getUint32(12);
|
|
866
|
+
const inMap = (at, count) => at >= mapOffset && at + count <= mapEnd;
|
|
867
|
+
if (!inMap(mapOffset + 24, 2)) return [];
|
|
868
|
+
const typeList = mapOffset + view.getUint16(mapOffset + 24);
|
|
869
|
+
if (!inMap(typeList, 2)) return [];
|
|
870
|
+
const typeCount = view.getUint16(typeList) + 1 & 65535;
|
|
871
|
+
const out = [];
|
|
872
|
+
for (let t = 0; t < typeCount; t++) {
|
|
873
|
+
const entry = typeList + 2 + t * TYPE_ENTRY_BYTES;
|
|
874
|
+
if (!inMap(entry, TYPE_ENTRY_BYTES)) throw truncated();
|
|
875
|
+
if (tagAt(view, entry) !== "sfnt") continue;
|
|
876
|
+
const count = view.getUint16(entry + 4) + 1;
|
|
877
|
+
const references = typeList + view.getUint16(entry + 6);
|
|
878
|
+
for (let i = 0; i < count; i++) {
|
|
879
|
+
const ref = references + i * REFERENCE_BYTES;
|
|
880
|
+
if (!inMap(ref, REFERENCE_BYTES)) throw truncated();
|
|
881
|
+
const offset = view.getUint32(ref + 4) & 16777215;
|
|
882
|
+
if (offset + 4 > dataLength) throw truncated();
|
|
883
|
+
const start = dataOffset + offset + 4;
|
|
884
|
+
const length = view.getUint32(start - 4);
|
|
885
|
+
if (offset + 4 + length > dataLength) throw truncated();
|
|
886
|
+
out.push(bytes.slice(start, start + length));
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
return out;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// src/outline/sfnt.ts
|
|
893
|
+
var SFNT_HEADER_BYTES = 12;
|
|
894
|
+
var TABLE_RECORD_BYTES = 16;
|
|
895
|
+
function isFontCollection(bytes) {
|
|
896
|
+
if (bytes.byteLength < 4) return false;
|
|
897
|
+
return tagAt(new DataView(bytes), 0) === "ttcf";
|
|
898
|
+
}
|
|
639
899
|
function readTableDirectory(view, dirOffset) {
|
|
640
900
|
const numTables = view.getUint16(dirOffset + 4);
|
|
641
901
|
const records = [];
|
|
@@ -674,7 +934,12 @@ function extractFont(source, view, dirOffset) {
|
|
|
674
934
|
const records = readTableDirectory(view, dirOffset);
|
|
675
935
|
const padded = (n) => n + 3 & -4;
|
|
676
936
|
let total = SFNT_HEADER_BYTES + records.length * TABLE_RECORD_BYTES;
|
|
677
|
-
for (const r of records)
|
|
937
|
+
for (const r of records) {
|
|
938
|
+
if (r.offset + r.length > source.byteLength) {
|
|
939
|
+
throw new Error(`font collection is truncated: table "${r.tag}" runs past the end of the file`);
|
|
940
|
+
}
|
|
941
|
+
total += padded(r.length);
|
|
942
|
+
}
|
|
678
943
|
const out = new ArrayBuffer(total);
|
|
679
944
|
const dst = new DataView(out);
|
|
680
945
|
const dstBytes = new Uint8Array(out);
|
|
@@ -699,26 +964,34 @@ function extractFont(source, view, dirOffset) {
|
|
|
699
964
|
return out;
|
|
700
965
|
}
|
|
701
966
|
function sfntFromCollection(bytes, postScriptName) {
|
|
967
|
+
const members = membersOf(bytes);
|
|
968
|
+
if (!members) return { bytes, matched: true, names: [] };
|
|
969
|
+
const names2 = members.map((m) => m.name);
|
|
970
|
+
const at = postScriptName ? names2.indexOf(postScriptName) : 0;
|
|
971
|
+
return { bytes: members[Math.max(at, 0)].bytes(), matched: at >= 0, names: names2 };
|
|
972
|
+
}
|
|
973
|
+
function membersOf(bytes) {
|
|
702
974
|
if (isDataForkFont(bytes)) {
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
)
|
|
975
|
+
const fonts = sfntResources(bytes);
|
|
976
|
+
if (fonts.length === 0) throw new Error("Datafork TrueType (.dfont) holds no sfnt resource.");
|
|
977
|
+
return fonts.map((font) => {
|
|
978
|
+
const view2 = new DataView(font);
|
|
979
|
+
return { name: postScriptNameAt(view2, readTableDirectory(view2, 0)), bytes: () => font };
|
|
980
|
+
});
|
|
706
981
|
}
|
|
707
|
-
if (!isFontCollection(bytes)) return
|
|
982
|
+
if (!isFontCollection(bytes)) return null;
|
|
708
983
|
const view = new DataView(bytes);
|
|
709
984
|
const numFonts = view.getUint32(8);
|
|
710
985
|
if (numFonts === 0) throw new Error("font collection contains no fonts");
|
|
711
|
-
const
|
|
712
|
-
for (let i = 0; i < numFonts; i++)
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
}
|
|
719
|
-
}
|
|
986
|
+
const members = [];
|
|
987
|
+
for (let i = 0; i < numFonts; i++) {
|
|
988
|
+
const dirOffset = view.getUint32(12 + i * 4);
|
|
989
|
+
members.push({
|
|
990
|
+
name: postScriptNameAt(view, readTableDirectory(view, dirOffset)),
|
|
991
|
+
bytes: () => extractFont(bytes, view, dirOffset)
|
|
992
|
+
});
|
|
720
993
|
}
|
|
721
|
-
return
|
|
994
|
+
return members;
|
|
722
995
|
}
|
|
723
996
|
|
|
724
997
|
// src/outline/gposKerning.ts
|
|
@@ -919,7 +1192,12 @@ function faceFor(font, gpos) {
|
|
|
919
1192
|
function createOpenTypeParser(postScriptName) {
|
|
920
1193
|
return async (bytes) => {
|
|
921
1194
|
const parse = parserOf(await loadOpenType());
|
|
922
|
-
const { bytes: single } = sfntFromCollection(bytes, postScriptName);
|
|
1195
|
+
const { bytes: single, matched, names: names2 } = sfntFromCollection(bytes, postScriptName);
|
|
1196
|
+
if (!matched) {
|
|
1197
|
+
throw new Error(
|
|
1198
|
+
`no member of the font file is named "${postScriptName}" (it holds ${names2.join(", ")})`
|
|
1199
|
+
);
|
|
1200
|
+
}
|
|
923
1201
|
return faceFor(parse(single), gposPairKerning(single));
|
|
924
1202
|
};
|
|
925
1203
|
}
|
|
@@ -996,7 +1274,7 @@ function glyphOutline(family, weight, style, cp) {
|
|
|
996
1274
|
const raw = slot.face.glyphD(cp);
|
|
997
1275
|
d = raw === null ? null : closeContours(raw);
|
|
998
1276
|
} catch (err) {
|
|
999
|
-
|
|
1277
|
+
warnOnce2(
|
|
1000
1278
|
`glyph|${slotKey(family, weight, style)}`,
|
|
1001
1279
|
`weasel: outline face "${family}" (${weight}/${style}) could not produce a glyph for U+${cp.toString(16).toUpperCase().padStart(4, "0")} \u2014 ${err instanceof Error ? err.message : String(err)}. Falling back to the SDF tier.`
|
|
1002
1280
|
);
|
|
@@ -1038,7 +1316,7 @@ async function runLoad(slot) {
|
|
|
1038
1316
|
slot.status = "failed";
|
|
1039
1317
|
slots.bump();
|
|
1040
1318
|
notifyGlyphReady();
|
|
1041
|
-
|
|
1319
|
+
warnOnce2(
|
|
1042
1320
|
`load|${slotKey(slot.family, slot.weight, slot.style)}`,
|
|
1043
1321
|
`weasel registerFontOutlines("${slot.family}" ${slot.weight}/${slot.style}): ${err instanceof Error ? err.message : String(err)}. Large text in this face keeps rendering from the SDF tier.`
|
|
1044
1322
|
);
|
|
@@ -1074,15 +1352,15 @@ async function readSource(source) {
|
|
|
1074
1352
|
if (resolved instanceof ArrayBuffer) return resolved;
|
|
1075
1353
|
return resolved.arrayBuffer();
|
|
1076
1354
|
}
|
|
1077
|
-
var
|
|
1078
|
-
function
|
|
1079
|
-
if (
|
|
1080
|
-
|
|
1355
|
+
var warned2 = /* @__PURE__ */ new Set();
|
|
1356
|
+
function warnOnce2(key, message) {
|
|
1357
|
+
if (warned2.has(key)) return;
|
|
1358
|
+
warned2.add(key);
|
|
1081
1359
|
console.warn(message);
|
|
1082
1360
|
}
|
|
1083
1361
|
function _resetFontOutlinesForTests() {
|
|
1084
1362
|
slots.clear();
|
|
1085
|
-
|
|
1363
|
+
warned2.clear();
|
|
1086
1364
|
}
|
|
1087
1365
|
var registry = createReflectable();
|
|
1088
1366
|
var fontRegistry = registry.reflection;
|
|
@@ -1301,6 +1579,7 @@ function missResolveResult(family, weight, style, suppressWarn = false) {
|
|
|
1301
1579
|
source: "outline"
|
|
1302
1580
|
};
|
|
1303
1581
|
}
|
|
1582
|
+
if (fontPending(family, weight, style)) return pendingMiss(family, weight, style);
|
|
1304
1583
|
const policy2 = getFontFallbackPolicy();
|
|
1305
1584
|
if (policy2 === "canvas") {
|
|
1306
1585
|
autoEnrollCanvasFont(family);
|
|
@@ -1506,8 +1785,8 @@ function resolveFontVariantInternal(family, weight, style, suppressWarn) {
|
|
|
1506
1785
|
|
|
1507
1786
|
// src/domFace.ts
|
|
1508
1787
|
var names = /* @__PURE__ */ new Map();
|
|
1509
|
-
var
|
|
1510
|
-
var
|
|
1788
|
+
var faces3 = /* @__PURE__ */ new Map();
|
|
1789
|
+
var warned3 = /* @__PURE__ */ new Set();
|
|
1511
1790
|
function domFonts() {
|
|
1512
1791
|
return typeof FontFace !== "undefined" && typeof document !== "undefined" && !!document.fonts;
|
|
1513
1792
|
}
|
|
@@ -1531,8 +1810,8 @@ function privateFamily(family, variant) {
|
|
|
1531
1810
|
const target = resolved.resolved.family;
|
|
1532
1811
|
const variants = listFontOutlines().filter((f) => f.family === target && f.status !== "failed");
|
|
1533
1812
|
if (variants.length === 0) {
|
|
1534
|
-
if (resolved.entry && !
|
|
1535
|
-
|
|
1813
|
+
if (resolved.entry && !warned3.has(target)) {
|
|
1814
|
+
warned3.add(target);
|
|
1536
1815
|
console.warn(
|
|
1537
1816
|
`weasel: "${target}" draws from a baked atlas with no font file registered, so DOM text in it (the text edit overlay) uses the browser's own "${family}" and will not match the canvas. Register the file the atlas was baked from: registerFontOutlines("${target}", \u2026).`
|
|
1538
1817
|
);
|
|
@@ -1556,7 +1835,7 @@ function cssFontFamilyLoading(family, variant = {}) {
|
|
|
1556
1835
|
const weight = variant.weight ?? 400;
|
|
1557
1836
|
const style = variant.style ?? "normal";
|
|
1558
1837
|
const exact = priv.variants.filter((v) => v.weight === weight && v.style === style);
|
|
1559
|
-
const pending2 = (exact.length > 0 ? exact : priv.variants).map((v) =>
|
|
1838
|
+
const pending2 = (exact.length > 0 ? exact : priv.variants).map((v) => faces3.get(`${v.family}|${v.weight}|${v.style}`)).filter((l) => l !== void 0 && !l.settled).map((l) => l.done);
|
|
1560
1839
|
return pending2.length === 0 ? null : Promise.all(pending2).then(() => {
|
|
1561
1840
|
});
|
|
1562
1841
|
}
|
|
@@ -1564,7 +1843,7 @@ function ensureFace(name, family, weight, style, bytes) {
|
|
|
1564
1843
|
const key = `${family}|${weight}|${style}`;
|
|
1565
1844
|
const css = outlineCssSource(family, weight, style);
|
|
1566
1845
|
if (!css) return;
|
|
1567
|
-
const have =
|
|
1846
|
+
const have = faces3.get(key);
|
|
1568
1847
|
if (have?.from === css.registration) return;
|
|
1569
1848
|
if (have?.face) document.fonts.delete(have.face);
|
|
1570
1849
|
let settle2;
|
|
@@ -1580,10 +1859,10 @@ function ensureFace(name, family, weight, style, bytes) {
|
|
|
1580
1859
|
entry.settled = true;
|
|
1581
1860
|
settle2();
|
|
1582
1861
|
};
|
|
1583
|
-
|
|
1862
|
+
faces3.set(key, entry);
|
|
1584
1863
|
const descriptors = { weight: String(weight), style, display: "swap" };
|
|
1585
1864
|
const add = (face) => {
|
|
1586
|
-
if (
|
|
1865
|
+
if (faces3.get(key) !== entry) return finish();
|
|
1587
1866
|
entry.face = face;
|
|
1588
1867
|
document.fonts.add(face);
|
|
1589
1868
|
face.load().then(finish, () => {
|
|
@@ -1598,13 +1877,13 @@ function ensureFace(name, family, weight, style, bytes) {
|
|
|
1598
1877
|
}
|
|
1599
1878
|
function _resetDomFacesForTests() {
|
|
1600
1879
|
if (typeof document !== "undefined" && document.fonts) {
|
|
1601
|
-
for (const { face } of
|
|
1880
|
+
for (const { face } of faces3.values()) if (face) document.fonts.delete(face);
|
|
1602
1881
|
}
|
|
1603
1882
|
names.clear();
|
|
1604
|
-
|
|
1605
|
-
|
|
1883
|
+
faces3.clear();
|
|
1884
|
+
warned3.clear();
|
|
1606
1885
|
}
|
|
1607
1886
|
|
|
1608
|
-
export { DEFAULT_BAKE_BUDGET, FIXTURE_FONT, PAGE_SIZE, SDF_RADIUS, __setGlyphRasterizerForTests, _getPagesForTests, _resetDomFacesForTests, _resetDynamicFontsForTests, _resetFallbackForTests, _resetFontOutlinesForTests, _resetFontRegistryForTests, createOpenTypeParser, cssFamilyName, cssFontFamily, cssFontFamilyLoading, dynamicPageTextureId, ensureFontTexture, faceMetricsFor, faceMetricsFromTables, faceMetricsOf, fontOutlineRegistry, fontPending, fontRegistry, getDefaultFontFamily, getFont, getFontFallbackPolicy, glyphGeneration, glyphOutline, hasFontOutlines, isCanvasFont, listCanvasFonts, listFontOutlines, listFontWeights, listFonts, loadFontOutlines, markAllFontsNotUploaded, outlineMetrics, outlineStatus, parseBmFont, registerCanvasFont, registerFont, registerFontOutlines, resetBakeBudget, resolveFontVariant, resolveGlyphFallback, setDefaultFontFamily, setFontFallbackPolicy, subscribeGlyphReady, syncDynamicPageTexture, textureCacheKey, unregisterCanvasFont, unregisterFontOutlines, verticalMetricsFromTables, warmFonts };
|
|
1609
|
-
//# sourceMappingURL=chunk-
|
|
1610
|
-
//# sourceMappingURL=chunk-
|
|
1887
|
+
export { DEFAULT_BAKE_BUDGET, FIXTURE_FONT, PAGE_SIZE, SDF_RADIUS, __setGlyphRasterizerForTests, _getPagesForTests, _resetDomFacesForTests, _resetDynamicFontsForTests, _resetFallbackForTests, _resetFontOutlinesForTests, _resetFontRegistryForTests, claimFallbackWarning, colorPageTextureId, createOpenTypeParser, cssFamilyName, cssFontFamily, cssFontFamilyLoading, dynamicPageTextureId, ensureFontTexture, faceGeneration, faceMetricsFor, faceMetricsFromTables, faceMetricsOf, fallbackWarningClaimed, fontOutlineRegistry, fontPending, fontRegistry, getDefaultFontFamily, getFont, getFontFallbackPolicy, glyphGeneration, glyphOutline, hasFontOutlines, isCanvasFont, listCanvasFonts, listFontOutlines, listFontWeights, listFonts, loadFontOutlines, markAllFontsNotUploaded, outlineMetrics, outlineStatus, parseBmFont, refreshCanvasFont, registerCanvasFont, registerFont, registerFontOutlines, resetBakeBudget, resolveFontVariant, resolveGlyphFallback, setDefaultFontFamily, setFontFallbackPolicy, subscribeGlyphReady, syncColorPageTexture, syncDynamicPageTexture, textureCacheKey, unregisterCanvasFont, unregisterFontOutlines, verticalMetricsFromTables, warmFonts };
|
|
1888
|
+
//# sourceMappingURL=chunk-NM6K6M4Y.js.map
|
|
1889
|
+
//# sourceMappingURL=chunk-NM6K6M4Y.js.map
|