modern-text 1.12.0 → 2.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/dist/index.mjs CHANGED
@@ -1,10 +1,10 @@
1
- import { T as Text } from './shared/modern-text.CrFHIJtB.mjs';
2
- export { C as Canvas2DRenderer, a as Character, F as Fragment, M as Measurer, P as Paragraph, b as backgroundPlugin, c as createSvgLoader, d as createSvgParser, g as getEffectTransform2D, e as getHighlightStyle, h as highlightPlugin, i as isEqualObject, f as isEqualValue, l as listStylePlugin, o as outlinePlugin, p as parseColormap, j as parseTransformOrigin, k as parseValueNumber, r as renderPlugin, t as textDecorationPlugin, m as textDefaultStyle } from './shared/modern-text.CrFHIJtB.mjs';
1
+ import { T as Text } from './shared/modern-text.EUexrM_5.mjs';
2
+ export { C as Canvas2DRenderer, a as Character, D as DomMeasurer, F as FontMeasurer, b as Fragment, P as Paragraph, c as backgroundPlugin, d as createSvgLoader, e as createSvgParser, g as getEffectTransform2D, f as getHighlightStyle, h as highlightPlugin, i as isEqualObject, j as isEqualValue, l as listStylePlugin, o as outlinePlugin, p as parseColormap, k as parseTransformOrigin, m as parseValueNumber, r as renderPlugin, t as textDecorationPlugin, n as textDefaultStyle } from './shared/modern-text.EUexrM_5.mjs';
3
3
  export { d as defineDeformation, a as definePlugin, b as deformationPlugin, g as getDeformationNames, r as removeDeformation } from './shared/modern-text.JF1ny7A-.mjs';
4
4
  export { C as CircleCurve, E as EllipseCurve, H as HeartCurve, P as PolygonCurve, R as RectangularCurve } from './shared/modern-text.fT17R5HY.mjs';
5
+ import 'modern-font';
5
6
  import 'modern-idoc';
6
7
  import 'modern-path2d';
7
- import 'modern-font';
8
8
 
9
9
  function measureText(options, load) {
10
10
  const text = new Text(options);
@@ -1,9 +1,9 @@
1
1
  'use strict';
2
2
 
3
+ const modernFont = require('modern-font');
3
4
  const modernIdoc = require('modern-idoc');
4
5
  const modernPath2d = require('modern-path2d');
5
6
  const deformation = require('./modern-text.B2xfrqDc.cjs');
6
- const modernFont = require('modern-font');
7
7
 
8
8
  function createSvgLoader() {
9
9
  const loaded = /* @__PURE__ */ new Map();
@@ -531,6 +531,18 @@ class Character {
531
531
  this.fontStyle = fsSelectionMap[os2.fsSelection] ?? macStyleMap[head.macStyle];
532
532
  return this;
533
533
  }
534
+ /**
535
+ * Populate glyph metrics only (advance width/height, ascender/descender,
536
+ * baseline, …) without building the glyph `path` or touching boxes.
537
+ *
538
+ * The DOM {@link DomMeasurer} never needs this — it reads positions back from the
539
+ * browser. A pure-JS measurer (e.g. `FontMeasurer`) must know advances *before*
540
+ * it can place characters, so it calls this ahead of layout. `update()` later
541
+ * recomputes the same metrics while building the path, so this is idempotent.
542
+ */
543
+ measureGlyph(fonts) {
544
+ return this.updateGlyph(this._getFontSFNT(fonts));
545
+ }
534
546
  update(fonts) {
535
547
  const sfnt = this._getFontSFNT(fonts);
536
548
  if (!sfnt) {
@@ -723,7 +735,7 @@ function getSharedContainer() {
723
735
  sharedContainer = container;
724
736
  return container;
725
737
  }
726
- class Measurer {
738
+ class DomMeasurer {
727
739
  static notZeroStyles = /* @__PURE__ */ new Set([
728
740
  "width",
729
741
  "height"
@@ -758,7 +770,7 @@ class Measurer {
758
770
  return cached;
759
771
  }
760
772
  const domStyle = {};
761
- const { notZeroStyles, pxStyles } = Measurer;
773
+ const { notZeroStyles, pxStyles } = DomMeasurer;
762
774
  for (const key in style) {
763
775
  const value = style[key];
764
776
  if (notZeroStyles.has(key) && value === 0) {
@@ -1067,6 +1079,253 @@ class Measurer {
1067
1079
  }
1068
1080
  }
1069
1081
 
1082
+ function side(style, name, edge) {
1083
+ return style[`${name}${edge}`] ?? style[name] ?? 0;
1084
+ }
1085
+ class FontMeasurer {
1086
+ measure(paragraphs, rootStyle, _dom, fonts) {
1087
+ const _fonts = fonts ?? modernFont.fonts;
1088
+ for (const paragraph of paragraphs) {
1089
+ for (const fragment of paragraph.fragments) {
1090
+ for (const character of fragment.characters) {
1091
+ character.measureGlyph(_fonts);
1092
+ }
1093
+ }
1094
+ }
1095
+ return rootStyle.writingMode.includes("vertical") ? this._measureVertical(paragraphs, rootStyle) : this._measureHorizontal(paragraphs, rootStyle);
1096
+ }
1097
+ _rootPadding(rootStyle) {
1098
+ return {
1099
+ top: side(rootStyle, "padding", "Top"),
1100
+ right: side(rootStyle, "padding", "Right"),
1101
+ bottom: side(rootStyle, "padding", "Bottom"),
1102
+ left: side(rootStyle, "padding", "Left")
1103
+ };
1104
+ }
1105
+ _measureHorizontal(paragraphs, rootStyle) {
1106
+ const rootPad = this._rootPadding(rootStyle);
1107
+ const hasWidth = typeof rootStyle.width === "number";
1108
+ const availWidth = hasWidth ? rootStyle.width - rootPad.left - rootPad.right : Infinity;
1109
+ let y = rootPad.top;
1110
+ let maxRight = rootPad.left;
1111
+ for (const paragraph of paragraphs) {
1112
+ const pStyle = paragraph.computedStyle;
1113
+ const pBox = paragraph.style;
1114
+ const mTop = side(pBox, "margin", "Top");
1115
+ const mBottom = side(pBox, "margin", "Bottom");
1116
+ const mLeft = side(pBox, "margin", "Left");
1117
+ const mRight = side(pBox, "margin", "Right");
1118
+ const pTop = side(pBox, "padding", "Top");
1119
+ const pBottom = side(pBox, "padding", "Bottom");
1120
+ const pLeft = side(pBox, "padding", "Left");
1121
+ const pRight = side(pBox, "padding", "Right");
1122
+ const liLeft = rootPad.left + mLeft + pLeft;
1123
+ const liAvail = availWidth === Infinity ? Infinity : availWidth - mLeft - mRight - pLeft - pRight;
1124
+ y += mTop + pTop;
1125
+ const paraTop = y;
1126
+ let paraRight = liLeft;
1127
+ const lines = this._breakLines(paragraph, liAvail);
1128
+ const align = pStyle.textAlign;
1129
+ const indent = pStyle.textIndent ?? 0;
1130
+ for (let i = 0; i < lines.length; i++) {
1131
+ const line = lines[i];
1132
+ const lineIndent = i === 0 ? indent : 0;
1133
+ let lineHeight = pStyle.fontSize * pStyle.lineHeight;
1134
+ let contentWidth = 0;
1135
+ for (const c of line) {
1136
+ if (c.fontHeight > lineHeight) {
1137
+ lineHeight = c.fontHeight;
1138
+ }
1139
+ contentWidth += this._advance(c);
1140
+ }
1141
+ let x = liLeft + lineIndent;
1142
+ if (liAvail !== Infinity) {
1143
+ const slack = liAvail - lineIndent - contentWidth;
1144
+ if (align === "center") {
1145
+ x += slack / 2;
1146
+ } else if (align === "end" || align === "right") {
1147
+ x += slack;
1148
+ }
1149
+ }
1150
+ for (const c of line) {
1151
+ const adv = c.advanceWidth;
1152
+ const contentHeight = c.advanceHeight;
1153
+ const fontHeight = c.fontHeight;
1154
+ c.inlineBox.left = x;
1155
+ c.inlineBox.top = y + (lineHeight - contentHeight) / 2;
1156
+ c.inlineBox.width = adv;
1157
+ c.inlineBox.height = contentHeight;
1158
+ c.lineBox.left = x;
1159
+ c.lineBox.top = c.inlineBox.top + (contentHeight - fontHeight) / 2;
1160
+ c.lineBox.width = adv;
1161
+ c.lineBox.height = fontHeight;
1162
+ x += this._advance(c);
1163
+ }
1164
+ if (x > paraRight) {
1165
+ paraRight = x;
1166
+ }
1167
+ y += lineHeight;
1168
+ }
1169
+ if (paraRight > maxRight) {
1170
+ maxRight = paraRight;
1171
+ }
1172
+ for (const fragment of paragraph.fragments) {
1173
+ this._unionInto(fragment.inlineBox, fragment.characters.map((c) => c.inlineBox));
1174
+ }
1175
+ paragraph.lineBox.left = liLeft;
1176
+ paragraph.lineBox.top = paraTop;
1177
+ paragraph.lineBox.width = liAvail === Infinity ? paraRight - liLeft : liAvail;
1178
+ paragraph.lineBox.height = y - paraTop;
1179
+ y += pBottom + mBottom;
1180
+ }
1181
+ const contentBottom = y + rootPad.bottom;
1182
+ const totalWidth = hasWidth ? rootStyle.width : maxRight + rootPad.right;
1183
+ const totalHeight = typeof rootStyle.height === "number" ? rootStyle.height : contentBottom;
1184
+ if (typeof rootStyle.height === "number") {
1185
+ const slack = totalHeight - contentBottom;
1186
+ const va = rootStyle.verticalAlign;
1187
+ const dy = va === "middle" ? slack / 2 : va === "bottom" ? slack : 0;
1188
+ if (dy) {
1189
+ this._shiftAll(paragraphs, dy);
1190
+ }
1191
+ }
1192
+ return {
1193
+ paragraphs,
1194
+ boundingBox: new modernPath2d.BoundingBox(0, 0, totalWidth, totalHeight)
1195
+ };
1196
+ }
1197
+ /**
1198
+ * Vertical writing-mode (`vertical-rl`): columns stack right-to-left, glyphs
1199
+ * flow top→bottom. It is the horizontal layout with the inline and block axes
1200
+ * swapped — the inline (down-column) advance is `advanceWidth` (CJK upright = em;
1201
+ * Latin is rotated, so its advance ≈ its width), the cross-axis content box is
1202
+ * `advanceHeight`, and the column thickness is `fontHeight`. The lineBox is
1203
+ * derived exactly as DomMeasurer.measureParagraphDom's vertical branch, so it
1204
+ * stays accurate even though inlineBox carries the content-box rounding residual.
1205
+ *
1206
+ * v1: `vertical-rl` only; no per-paragraph margin/padding or block alignment.
1207
+ */
1208
+ _measureVertical(paragraphs, rootStyle) {
1209
+ const rootPad = this._rootPadding(rootStyle);
1210
+ const hasHeight = typeof rootStyle.height === "number";
1211
+ const availHeight = hasHeight ? rootStyle.height - rootPad.top - rootPad.bottom : Infinity;
1212
+ const columns = [];
1213
+ for (const paragraph of paragraphs) {
1214
+ const strut = paragraph.computedStyle.fontSize * paragraph.computedStyle.lineHeight;
1215
+ for (const line of this._breakLines(paragraph, availHeight)) {
1216
+ let thickness = strut;
1217
+ for (const c of line) {
1218
+ if (c.fontHeight > thickness) {
1219
+ thickness = c.fontHeight;
1220
+ }
1221
+ }
1222
+ columns.push({ chars: line, thickness });
1223
+ }
1224
+ }
1225
+ const totalThickness = columns.reduce((sum, c) => sum + c.thickness, 0);
1226
+ const hasWidth = typeof rootStyle.width === "number";
1227
+ const blockWidth = hasWidth ? Math.max(rootStyle.width - rootPad.left - rootPad.right, totalThickness) : totalThickness;
1228
+ let xRight = rootPad.left + blockWidth;
1229
+ let maxBottom = rootPad.top;
1230
+ for (const column of columns) {
1231
+ xRight -= column.thickness;
1232
+ const colLeft = xRight;
1233
+ let y = rootPad.top;
1234
+ for (const c of column.chars) {
1235
+ const advance = c.advanceWidth;
1236
+ const contentWidth = c.advanceHeight;
1237
+ const fontHeight = c.fontHeight;
1238
+ c.inlineBox.top = y;
1239
+ c.inlineBox.height = advance;
1240
+ c.inlineBox.width = contentWidth;
1241
+ c.inlineBox.left = colLeft + (column.thickness - contentWidth) / 2;
1242
+ c.lineBox.left = c.inlineBox.left + (contentWidth - fontHeight) / 2;
1243
+ c.lineBox.top = y;
1244
+ c.lineBox.width = fontHeight;
1245
+ c.lineBox.height = advance;
1246
+ y += this._advance(c);
1247
+ }
1248
+ if (y > maxBottom) {
1249
+ maxBottom = y;
1250
+ }
1251
+ }
1252
+ for (const paragraph of paragraphs) {
1253
+ for (const fragment of paragraph.fragments) {
1254
+ this._unionInto(fragment.inlineBox, fragment.characters.map((c) => c.inlineBox));
1255
+ }
1256
+ this._unionInto(
1257
+ paragraph.lineBox,
1258
+ paragraph.fragments.flatMap((f) => f.characters.map((c) => c.inlineBox))
1259
+ );
1260
+ }
1261
+ const totalWidth = hasWidth ? rootStyle.width : blockWidth + rootPad.left + rootPad.right;
1262
+ const totalHeight = hasHeight ? rootStyle.height : maxBottom + rootPad.bottom;
1263
+ return {
1264
+ paragraphs,
1265
+ boundingBox: new modernPath2d.BoundingBox(0, 0, totalWidth, totalHeight)
1266
+ };
1267
+ }
1268
+ /** Advance step including CSS letter-spacing (px). */
1269
+ _advance(character) {
1270
+ return character.advanceWidth + (character.computedStyle.letterSpacing ?? 0);
1271
+ }
1272
+ /**
1273
+ * Break a paragraph's characters into visual lines.
1274
+ * v1: `word-break: break-all` (break before any character that would overflow)
1275
+ * plus explicit `\n`/`\r` hard breaks. The newline itself occupies no line box.
1276
+ */
1277
+ _breakLines(paragraph, avail) {
1278
+ const lines = [];
1279
+ let current = [];
1280
+ let width = 0;
1281
+ const flush = () => {
1282
+ lines.push(current);
1283
+ current = [];
1284
+ width = 0;
1285
+ };
1286
+ for (const fragment of paragraph.fragments) {
1287
+ for (const c of fragment.characters) {
1288
+ if (c.content === "\n" || c.content === "\r") {
1289
+ flush();
1290
+ continue;
1291
+ }
1292
+ if (avail !== Infinity && current.length > 0 && width + c.advanceWidth > avail) {
1293
+ flush();
1294
+ }
1295
+ current.push(c);
1296
+ width += this._advance(c);
1297
+ }
1298
+ }
1299
+ flush();
1300
+ return lines;
1301
+ }
1302
+ _unionInto(target, boxes) {
1303
+ const used = boxes.filter((b) => b.width !== 0 || b.height !== 0);
1304
+ if (!used.length) {
1305
+ return;
1306
+ }
1307
+ const u = modernPath2d.BoundingBox.from(...used);
1308
+ target.left = u.left;
1309
+ target.top = u.top;
1310
+ target.width = u.width;
1311
+ target.height = u.height;
1312
+ }
1313
+ _shiftAll(paragraphs, dy) {
1314
+ for (const paragraph of paragraphs) {
1315
+ paragraph.lineBox.top += dy;
1316
+ for (const fragment of paragraph.fragments) {
1317
+ fragment.inlineBox.top += dy;
1318
+ for (const character of fragment.characters) {
1319
+ character.inlineBox.top += dy;
1320
+ character.lineBox.top += dy;
1321
+ }
1322
+ }
1323
+ }
1324
+ }
1325
+ dispose() {
1326
+ }
1327
+ }
1328
+
1070
1329
  function backgroundPlugin() {
1071
1330
  const pathSet = new modernPath2d.Path2DSet();
1072
1331
  const loader = createSvgLoader();
@@ -1825,7 +2084,7 @@ class Text extends modernIdoc.Reactivable {
1825
2084
  glyphBox = new modernPath2d.BoundingBox();
1826
2085
  pathBox = new modernPath2d.BoundingBox();
1827
2086
  boundingBox = new modernPath2d.BoundingBox();
1828
- measurer = new Measurer();
2087
+ measurer = new DomMeasurer();
1829
2088
  plugins = /* @__PURE__ */ new Map();
1830
2089
  pathSets = [];
1831
2090
  _paragraphs = [];
@@ -1883,6 +2142,12 @@ class Text extends modernIdoc.Reactivable {
1883
2142
  outline,
1884
2143
  deformation: deformation$1
1885
2144
  } = modernIdoc.normalizeText(options);
2145
+ if (options.measurer && typeof options.measurer !== "string") {
2146
+ this.measurer = options.measurer;
2147
+ } else {
2148
+ const kind = options.measurer ?? (fonts ? "font" : "dom");
2149
+ this.measurer = kind === "font" ? new FontMeasurer() : new DomMeasurer();
2150
+ }
1886
2151
  this.debug = options.debug ?? false;
1887
2152
  this.content = content;
1888
2153
  this.effects = effects;
@@ -1922,7 +2187,35 @@ class Text extends modernIdoc.Reactivable {
1922
2187
  }
1923
2188
  async load() {
1924
2189
  this._update();
1925
- await Promise.all(Array.from(this.plugins.values()).map((p) => p.load?.(this)));
2190
+ await Promise.all([
2191
+ this._decodeFonts(),
2192
+ ...Array.from(this.plugins.values()).map((p) => p.load?.(this))
2193
+ ]);
2194
+ }
2195
+ /**
2196
+ * Eagerly decode the fonts this text uses, off the main thread — WOFF tables
2197
+ * are decompressed via modern-font's async `createSFNTAsync` (fflate async).
2198
+ * This warms the SFNT cache so the synchronous `measure()` / `render()` pass
2199
+ * never stalls the main thread inflating WOFF tables on first glyph access.
2200
+ *
2201
+ * No-op for already-decoded fonts and for formats without async decoding.
2202
+ */
2203
+ async _decodeFonts() {
2204
+ const fonts = this.fonts ?? modernFont.fonts;
2205
+ const entries = /* @__PURE__ */ new Set();
2206
+ for (const character of this.characters) {
2207
+ const family = character.computedStyle.fontFamily;
2208
+ if (family) {
2209
+ entries.add(fonts.get(family));
2210
+ }
2211
+ }
2212
+ entries.add(fonts.fallbackFont);
2213
+ await Promise.all(Array.from(entries, async (entry) => {
2214
+ const font = entry?.getFont();
2215
+ if (font && typeof font.createSFNTAsync === "function" && !font._sfnt) {
2216
+ font._sfnt = await font.createSFNTAsync();
2217
+ }
2218
+ }));
1926
2219
  }
1927
2220
  _update() {
1928
2221
  this.computedStyle = { ...textDefaultStyle, ...this.style };
@@ -1957,6 +2250,9 @@ class Text extends modernIdoc.Reactivable {
1957
2250
  }
1958
2251
  createDom() {
1959
2252
  this._update();
2253
+ if (!this.measurer.createDom) {
2254
+ throw new Error("current measurer does not support createDom()");
2255
+ }
1960
2256
  return this.measurer.createDom(this.paragraphs, this.computedStyle);
1961
2257
  }
1962
2258
  measure(dom = this.measureDom) {
@@ -1970,7 +2266,7 @@ class Text extends modernIdoc.Reactivable {
1970
2266
  boundingBox: this.boundingBox
1971
2267
  };
1972
2268
  this._update();
1973
- const result = this.measurer.measure(this.paragraphs, this.computedStyle, dom);
2269
+ const result = this.measurer.measure(this.paragraphs, this.computedStyle, dom, this.fonts);
1974
2270
  this.paragraphs = result.paragraphs;
1975
2271
  this.lineBox = result.boundingBox;
1976
2272
  const characters = this.characters;
@@ -2102,7 +2398,7 @@ class Text extends modernIdoc.Reactivable {
2102
2398
  options.onContext?.(ctx);
2103
2399
  }
2104
2400
  dispose() {
2105
- this.measurer.dispose();
2401
+ this.measurer.dispose?.();
2106
2402
  this._renderer = void 0;
2107
2403
  this._rendererCtx = void 0;
2108
2404
  }
@@ -2140,8 +2436,9 @@ __decorateClass([
2140
2436
 
2141
2437
  exports.Canvas2DRenderer = Canvas2DRenderer;
2142
2438
  exports.Character = Character;
2439
+ exports.DomMeasurer = DomMeasurer;
2440
+ exports.FontMeasurer = FontMeasurer;
2143
2441
  exports.Fragment = Fragment;
2144
- exports.Measurer = Measurer;
2145
2442
  exports.Paragraph = Paragraph;
2146
2443
  exports.Text = Text;
2147
2444
  exports.backgroundPlugin = backgroundPlugin;
@@ -57,6 +57,16 @@ declare class Character {
57
57
  constructor(content: string, index: number, parent: Fragment);
58
58
  protected _getFontSFNT(fonts?: Fonts): SFNT | undefined;
59
59
  updateGlyph(sfnt?: SFNT | undefined): this;
60
+ /**
61
+ * Populate glyph metrics only (advance width/height, ascender/descender,
62
+ * baseline, …) without building the glyph `path` or touching boxes.
63
+ *
64
+ * The DOM {@link DomMeasurer} never needs this — it reads positions back from the
65
+ * browser. A pure-JS measurer (e.g. `FontMeasurer`) must know advances *before*
66
+ * it can place characters, so it calls this ahead of layout. `update()` later
67
+ * recomputes the same metrics while building the path, so this is idempotent.
68
+ */
69
+ measureGlyph(fonts?: Fonts): this;
60
70
  update(fonts?: Fonts): this;
61
71
  protected _italic(path: Path2D, startPoint?: Vector2Like): void;
62
72
  getGlyphMinMax(min?: Vector2, max?: Vector2, withStyle?: boolean): {
@@ -66,24 +76,6 @@ declare class Character {
66
76
  getGlyphBoundingBox(withStyle?: boolean): BoundingBox | undefined;
67
77
  }
68
78
 
69
- interface Plugin {
70
- name: string;
71
- pathSet?: Path2DSet;
72
- getBoundingBox?: (text: Text$1) => BoundingBox | undefined;
73
- update?: (text: Text$1) => void;
74
- updateOrder?: number;
75
- render?: (renderer: Canvas2DRenderer) => void;
76
- renderOrder?: number;
77
- load?: (text: Text$1) => Promise<void>;
78
- context?: Record<string, any>;
79
- }
80
- interface Options extends TextObject {
81
- debug?: boolean;
82
- measureDom?: HTMLElement;
83
- fonts?: Fonts;
84
- plugins?: Plugin[];
85
- }
86
-
87
79
  interface MeasuredParagraph {
88
80
  paragraphIndex: number;
89
81
  left: number;
@@ -127,7 +119,7 @@ interface RootDomStyles {
127
119
  section: Record<string, any>;
128
120
  ul: Record<string, any>;
129
121
  }
130
- declare class Measurer {
122
+ declare class DomMeasurer {
131
123
  static notZeroStyles: Set<string>;
132
124
  static pxStyles: Set<string>;
133
125
  protected _styleCache: WeakMap<object, Record<string, any>>;
@@ -166,6 +158,47 @@ declare class Measurer {
166
158
  dispose(): void;
167
159
  }
168
160
 
161
+ interface Plugin {
162
+ name: string;
163
+ pathSet?: Path2DSet;
164
+ getBoundingBox?: (text: Text$1) => BoundingBox | undefined;
165
+ update?: (text: Text$1) => void;
166
+ updateOrder?: number;
167
+ render?: (renderer: Canvas2DRenderer) => void;
168
+ renderOrder?: number;
169
+ load?: (text: Text$1) => Promise<void>;
170
+ context?: Record<string, any>;
171
+ }
172
+ /**
173
+ * Pluggable layout backend. Implementations fill the four-level boxes
174
+ * (`character.inlineBox`/`lineBox`, `fragment.inlineBox`, `paragraph.lineBox`)
175
+ * in place and return the overall bounding box.
176
+ *
177
+ * - `DomMeasurer` — DOM-based, uses the browser as ground truth (default).
178
+ * - `FontMeasurer` — pure-JS, DOM-free; needs `fonts` to resolve glyph advances.
179
+ *
180
+ * `fonts` is passed positionally by `Text.measure()`; DOM-based measurers ignore
181
+ * it (a method may safely declare fewer parameters than the interface).
182
+ */
183
+ interface TextMeasurer {
184
+ measure: (paragraphs: Paragraph[], rootStyle: FullStyle, dom?: HTMLElement, fonts?: Fonts) => MeasureDomResult;
185
+ createDom?: (paragraphs: Paragraph[], rootStyle: FullStyle) => HTMLElement;
186
+ dispose?: () => void;
187
+ }
188
+ /** Built-in layout backends. `'font'` → `FontMeasurer`, `'dom'` → `DomMeasurer`. */
189
+ type MeasurerKind = 'dom' | 'font';
190
+ interface Options extends TextObject {
191
+ debug?: boolean;
192
+ measureDom?: HTMLElement;
193
+ fonts?: Fonts;
194
+ plugins?: Plugin[];
195
+ /**
196
+ * Layout backend: `'font'` (pure-JS) or `'dom'` (browser), or a custom
197
+ * `TextMeasurer`. Defaults to `'font'` when `fonts` are provided, else `'dom'`.
198
+ */
199
+ measurer?: MeasurerKind | TextMeasurer;
200
+ }
201
+
169
202
  interface RenderOptions {
170
203
  view: HTMLCanvasElement;
171
204
  pixelRatio?: number;
@@ -221,7 +254,7 @@ declare class Text$1 extends Reactivable {
221
254
  glyphBox: BoundingBox;
222
255
  pathBox: BoundingBox;
223
256
  boundingBox: BoundingBox;
224
- measurer: Measurer;
257
+ measurer: TextMeasurer;
225
258
  plugins: Map<string, Plugin>;
226
259
  pathSets: Path2DSet[];
227
260
  protected _paragraphs: Paragraph[];
@@ -246,6 +279,15 @@ declare class Text$1 extends Reactivable {
246
279
  characterIndex: number;
247
280
  }) => void): this;
248
281
  load(): Promise<void>;
282
+ /**
283
+ * Eagerly decode the fonts this text uses, off the main thread — WOFF tables
284
+ * are decompressed via modern-font's async `createSFNTAsync` (fflate async).
285
+ * This warms the SFNT cache so the synchronous `measure()` / `render()` pass
286
+ * never stalls the main thread inflating WOFF tables on first glyph access.
287
+ *
288
+ * No-op for already-decoded fonts and for formats without async decoding.
289
+ */
290
+ protected _decodeFonts(): Promise<void>;
249
291
  protected _update(): this;
250
292
  createDom(): HTMLElement;
251
293
  measure(dom?: HTMLElement | undefined): MeasureResult;
@@ -304,5 +346,5 @@ declare class Canvas2DRenderer {
304
346
  drawCharacter: (character: Character, effect?: NormalizedEffect) => void;
305
347
  }
306
348
 
307
- export { Canvas2DRenderer as C, Fragment as F, Paragraph as P, Text$1 as T, Character as a, Measurer as g, textDefaultStyle as t };
308
- export type { DrawShapePathsOptions as D, MeasureDomResult as M, Options as O, RenderOptions as R, MeasureResult as b, MeasuredCharacter as c, MeasuredCharacterRect as d, MeasuredFragment as e, MeasuredParagraph as f, Plugin as h, TextEvents as i };
349
+ export { Canvas2DRenderer as C, DomMeasurer as D, Fragment as F, Paragraph as P, Text$1 as T, Character as a, textDefaultStyle as t };
350
+ export type { MeasureDomResult as M, Options as O, RenderOptions as R, DrawShapePathsOptions as b, MeasureResult as c, MeasuredCharacter as d, MeasuredCharacterRect as e, MeasuredFragment as f, MeasuredParagraph as g, MeasurerKind as h, Plugin as i, TextEvents as j, TextMeasurer as k };
@@ -57,6 +57,16 @@ declare class Character {
57
57
  constructor(content: string, index: number, parent: Fragment);
58
58
  protected _getFontSFNT(fonts?: Fonts): SFNT | undefined;
59
59
  updateGlyph(sfnt?: SFNT | undefined): this;
60
+ /**
61
+ * Populate glyph metrics only (advance width/height, ascender/descender,
62
+ * baseline, …) without building the glyph `path` or touching boxes.
63
+ *
64
+ * The DOM {@link DomMeasurer} never needs this — it reads positions back from the
65
+ * browser. A pure-JS measurer (e.g. `FontMeasurer`) must know advances *before*
66
+ * it can place characters, so it calls this ahead of layout. `update()` later
67
+ * recomputes the same metrics while building the path, so this is idempotent.
68
+ */
69
+ measureGlyph(fonts?: Fonts): this;
60
70
  update(fonts?: Fonts): this;
61
71
  protected _italic(path: Path2D, startPoint?: Vector2Like): void;
62
72
  getGlyphMinMax(min?: Vector2, max?: Vector2, withStyle?: boolean): {
@@ -66,24 +76,6 @@ declare class Character {
66
76
  getGlyphBoundingBox(withStyle?: boolean): BoundingBox | undefined;
67
77
  }
68
78
 
69
- interface Plugin {
70
- name: string;
71
- pathSet?: Path2DSet;
72
- getBoundingBox?: (text: Text$1) => BoundingBox | undefined;
73
- update?: (text: Text$1) => void;
74
- updateOrder?: number;
75
- render?: (renderer: Canvas2DRenderer) => void;
76
- renderOrder?: number;
77
- load?: (text: Text$1) => Promise<void>;
78
- context?: Record<string, any>;
79
- }
80
- interface Options extends TextObject {
81
- debug?: boolean;
82
- measureDom?: HTMLElement;
83
- fonts?: Fonts;
84
- plugins?: Plugin[];
85
- }
86
-
87
79
  interface MeasuredParagraph {
88
80
  paragraphIndex: number;
89
81
  left: number;
@@ -127,7 +119,7 @@ interface RootDomStyles {
127
119
  section: Record<string, any>;
128
120
  ul: Record<string, any>;
129
121
  }
130
- declare class Measurer {
122
+ declare class DomMeasurer {
131
123
  static notZeroStyles: Set<string>;
132
124
  static pxStyles: Set<string>;
133
125
  protected _styleCache: WeakMap<object, Record<string, any>>;
@@ -166,6 +158,47 @@ declare class Measurer {
166
158
  dispose(): void;
167
159
  }
168
160
 
161
+ interface Plugin {
162
+ name: string;
163
+ pathSet?: Path2DSet;
164
+ getBoundingBox?: (text: Text$1) => BoundingBox | undefined;
165
+ update?: (text: Text$1) => void;
166
+ updateOrder?: number;
167
+ render?: (renderer: Canvas2DRenderer) => void;
168
+ renderOrder?: number;
169
+ load?: (text: Text$1) => Promise<void>;
170
+ context?: Record<string, any>;
171
+ }
172
+ /**
173
+ * Pluggable layout backend. Implementations fill the four-level boxes
174
+ * (`character.inlineBox`/`lineBox`, `fragment.inlineBox`, `paragraph.lineBox`)
175
+ * in place and return the overall bounding box.
176
+ *
177
+ * - `DomMeasurer` — DOM-based, uses the browser as ground truth (default).
178
+ * - `FontMeasurer` — pure-JS, DOM-free; needs `fonts` to resolve glyph advances.
179
+ *
180
+ * `fonts` is passed positionally by `Text.measure()`; DOM-based measurers ignore
181
+ * it (a method may safely declare fewer parameters than the interface).
182
+ */
183
+ interface TextMeasurer {
184
+ measure: (paragraphs: Paragraph[], rootStyle: FullStyle, dom?: HTMLElement, fonts?: Fonts) => MeasureDomResult;
185
+ createDom?: (paragraphs: Paragraph[], rootStyle: FullStyle) => HTMLElement;
186
+ dispose?: () => void;
187
+ }
188
+ /** Built-in layout backends. `'font'` → `FontMeasurer`, `'dom'` → `DomMeasurer`. */
189
+ type MeasurerKind = 'dom' | 'font';
190
+ interface Options extends TextObject {
191
+ debug?: boolean;
192
+ measureDom?: HTMLElement;
193
+ fonts?: Fonts;
194
+ plugins?: Plugin[];
195
+ /**
196
+ * Layout backend: `'font'` (pure-JS) or `'dom'` (browser), or a custom
197
+ * `TextMeasurer`. Defaults to `'font'` when `fonts` are provided, else `'dom'`.
198
+ */
199
+ measurer?: MeasurerKind | TextMeasurer;
200
+ }
201
+
169
202
  interface RenderOptions {
170
203
  view: HTMLCanvasElement;
171
204
  pixelRatio?: number;
@@ -221,7 +254,7 @@ declare class Text$1 extends Reactivable {
221
254
  glyphBox: BoundingBox;
222
255
  pathBox: BoundingBox;
223
256
  boundingBox: BoundingBox;
224
- measurer: Measurer;
257
+ measurer: TextMeasurer;
225
258
  plugins: Map<string, Plugin>;
226
259
  pathSets: Path2DSet[];
227
260
  protected _paragraphs: Paragraph[];
@@ -246,6 +279,15 @@ declare class Text$1 extends Reactivable {
246
279
  characterIndex: number;
247
280
  }) => void): this;
248
281
  load(): Promise<void>;
282
+ /**
283
+ * Eagerly decode the fonts this text uses, off the main thread — WOFF tables
284
+ * are decompressed via modern-font's async `createSFNTAsync` (fflate async).
285
+ * This warms the SFNT cache so the synchronous `measure()` / `render()` pass
286
+ * never stalls the main thread inflating WOFF tables on first glyph access.
287
+ *
288
+ * No-op for already-decoded fonts and for formats without async decoding.
289
+ */
290
+ protected _decodeFonts(): Promise<void>;
249
291
  protected _update(): this;
250
292
  createDom(): HTMLElement;
251
293
  measure(dom?: HTMLElement | undefined): MeasureResult;
@@ -304,5 +346,5 @@ declare class Canvas2DRenderer {
304
346
  drawCharacter: (character: Character, effect?: NormalizedEffect) => void;
305
347
  }
306
348
 
307
- export { Canvas2DRenderer as C, Fragment as F, Paragraph as P, Text$1 as T, Character as a, Measurer as g, textDefaultStyle as t };
308
- export type { DrawShapePathsOptions as D, MeasureDomResult as M, Options as O, RenderOptions as R, MeasureResult as b, MeasuredCharacter as c, MeasuredCharacterRect as d, MeasuredFragment as e, MeasuredParagraph as f, Plugin as h, TextEvents as i };
349
+ export { Canvas2DRenderer as C, DomMeasurer as D, Fragment as F, Paragraph as P, Text$1 as T, Character as a, textDefaultStyle as t };
350
+ export type { MeasureDomResult as M, Options as O, RenderOptions as R, DrawShapePathsOptions as b, MeasureResult as c, MeasuredCharacter as d, MeasuredCharacterRect as e, MeasuredFragment as f, MeasuredParagraph as g, MeasurerKind as h, Plugin as i, TextEvents as j, TextMeasurer as k };