@weasel-js/font 1.9.4 → 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.
@@ -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);
@@ -136,25 +136,29 @@ function parseBmFont(raw) {
136
136
  ...typeof range === "number" && range > 0 ? { distanceRange: range } : {}
137
137
  };
138
138
  }
139
-
140
- // src/glyphReady.ts
141
- var subscribers = /* @__PURE__ */ new Set();
139
+ var ready = createEmitter();
142
140
  var generation = 0;
143
141
  function glyphGeneration() {
144
142
  return generation;
145
143
  }
146
144
  function subscribeGlyphReady(cb) {
147
- subscribers.add(cb);
148
- return () => {
149
- subscribers.delete(cb);
150
- };
145
+ return ready.subscribe(cb);
146
+ }
147
+ var faces = 0;
148
+ function faceGeneration() {
149
+ return faces;
151
150
  }
152
151
  function notifyGlyphReady() {
153
152
  generation++;
154
- for (const cb of subscribers) cb();
153
+ faces++;
154
+ ready.emit();
155
+ }
156
+ function notifyGlyphBaked() {
157
+ generation++;
158
+ ready.emit();
155
159
  }
156
160
  function _clearGlyphReadySubscribers() {
157
- subscribers.clear();
161
+ ready = createEmitter();
158
162
  }
159
163
 
160
164
  // src/fallback.ts
@@ -182,6 +186,9 @@ function claimFallbackWarning(key) {
182
186
  warnedFallbacks.add(key);
183
187
  return true;
184
188
  }
189
+ function fallbackWarningClaimed(key) {
190
+ return warnedFallbacks.has(key);
191
+ }
185
192
  function _clearFallbackWarnings() {
186
193
  warnedFallbacks.clear();
187
194
  }
@@ -191,26 +198,115 @@ function _resetFallbackForTests() {
191
198
  warnedFallbacks.clear();
192
199
  }
193
200
 
194
- // src/cssFamily.ts
195
- function cssFamilyName(family) {
196
- if (/[,"']/.test(family)) return family;
197
- return GENERIC_FAMILIES.has(family.trim().toLowerCase()) ? family.trim() : JSON.stringify(family);
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 = [];
198
309
  }
199
- var GENERIC_FAMILIES = /* @__PURE__ */ new Set([
200
- "serif",
201
- "sans-serif",
202
- "monospace",
203
- "cursive",
204
- "fantasy",
205
- "system-ui",
206
- "ui-serif",
207
- "ui-sans-serif",
208
- "ui-monospace",
209
- "ui-rounded",
210
- "math",
211
- "emoji",
212
- "fangsong"
213
- ]);
214
310
 
215
311
  // src/dynamic/shelfPack.ts
216
312
  var ShelfPacker = class {
@@ -221,13 +317,16 @@ var ShelfPacker = class {
221
317
  pageSize;
222
318
  maxPages;
223
319
  pages = [];
224
- warned = false;
225
320
  get pageCount() {
226
321
  return this.pages.length;
227
322
  }
228
- /** Allocate a w×h rect. Returns null (warning once) when capacity is out. */
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. */
229
328
  alloc(w, h) {
230
- if (w > this.pageSize || h > this.pageSize) return this.fail();
329
+ if (!this.fits(w, h)) return null;
231
330
  for (let p = 0; p < this.pages.length; p++) {
232
331
  const spot = this.allocInPage(this.pages[p], w, h);
233
332
  if (spot) return { page: p, ...spot };
@@ -238,7 +337,7 @@ var ShelfPacker = class {
238
337
  const spot = this.allocInPage(page, w, h);
239
338
  if (spot) return { page: this.pages.length - 1, ...spot };
240
339
  }
241
- return this.fail();
340
+ return null;
242
341
  }
243
342
  allocInPage(page, w, h) {
244
343
  for (const shelf of page.shelves) {
@@ -256,15 +355,6 @@ var ShelfPacker = class {
256
355
  }
257
356
  return null;
258
357
  }
259
- fail() {
260
- if (!this.warned) {
261
- this.warned = true;
262
- console.warn(
263
- `weasel DynamicGlyphAtlas: glyph pages full (${this.maxPages} \xD7 ${this.pageSize}\xB2); further dynamic glyphs will not render.`
264
- );
265
- }
266
- return null;
267
- }
268
358
  };
269
359
 
270
360
  // src/dynamic/distanceTransform.ts
@@ -328,9 +418,179 @@ function alphaToSdf(alpha, width, height, radius, cutoff) {
328
418
  return out;
329
419
  }
330
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
+
331
575
  // src/dynamic/glyphRasterizer.ts
332
576
  var BAKE_SIZE = 48;
333
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
+ }
334
594
  var METRICS_SIZE = 1e3;
335
595
  function cssFontString(weight, style, family, size = BAKE_SIZE) {
336
596
  return `${style === "italic" ? "italic " : ""}${weight} ${size}px ${cssFamilyName(family)}`;
@@ -353,6 +613,38 @@ function createCanvasRasterizer() {
353
613
  ctx.textBaseline = "alphabetic";
354
614
  ctx.textAlign = "left";
355
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
+ }
356
648
  return {
357
649
  faceMetrics(family, weight, style) {
358
650
  setFont(family, weight, style, METRICS_SIZE);
@@ -364,59 +656,33 @@ function createCanvasRasterizer() {
364
656
  };
365
657
  },
366
658
  rasterize(family, weight, style, codepoint) {
367
- setFont(family, weight, style);
368
- const chStr = String.fromCodePoint(codepoint);
369
- const m = ctx.measureText(chStr);
370
- const advance = m.width;
371
- const inkLeft = Math.ceil(m.actualBoundingBoxLeft ?? 0);
372
- const inkRight = Math.ceil(m.actualBoundingBoxRight ?? advance);
373
- const inkAscent = Math.ceil(m.actualBoundingBoxAscent ?? BAKE_SIZE * 0.8);
374
- const inkDescent = Math.ceil(m.actualBoundingBoxDescent ?? BAKE_SIZE * 0.2);
375
- const inkW = inkLeft + inkRight;
376
- const inkH = inkAscent + inkDescent;
377
- if (inkW <= 0 || inkH <= 0) {
378
- return { width: 0, height: 0, alpha: new Uint8ClampedArray(0), left: 0, top: 0, advance };
379
- }
380
- const w = inkW + 2 * PAD;
381
- const h = inkH + 2 * PAD;
382
- if (canvas.width < w || canvas.height < h) {
383
- canvas.width = Math.max(canvas.width, w);
384
- canvas.height = Math.max(canvas.height, h);
385
- setFont(family, weight, style);
386
- }
387
- ctx.clearRect(0, 0, w, h);
388
- ctx.fillStyle = "#fff";
389
- ctx.fillText(chStr, PAD + inkLeft, PAD + inkAscent);
390
- const img = ctx.getImageData(0, 0, w, h);
391
- const alpha = new Uint8ClampedArray(w * h);
392
- for (let i = 0; i < alpha.length; i++) alpha[i] = img.data[i * 4 + 3];
393
- return {
394
- width: w,
395
- height: h,
396
- alpha,
397
- left: -(inkLeft + PAD),
398
- top: inkAscent + PAD,
399
- advance
400
- };
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 };
401
668
  }
402
669
  };
403
670
  }
404
671
 
405
672
  // src/dynamic/dynamicAtlas.ts
406
- var PAGE_SIZE = 1024;
407
- var MAX_PAGES = 4;
408
- var SDF_RADIUS = 8;
409
- var SDF_CUTOFF = 0.5;
410
- var DEFAULT_BAKE_BUDGET = 16;
411
673
  var canvasFamilies = /* @__PURE__ */ new Set();
412
674
  var autoEnrolledFamilies = /* @__PURE__ */ new Set();
413
- var faces = /* @__PURE__ */ new Map();
414
- var pages = [];
415
- var packer = new ShelfPacker(PAGE_SIZE, MAX_PAGES);
416
- var pending = [];
417
- var flushScheduled = false;
418
- var budget = DEFAULT_BAKE_BUDGET;
675
+ var faces2 = /* @__PURE__ */ new Map();
419
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
+ });
420
686
  function getRasterizer() {
421
687
  if (!rasterizer) rasterizer = createCanvasRasterizer();
422
688
  return rasterizer;
@@ -451,162 +717,105 @@ function unregisterCanvasFont(family) {
451
717
  if (!canvasFamilies.has(family)) return;
452
718
  canvasFamilies.delete(family);
453
719
  autoEnrolledFamilies.delete(family);
454
- for (const key of [...faces.keys()]) {
455
- if (faces.get(key).family === family) faces.delete(key);
720
+ for (const key of [...faces2.keys()]) {
721
+ if (faces2.get(key).family === family) faces2.delete(key);
456
722
  }
457
723
  notifyGlyphReady();
458
724
  }
459
725
  function getDynamicFace(family, weight, style) {
460
726
  const key = `${family}|${weight}|${style}`;
461
- const existing = faces.get(key);
727
+ const existing = faces2.get(key);
462
728
  if (existing) return existing;
463
729
  const r = getRasterizer();
464
- const metrics = r.faceMetrics(family, weight, style);
465
- const base = Math.round(metrics.ascent);
466
730
  const font = {
467
731
  info: { face: family, size: BAKE_SIZE },
468
- common: {
469
- lineHeight: Math.round(metrics.ascent + metrics.descent),
470
- base,
471
- scaleW: PAGE_SIZE,
472
- scaleH: PAGE_SIZE
473
- },
732
+ common: { lineHeight: 0, base: 0, scaleW: PAGE_SIZE, scaleH: PAGE_SIZE },
474
733
  chars: [],
475
734
  kernings: [],
476
735
  // no kerning in v1 (measured-pair kerning is future work)
477
736
  charMap: /* @__PURE__ */ new Map(),
478
- kerningMap: /* @__PURE__ */ new Map(),
479
- // Unrounded, unlike `common`: layout places the baseline from these.
480
- faceMetrics: { ascent: metrics.ascent / BAKE_SIZE, descent: metrics.descent / BAKE_SIZE }
737
+ kerningMap: /* @__PURE__ */ new Map()
481
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();
482
751
  const face = {
483
752
  family,
484
753
  weight,
485
754
  style,
486
755
  font,
756
+ clusters,
487
757
  requestGlyph(cp) {
488
758
  const cached = font.charMap.get(cp);
489
759
  if (cached) return cached;
490
760
  const raster = r.rasterize(family, weight, style, cp);
491
- const char = {
492
- id: cp,
493
- x: 0,
494
- y: 0,
495
- width: 0,
496
- height: 0,
497
- xoffset: raster.left,
498
- yoffset: base - raster.top,
499
- xadvance: raster.advance,
500
- page: -1
501
- };
761
+ const char = record(cp, raster);
502
762
  font.charMap.set(cp, char);
503
763
  font.chars.push(char);
504
- if (raster.width === 0 || raster.height === 0) {
505
- char.page = 0;
506
- return char;
507
- }
508
- if (budget > 0) {
509
- budget--;
510
- bake(char, raster);
511
- } else {
512
- pending.push({ char, raster });
513
- scheduleFlush();
514
- }
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);
515
774
  return char;
516
775
  }
517
776
  };
518
- faces.set(key, face);
777
+ measureFace(face);
778
+ faces2.set(key, face);
779
+ stopWatchingLoads ??= watchFontLoads(refreshFamilies);
519
780
  return face;
520
781
  }
521
- function bake(char, raster) {
522
- const spot = packer.alloc(raster.width, raster.height);
523
- if (!spot) return;
524
- const sdf = alphaToSdf(raster.alpha, raster.width, raster.height, SDF_RADIUS, SDF_CUTOFF);
525
- while (pages.length <= spot.page) {
526
- pages.push({ data: new Uint8Array(PAGE_SIZE * PAGE_SIZE), version: 0, patches: [] });
527
- }
528
- const page = pages[spot.page];
529
- for (let row = 0; row < raster.height; row++) {
530
- page.data.set(
531
- sdf.subarray(row * raster.width, (row + 1) * raster.width),
532
- (spot.y + row) * PAGE_SIZE + spot.x
533
- );
534
- }
535
- page.version++;
536
- page.patches.push({ seq: page.version, x: spot.x, y: spot.y, w: raster.width, h: raster.height });
537
- char.x = spot.x;
538
- char.y = spot.y;
539
- char.width = raster.width;
540
- char.height = raster.height;
541
- char.page = spot.page;
542
- }
543
- function scheduleFlush() {
544
- if (flushScheduled) return;
545
- flushScheduled = true;
546
- setTimeout(flushPending, 0);
547
- }
548
- function flushPending() {
549
- flushScheduled = false;
550
- let n = 0;
551
- while (pending.length > 0 && n < DEFAULT_BAKE_BUDGET) {
552
- const job = pending.shift();
553
- if (job.char.page !== -1) continue;
554
- bake(job.char, job.raster);
555
- n++;
556
- }
557
- 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);
558
806
  notifyGlyphReady();
559
807
  }
560
- function resetBakeBudget(n = DEFAULT_BAKE_BUDGET) {
561
- budget = n;
562
- }
563
- function dynamicPageTextureId(page) {
564
- return `weasel-dyn-sdf-page-${page}`;
565
- }
566
- var uploadedVersions = /* @__PURE__ */ new WeakMap();
567
- function syncDynamicPageTexture(cache, pageIndex) {
568
- const page = pages[pageIndex];
569
- if (!page) return false;
570
- const id = dynamicPageTextureId(pageIndex);
571
- let seen = uploadedVersions.get(cache);
572
- if (!seen) {
573
- seen = /* @__PURE__ */ new Map();
574
- uploadedVersions.set(cache, seen);
575
- }
576
- if (!cache.has(id)) {
577
- cache.uploadR8(id, PAGE_SIZE, PAGE_SIZE, page.data);
578
- seen.set(pageIndex, page.version);
579
- return true;
580
- }
581
- const last = seen.get(pageIndex) ?? 0;
582
- if (last >= page.version) return true;
583
- for (const patch of page.patches) {
584
- if (patch.seq <= last) continue;
585
- const tight = new Uint8Array(patch.w * patch.h);
586
- for (let row = 0; row < patch.h; row++) {
587
- const src = (patch.y + row) * PAGE_SIZE + patch.x;
588
- tight.set(page.data.subarray(src, src + patch.w), row * patch.w);
589
- }
590
- cache.subImageR8(id, patch.x, patch.y, patch.w, patch.h, tight);
591
- }
592
- seen.set(pageIndex, page.version);
593
- return true;
594
- }
595
808
  function __setGlyphRasterizerForTests(r) {
596
809
  rasterizer = r;
597
810
  }
598
- function _getPagesForTests() {
599
- return pages;
600
- }
601
811
  function _resetDynamicFontsForTests() {
602
812
  canvasFamilies.clear();
603
813
  autoEnrolledFamilies.clear();
604
- faces = /* @__PURE__ */ new Map();
605
- pages = [];
606
- packer = new ShelfPacker(PAGE_SIZE, MAX_PAGES);
607
- pending = [];
608
- flushScheduled = false;
609
- budget = DEFAULT_BAKE_BUDGET;
814
+ faces2 = /* @__PURE__ */ new Map();
815
+ _resetPagesForTests();
816
+ _resetBakeQueueForTests();
817
+ stopWatchingLoads?.();
818
+ stopWatchingLoads = null;
610
819
  _clearGlyphReadySubscribers();
611
820
  rasterizer = null;
612
821
  }
@@ -755,30 +964,34 @@ function extractFont(source, view, dirOffset) {
755
964
  return out;
756
965
  }
757
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) {
758
974
  if (isDataForkFont(bytes)) {
759
975
  const fonts = sfntResources(bytes);
760
976
  if (fonts.length === 0) throw new Error("Datafork TrueType (.dfont) holds no sfnt resource.");
761
- const named = postScriptName ? fonts.find((font) => {
977
+ return fonts.map((font) => {
762
978
  const view2 = new DataView(font);
763
- return postScriptNameAt(view2, readTableDirectory(view2, 0)) === postScriptName;
764
- }) : void 0;
765
- return { bytes: named ?? fonts[0], matched: named !== void 0 || !postScriptName };
979
+ return { name: postScriptNameAt(view2, readTableDirectory(view2, 0)), bytes: () => font };
980
+ });
766
981
  }
767
- if (!isFontCollection(bytes)) return { bytes, matched: true };
982
+ if (!isFontCollection(bytes)) return null;
768
983
  const view = new DataView(bytes);
769
984
  const numFonts = view.getUint32(8);
770
985
  if (numFonts === 0) throw new Error("font collection contains no fonts");
771
- const offsets = [];
772
- for (let i = 0; i < numFonts; i++) offsets.push(view.getUint32(12 + i * 4));
773
- if (postScriptName) {
774
- for (const dirOffset of offsets) {
775
- const records = readTableDirectory(view, dirOffset);
776
- if (postScriptNameAt(view, records) === postScriptName) {
777
- return { bytes: extractFont(bytes, view, dirOffset), matched: true };
778
- }
779
- }
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
+ });
780
993
  }
781
- return { bytes: extractFont(bytes, view, offsets[0]), matched: !postScriptName };
994
+ return members;
782
995
  }
783
996
 
784
997
  // src/outline/gposKerning.ts
@@ -979,7 +1192,12 @@ function faceFor(font, gpos) {
979
1192
  function createOpenTypeParser(postScriptName) {
980
1193
  return async (bytes) => {
981
1194
  const parse = parserOf(await loadOpenType());
982
- 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
+ }
983
1201
  return faceFor(parse(single), gposPairKerning(single));
984
1202
  };
985
1203
  }
@@ -1056,7 +1274,7 @@ function glyphOutline(family, weight, style, cp) {
1056
1274
  const raw = slot.face.glyphD(cp);
1057
1275
  d = raw === null ? null : closeContours(raw);
1058
1276
  } catch (err) {
1059
- warnOnce(
1277
+ warnOnce2(
1060
1278
  `glyph|${slotKey(family, weight, style)}`,
1061
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.`
1062
1280
  );
@@ -1098,7 +1316,7 @@ async function runLoad(slot) {
1098
1316
  slot.status = "failed";
1099
1317
  slots.bump();
1100
1318
  notifyGlyphReady();
1101
- warnOnce(
1319
+ warnOnce2(
1102
1320
  `load|${slotKey(slot.family, slot.weight, slot.style)}`,
1103
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.`
1104
1322
  );
@@ -1134,15 +1352,15 @@ async function readSource(source) {
1134
1352
  if (resolved instanceof ArrayBuffer) return resolved;
1135
1353
  return resolved.arrayBuffer();
1136
1354
  }
1137
- var warned = /* @__PURE__ */ new Set();
1138
- function warnOnce(key, message) {
1139
- if (warned.has(key)) return;
1140
- warned.add(key);
1355
+ var warned2 = /* @__PURE__ */ new Set();
1356
+ function warnOnce2(key, message) {
1357
+ if (warned2.has(key)) return;
1358
+ warned2.add(key);
1141
1359
  console.warn(message);
1142
1360
  }
1143
1361
  function _resetFontOutlinesForTests() {
1144
1362
  slots.clear();
1145
- warned.clear();
1363
+ warned2.clear();
1146
1364
  }
1147
1365
  var registry = createReflectable();
1148
1366
  var fontRegistry = registry.reflection;
@@ -1567,8 +1785,8 @@ function resolveFontVariantInternal(family, weight, style, suppressWarn) {
1567
1785
 
1568
1786
  // src/domFace.ts
1569
1787
  var names = /* @__PURE__ */ new Map();
1570
- var faces2 = /* @__PURE__ */ new Map();
1571
- var warned2 = /* @__PURE__ */ new Set();
1788
+ var faces3 = /* @__PURE__ */ new Map();
1789
+ var warned3 = /* @__PURE__ */ new Set();
1572
1790
  function domFonts() {
1573
1791
  return typeof FontFace !== "undefined" && typeof document !== "undefined" && !!document.fonts;
1574
1792
  }
@@ -1592,8 +1810,8 @@ function privateFamily(family, variant) {
1592
1810
  const target = resolved.resolved.family;
1593
1811
  const variants = listFontOutlines().filter((f) => f.family === target && f.status !== "failed");
1594
1812
  if (variants.length === 0) {
1595
- if (resolved.entry && !warned2.has(target)) {
1596
- warned2.add(target);
1813
+ if (resolved.entry && !warned3.has(target)) {
1814
+ warned3.add(target);
1597
1815
  console.warn(
1598
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).`
1599
1817
  );
@@ -1617,7 +1835,7 @@ function cssFontFamilyLoading(family, variant = {}) {
1617
1835
  const weight = variant.weight ?? 400;
1618
1836
  const style = variant.style ?? "normal";
1619
1837
  const exact = priv.variants.filter((v) => v.weight === weight && v.style === style);
1620
- const pending2 = (exact.length > 0 ? exact : priv.variants).map((v) => faces2.get(`${v.family}|${v.weight}|${v.style}`)).filter((l) => l !== void 0 && !l.settled).map((l) => l.done);
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);
1621
1839
  return pending2.length === 0 ? null : Promise.all(pending2).then(() => {
1622
1840
  });
1623
1841
  }
@@ -1625,7 +1843,7 @@ function ensureFace(name, family, weight, style, bytes) {
1625
1843
  const key = `${family}|${weight}|${style}`;
1626
1844
  const css = outlineCssSource(family, weight, style);
1627
1845
  if (!css) return;
1628
- const have = faces2.get(key);
1846
+ const have = faces3.get(key);
1629
1847
  if (have?.from === css.registration) return;
1630
1848
  if (have?.face) document.fonts.delete(have.face);
1631
1849
  let settle2;
@@ -1641,10 +1859,10 @@ function ensureFace(name, family, weight, style, bytes) {
1641
1859
  entry.settled = true;
1642
1860
  settle2();
1643
1861
  };
1644
- faces2.set(key, entry);
1862
+ faces3.set(key, entry);
1645
1863
  const descriptors = { weight: String(weight), style, display: "swap" };
1646
1864
  const add = (face) => {
1647
- if (faces2.get(key) !== entry) return finish();
1865
+ if (faces3.get(key) !== entry) return finish();
1648
1866
  entry.face = face;
1649
1867
  document.fonts.add(face);
1650
1868
  face.load().then(finish, () => {
@@ -1659,13 +1877,13 @@ function ensureFace(name, family, weight, style, bytes) {
1659
1877
  }
1660
1878
  function _resetDomFacesForTests() {
1661
1879
  if (typeof document !== "undefined" && document.fonts) {
1662
- for (const { face } of faces2.values()) if (face) document.fonts.delete(face);
1880
+ for (const { face } of faces3.values()) if (face) document.fonts.delete(face);
1663
1881
  }
1664
1882
  names.clear();
1665
- faces2.clear();
1666
- warned2.clear();
1883
+ faces3.clear();
1884
+ warned3.clear();
1667
1885
  }
1668
1886
 
1669
- 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 };
1670
- //# sourceMappingURL=chunk-ERSHQYJL.js.map
1671
- //# sourceMappingURL=chunk-ERSHQYJL.js.map
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