feat(shared): add ensureCanvasFonts canvas-warm helper
This commit is contained in:
@@ -0,0 +1,71 @@
|
|||||||
|
/**
|
||||||
|
* Ensures a set of fonts is usable in a `<canvas>` measurement context.
|
||||||
|
*
|
||||||
|
* `document.fonts.load()` resolves once the FontFace bytes are fetched and
|
||||||
|
* parsed, but Chrome lazily registers fonts with the canvas measurement engine
|
||||||
|
* after that — `measureText` keeps returning a fallback width for some frames
|
||||||
|
* even though `document.fonts.check()` reports the font as loaded.
|
||||||
|
*
|
||||||
|
* Pretext caches measurements per font string forever, so a single fallback
|
||||||
|
* measurement during initial mount permanently poisons the cache and the
|
||||||
|
* rendered text drifts visibly from its measured box. This helper polls canvas
|
||||||
|
* measurement until each font reports a width that differs from the "unknown
|
||||||
|
* font family" fallback, guaranteeing the next `measureText` call sees the real
|
||||||
|
* glyph metrics.
|
||||||
|
*
|
||||||
|
* ponytail: deliberate copy of widgets/ComparisonView/lib's version — ADR-0002
|
||||||
|
* keeps the shelved morph tool untouched, so we don't move its util. The poll
|
||||||
|
* logic is the proven fix for Pretext's fallback-width cache poisoning; copying
|
||||||
|
* it is cheaper than refactoring frozen code.
|
||||||
|
*
|
||||||
|
* @param fontStrings - Pretext/canvas font strings (`weight sizepx "family"`) to warm.
|
||||||
|
*/
|
||||||
|
import { getPretextFontString } from '../getPretextFontString/getPretextFontString';
|
||||||
|
|
||||||
|
const PROBE_TEXT = 'mmmmmmmmmm';
|
||||||
|
const MAX_WAIT_MS = 1000;
|
||||||
|
const DEFAULT_PROBE_SIZE_PX = 16;
|
||||||
|
// Family unlikely to exist in any system — gives canvas's "unknown font" fallback width.
|
||||||
|
const FALLBACK_PROBE_FAMILY = '__glyphdiff_no_such_font_42__';
|
||||||
|
|
||||||
|
export async function ensureCanvasFonts(fontStrings: string[]): Promise<void> {
|
||||||
|
await Promise.all(fontStrings.map(f => document.fonts.load(f)));
|
||||||
|
|
||||||
|
// Pretext uses OffscreenCanvas when available; DOM canvas has separate font
|
||||||
|
// registration timing, so we MUST poll using the same canvas type pretext does.
|
||||||
|
const ctx = typeof OffscreenCanvas !== 'undefined'
|
||||||
|
? new OffscreenCanvas(1, 1).getContext('2d')
|
||||||
|
: document.createElement('canvas').getContext('2d');
|
||||||
|
if (!ctx) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Measure each font's "unknown font" fallback width (different per browser, per OS).
|
||||||
|
// Canvas uses this same fallback for any font family it can't resolve, so when the
|
||||||
|
// requested font finally registers, measureText will return a non-fallback width.
|
||||||
|
const fallbackWidths = new Map<string, number>();
|
||||||
|
for (const font of fontStrings) {
|
||||||
|
const sizeMatch = font.match(/(\d+(?:\.\d+)?)px/);
|
||||||
|
const sizePx = sizeMatch ? parseFloat(sizeMatch[1]) : DEFAULT_PROBE_SIZE_PX;
|
||||||
|
ctx.font = getPretextFontString(400, sizePx, FALLBACK_PROBE_FAMILY);
|
||||||
|
fallbackWidths.set(font, ctx.measureText(PROBE_TEXT).width);
|
||||||
|
}
|
||||||
|
|
||||||
|
const deadline = performance.now() + MAX_WAIT_MS;
|
||||||
|
const pending = new Set(fontStrings);
|
||||||
|
while (pending.size > 0 && performance.now() < deadline) {
|
||||||
|
for (const font of Array.from(pending)) {
|
||||||
|
ctx.font = font;
|
||||||
|
const w = ctx.measureText(PROBE_TEXT).width;
|
||||||
|
if (Math.abs(w - fallbackWidths.get(font)!) > 0.5) {
|
||||||
|
pending.delete(font);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (pending.size === 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Sequential by design: poll once per animation frame until fonts register.
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await new Promise<void>(resolve => requestAnimationFrame(() => resolve()));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ export {
|
|||||||
export { clampNumber } from './clampNumber/clampNumber';
|
export { clampNumber } from './clampNumber/clampNumber';
|
||||||
export { cn } from './cn';
|
export { cn } from './cn';
|
||||||
export { debounce } from './debounce/debounce';
|
export { debounce } from './debounce/debounce';
|
||||||
|
export { ensureCanvasFonts } from './ensureCanvasFonts/ensureCanvasFonts';
|
||||||
export { getDecimalPlaces } from './getDecimalPlaces/getDecimalPlaces';
|
export { getDecimalPlaces } from './getDecimalPlaces/getDecimalPlaces';
|
||||||
export { getPretextFontString } from './getPretextFontString/getPretextFontString';
|
export { getPretextFontString } from './getPretextFontString/getPretextFontString';
|
||||||
export { getSkeletonWidth } from './getSkeletonWidth/getSkeletonWidth';
|
export { getSkeletonWidth } from './getSkeletonWidth/getSkeletonWidth';
|
||||||
|
|||||||
Reference in New Issue
Block a user