modern-text 1.11.1 → 2.0.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.
Files changed (30) hide show
  1. package/README.md +283 -22
  2. package/dist/deformations/index.cjs +566 -0
  3. package/dist/deformations/index.d.cts +11 -0
  4. package/dist/deformations/index.d.mts +11 -0
  5. package/dist/deformations/index.d.ts +11 -0
  6. package/dist/deformations/index.mjs +563 -0
  7. package/dist/index.cjs +15 -3
  8. package/dist/index.d.cts +186 -6
  9. package/dist/index.d.mts +186 -6
  10. package/dist/index.d.ts +186 -6
  11. package/dist/index.js +6 -5
  12. package/dist/index.mjs +4 -2
  13. package/dist/shared/modern-text.B2xfrqDc.cjs +556 -0
  14. package/dist/shared/modern-text.BD7PBYt7.d.cts +100 -0
  15. package/dist/shared/modern-text.BxijkspX.d.ts +100 -0
  16. package/dist/shared/{modern-text.BKZQdmgG.cjs → modern-text.CBgc-cQ1.cjs} +288 -18
  17. package/dist/shared/modern-text.CYa4lfoG.d.mts +100 -0
  18. package/dist/shared/{modern-text.Dqw5Z6MV.mjs → modern-text.ChzjFjsk.mjs} +283 -13
  19. package/dist/shared/{modern-text.Db7Uoht6.d.cts → modern-text.D4WopQCu.d.cts} +56 -22
  20. package/dist/shared/{modern-text.Db7Uoht6.d.mts → modern-text.D4WopQCu.d.mts} +56 -22
  21. package/dist/shared/{modern-text.Db7Uoht6.d.ts → modern-text.D4WopQCu.d.ts} +56 -22
  22. package/dist/shared/modern-text.JF1ny7A-.mjs +550 -0
  23. package/dist/shared/modern-text.MC5bIC9E.cjs +316 -0
  24. package/dist/shared/modern-text.fT17R5HY.mjs +310 -0
  25. package/dist/web-components/index.cjs +2 -1
  26. package/dist/web-components/index.d.cts +1 -1
  27. package/dist/web-components/index.d.mts +1 -1
  28. package/dist/web-components/index.d.ts +1 -1
  29. package/dist/web-components/index.mjs +2 -1
  30. package/package.json +12 -7
@@ -1,6 +1,7 @@
1
1
  import { isNone, isGradient, normalizeGradient, clearUndef, Reactivable, normalizeText, getDefaultStyle, property } from 'modern-idoc';
2
2
  import { svgToDom, svgToPath2DSet, Path2DSet, Transform2D, setCanvasContext, Path2D, BoundingBox, Vector2 } from 'modern-path2d';
3
3
  import { fonts } from 'modern-font';
4
+ import { a as definePlugin, b as deformationPlugin } from './modern-text.JF1ny7A-.mjs';
4
5
 
5
6
  function createSvgLoader() {
6
7
  const loaded = /* @__PURE__ */ new Map();
@@ -528,6 +529,18 @@ class Character {
528
529
  this.fontStyle = fsSelectionMap[os2.fsSelection] ?? macStyleMap[head.macStyle];
529
530
  return this;
530
531
  }
532
+ /**
533
+ * Populate glyph metrics only (advance width/height, ascender/descender,
534
+ * baseline, …) without building the glyph `path` or touching boxes.
535
+ *
536
+ * The DOM {@link DomMeasurer} never needs this — it reads positions back from the
537
+ * browser. A pure-JS measurer (e.g. `FontMeasurer`) must know advances *before*
538
+ * it can place characters, so it calls this ahead of layout. `update()` later
539
+ * recomputes the same metrics while building the path, so this is idempotent.
540
+ */
541
+ measureGlyph(fonts) {
542
+ return this.updateGlyph(this._getFontSFNT(fonts));
543
+ }
531
544
  update(fonts) {
532
545
  const sfnt = this._getFontSFNT(fonts);
533
546
  if (!sfnt) {
@@ -702,10 +715,6 @@ class Paragraph {
702
715
  }
703
716
  }
704
717
 
705
- function definePlugin(options) {
706
- return options;
707
- }
708
-
709
718
  let sharedContainer;
710
719
  function getSharedContainer() {
711
720
  if (sharedContainer?.isConnected) {
@@ -724,7 +733,7 @@ function getSharedContainer() {
724
733
  sharedContainer = container;
725
734
  return container;
726
735
  }
727
- class Measurer {
736
+ class DomMeasurer {
728
737
  static notZeroStyles = /* @__PURE__ */ new Set([
729
738
  "width",
730
739
  "height"
@@ -759,7 +768,7 @@ class Measurer {
759
768
  return cached;
760
769
  }
761
770
  const domStyle = {};
762
- const { notZeroStyles, pxStyles } = Measurer;
771
+ const { notZeroStyles, pxStyles } = DomMeasurer;
763
772
  for (const key in style) {
764
773
  const value = style[key];
765
774
  if (notZeroStyles.has(key) && value === 0) {
@@ -1068,6 +1077,253 @@ class Measurer {
1068
1077
  }
1069
1078
  }
1070
1079
 
1080
+ function side(style, name, edge) {
1081
+ return style[`${name}${edge}`] ?? style[name] ?? 0;
1082
+ }
1083
+ class FontMeasurer {
1084
+ measure(paragraphs, rootStyle, _dom, fonts$1) {
1085
+ const _fonts = fonts$1 ?? fonts;
1086
+ for (const paragraph of paragraphs) {
1087
+ for (const fragment of paragraph.fragments) {
1088
+ for (const character of fragment.characters) {
1089
+ character.measureGlyph(_fonts);
1090
+ }
1091
+ }
1092
+ }
1093
+ return rootStyle.writingMode.includes("vertical") ? this._measureVertical(paragraphs, rootStyle) : this._measureHorizontal(paragraphs, rootStyle);
1094
+ }
1095
+ _rootPadding(rootStyle) {
1096
+ return {
1097
+ top: side(rootStyle, "padding", "Top"),
1098
+ right: side(rootStyle, "padding", "Right"),
1099
+ bottom: side(rootStyle, "padding", "Bottom"),
1100
+ left: side(rootStyle, "padding", "Left")
1101
+ };
1102
+ }
1103
+ _measureHorizontal(paragraphs, rootStyle) {
1104
+ const rootPad = this._rootPadding(rootStyle);
1105
+ const hasWidth = typeof rootStyle.width === "number";
1106
+ const availWidth = hasWidth ? rootStyle.width - rootPad.left - rootPad.right : Infinity;
1107
+ let y = rootPad.top;
1108
+ let maxRight = rootPad.left;
1109
+ for (const paragraph of paragraphs) {
1110
+ const pStyle = paragraph.computedStyle;
1111
+ const pBox = paragraph.style;
1112
+ const mTop = side(pBox, "margin", "Top");
1113
+ const mBottom = side(pBox, "margin", "Bottom");
1114
+ const mLeft = side(pBox, "margin", "Left");
1115
+ const mRight = side(pBox, "margin", "Right");
1116
+ const pTop = side(pBox, "padding", "Top");
1117
+ const pBottom = side(pBox, "padding", "Bottom");
1118
+ const pLeft = side(pBox, "padding", "Left");
1119
+ const pRight = side(pBox, "padding", "Right");
1120
+ const liLeft = rootPad.left + mLeft + pLeft;
1121
+ const liAvail = availWidth === Infinity ? Infinity : availWidth - mLeft - mRight - pLeft - pRight;
1122
+ y += mTop + pTop;
1123
+ const paraTop = y;
1124
+ let paraRight = liLeft;
1125
+ const lines = this._breakLines(paragraph, liAvail);
1126
+ const align = pStyle.textAlign;
1127
+ const indent = pStyle.textIndent ?? 0;
1128
+ for (let i = 0; i < lines.length; i++) {
1129
+ const line = lines[i];
1130
+ const lineIndent = i === 0 ? indent : 0;
1131
+ let lineHeight = pStyle.fontSize * pStyle.lineHeight;
1132
+ let contentWidth = 0;
1133
+ for (const c of line) {
1134
+ if (c.fontHeight > lineHeight) {
1135
+ lineHeight = c.fontHeight;
1136
+ }
1137
+ contentWidth += this._advance(c);
1138
+ }
1139
+ let x = liLeft + lineIndent;
1140
+ if (liAvail !== Infinity) {
1141
+ const slack = liAvail - lineIndent - contentWidth;
1142
+ if (align === "center") {
1143
+ x += slack / 2;
1144
+ } else if (align === "end" || align === "right") {
1145
+ x += slack;
1146
+ }
1147
+ }
1148
+ for (const c of line) {
1149
+ const adv = c.advanceWidth;
1150
+ const contentHeight = c.advanceHeight;
1151
+ const fontHeight = c.fontHeight;
1152
+ c.inlineBox.left = x;
1153
+ c.inlineBox.top = y + (lineHeight - contentHeight) / 2;
1154
+ c.inlineBox.width = adv;
1155
+ c.inlineBox.height = contentHeight;
1156
+ c.lineBox.left = x;
1157
+ c.lineBox.top = c.inlineBox.top + (contentHeight - fontHeight) / 2;
1158
+ c.lineBox.width = adv;
1159
+ c.lineBox.height = fontHeight;
1160
+ x += this._advance(c);
1161
+ }
1162
+ if (x > paraRight) {
1163
+ paraRight = x;
1164
+ }
1165
+ y += lineHeight;
1166
+ }
1167
+ if (paraRight > maxRight) {
1168
+ maxRight = paraRight;
1169
+ }
1170
+ for (const fragment of paragraph.fragments) {
1171
+ this._unionInto(fragment.inlineBox, fragment.characters.map((c) => c.inlineBox));
1172
+ }
1173
+ paragraph.lineBox.left = liLeft;
1174
+ paragraph.lineBox.top = paraTop;
1175
+ paragraph.lineBox.width = liAvail === Infinity ? paraRight - liLeft : liAvail;
1176
+ paragraph.lineBox.height = y - paraTop;
1177
+ y += pBottom + mBottom;
1178
+ }
1179
+ const contentBottom = y + rootPad.bottom;
1180
+ const totalWidth = hasWidth ? rootStyle.width : maxRight + rootPad.right;
1181
+ const totalHeight = typeof rootStyle.height === "number" ? rootStyle.height : contentBottom;
1182
+ if (typeof rootStyle.height === "number") {
1183
+ const slack = totalHeight - contentBottom;
1184
+ const va = rootStyle.verticalAlign;
1185
+ const dy = va === "middle" ? slack / 2 : va === "bottom" ? slack : 0;
1186
+ if (dy) {
1187
+ this._shiftAll(paragraphs, dy);
1188
+ }
1189
+ }
1190
+ return {
1191
+ paragraphs,
1192
+ boundingBox: new BoundingBox(0, 0, totalWidth, totalHeight)
1193
+ };
1194
+ }
1195
+ /**
1196
+ * Vertical writing-mode (`vertical-rl`): columns stack right-to-left, glyphs
1197
+ * flow top→bottom. It is the horizontal layout with the inline and block axes
1198
+ * swapped — the inline (down-column) advance is `advanceWidth` (CJK upright = em;
1199
+ * Latin is rotated, so its advance ≈ its width), the cross-axis content box is
1200
+ * `advanceHeight`, and the column thickness is `fontHeight`. The lineBox is
1201
+ * derived exactly as DomMeasurer.measureParagraphDom's vertical branch, so it
1202
+ * stays accurate even though inlineBox carries the content-box rounding residual.
1203
+ *
1204
+ * v1: `vertical-rl` only; no per-paragraph margin/padding or block alignment.
1205
+ */
1206
+ _measureVertical(paragraphs, rootStyle) {
1207
+ const rootPad = this._rootPadding(rootStyle);
1208
+ const hasHeight = typeof rootStyle.height === "number";
1209
+ const availHeight = hasHeight ? rootStyle.height - rootPad.top - rootPad.bottom : Infinity;
1210
+ const columns = [];
1211
+ for (const paragraph of paragraphs) {
1212
+ const strut = paragraph.computedStyle.fontSize * paragraph.computedStyle.lineHeight;
1213
+ for (const line of this._breakLines(paragraph, availHeight)) {
1214
+ let thickness = strut;
1215
+ for (const c of line) {
1216
+ if (c.fontHeight > thickness) {
1217
+ thickness = c.fontHeight;
1218
+ }
1219
+ }
1220
+ columns.push({ chars: line, thickness });
1221
+ }
1222
+ }
1223
+ const totalThickness = columns.reduce((sum, c) => sum + c.thickness, 0);
1224
+ const hasWidth = typeof rootStyle.width === "number";
1225
+ const blockWidth = hasWidth ? Math.max(rootStyle.width - rootPad.left - rootPad.right, totalThickness) : totalThickness;
1226
+ let xRight = rootPad.left + blockWidth;
1227
+ let maxBottom = rootPad.top;
1228
+ for (const column of columns) {
1229
+ xRight -= column.thickness;
1230
+ const colLeft = xRight;
1231
+ let y = rootPad.top;
1232
+ for (const c of column.chars) {
1233
+ const advance = c.advanceWidth;
1234
+ const contentWidth = c.advanceHeight;
1235
+ const fontHeight = c.fontHeight;
1236
+ c.inlineBox.top = y;
1237
+ c.inlineBox.height = advance;
1238
+ c.inlineBox.width = contentWidth;
1239
+ c.inlineBox.left = colLeft + (column.thickness - contentWidth) / 2;
1240
+ c.lineBox.left = c.inlineBox.left + (contentWidth - fontHeight) / 2;
1241
+ c.lineBox.top = y;
1242
+ c.lineBox.width = fontHeight;
1243
+ c.lineBox.height = advance;
1244
+ y += this._advance(c);
1245
+ }
1246
+ if (y > maxBottom) {
1247
+ maxBottom = y;
1248
+ }
1249
+ }
1250
+ for (const paragraph of paragraphs) {
1251
+ for (const fragment of paragraph.fragments) {
1252
+ this._unionInto(fragment.inlineBox, fragment.characters.map((c) => c.inlineBox));
1253
+ }
1254
+ this._unionInto(
1255
+ paragraph.lineBox,
1256
+ paragraph.fragments.flatMap((f) => f.characters.map((c) => c.inlineBox))
1257
+ );
1258
+ }
1259
+ const totalWidth = hasWidth ? rootStyle.width : blockWidth + rootPad.left + rootPad.right;
1260
+ const totalHeight = hasHeight ? rootStyle.height : maxBottom + rootPad.bottom;
1261
+ return {
1262
+ paragraphs,
1263
+ boundingBox: new BoundingBox(0, 0, totalWidth, totalHeight)
1264
+ };
1265
+ }
1266
+ /** Advance step including CSS letter-spacing (px). */
1267
+ _advance(character) {
1268
+ return character.advanceWidth + (character.computedStyle.letterSpacing ?? 0);
1269
+ }
1270
+ /**
1271
+ * Break a paragraph's characters into visual lines.
1272
+ * v1: `word-break: break-all` (break before any character that would overflow)
1273
+ * plus explicit `\n`/`\r` hard breaks. The newline itself occupies no line box.
1274
+ */
1275
+ _breakLines(paragraph, avail) {
1276
+ const lines = [];
1277
+ let current = [];
1278
+ let width = 0;
1279
+ const flush = () => {
1280
+ lines.push(current);
1281
+ current = [];
1282
+ width = 0;
1283
+ };
1284
+ for (const fragment of paragraph.fragments) {
1285
+ for (const c of fragment.characters) {
1286
+ if (c.content === "\n" || c.content === "\r") {
1287
+ flush();
1288
+ continue;
1289
+ }
1290
+ if (avail !== Infinity && current.length > 0 && width + c.advanceWidth > avail) {
1291
+ flush();
1292
+ }
1293
+ current.push(c);
1294
+ width += this._advance(c);
1295
+ }
1296
+ }
1297
+ flush();
1298
+ return lines;
1299
+ }
1300
+ _unionInto(target, boxes) {
1301
+ const used = boxes.filter((b) => b.width !== 0 || b.height !== 0);
1302
+ if (!used.length) {
1303
+ return;
1304
+ }
1305
+ const u = BoundingBox.from(...used);
1306
+ target.left = u.left;
1307
+ target.top = u.top;
1308
+ target.width = u.width;
1309
+ target.height = u.height;
1310
+ }
1311
+ _shiftAll(paragraphs, dy) {
1312
+ for (const paragraph of paragraphs) {
1313
+ paragraph.lineBox.top += dy;
1314
+ for (const fragment of paragraph.fragments) {
1315
+ fragment.inlineBox.top += dy;
1316
+ for (const character of fragment.characters) {
1317
+ character.inlineBox.top += dy;
1318
+ character.lineBox.top += dy;
1319
+ }
1320
+ }
1321
+ }
1322
+ }
1323
+ dispose() {
1324
+ }
1325
+ }
1326
+
1071
1327
  function backgroundPlugin() {
1072
1328
  const pathSet = new Path2DSet();
1073
1329
  const loader = createSvgLoader();
@@ -1456,7 +1712,7 @@ function highlightPlugin() {
1456
1712
  }
1457
1713
  boundingBoxs.push(box);
1458
1714
  });
1459
- return BoundingBox.from(...boundingBoxs);
1715
+ return boundingBoxs.length ? BoundingBox.from(...boundingBoxs) : void 0;
1460
1716
  },
1461
1717
  render: (renderer) => {
1462
1718
  const { text, context } = renderer;
@@ -1826,7 +2082,7 @@ class Text extends Reactivable {
1826
2082
  glyphBox = new BoundingBox();
1827
2083
  pathBox = new BoundingBox();
1828
2084
  boundingBox = new BoundingBox();
1829
- measurer = new Measurer();
2085
+ measurer = new DomMeasurer();
1830
2086
  plugins = /* @__PURE__ */ new Map();
1831
2087
  pathSets = [];
1832
2088
  _paragraphs = [];
@@ -1881,8 +2137,15 @@ class Text extends Reactivable {
1881
2137
  measureDom,
1882
2138
  fonts,
1883
2139
  fill,
1884
- outline
2140
+ outline,
2141
+ deformation
1885
2142
  } = normalizeText(options);
2143
+ if (options.measurer && typeof options.measurer !== "string") {
2144
+ this.measurer = options.measurer;
2145
+ } else {
2146
+ const kind = options.measurer ?? (fonts ? "font" : "dom");
2147
+ this.measurer = kind === "font" ? new FontMeasurer() : new DomMeasurer();
2148
+ }
1886
2149
  this.debug = options.debug ?? false;
1887
2150
  this.content = content;
1888
2151
  this.effects = effects;
@@ -1891,7 +2154,8 @@ class Text extends Reactivable {
1891
2154
  this.fonts = fonts;
1892
2155
  this.fill = fill;
1893
2156
  this.outline = outline;
1894
- this.use(backgroundPlugin()).use(outlinePlugin()).use(listStylePlugin()).use(textDecorationPlugin()).use(highlightPlugin()).use(renderPlugin());
2157
+ this.deformation = deformation;
2158
+ this.use(backgroundPlugin()).use(outlinePlugin()).use(listStylePlugin()).use(textDecorationPlugin()).use(highlightPlugin()).use(renderPlugin()).use(deformationPlugin());
1895
2159
  (options.plugins ?? []).forEach((plugin) => {
1896
2160
  this.use(plugin);
1897
2161
  });
@@ -1956,6 +2220,9 @@ class Text extends Reactivable {
1956
2220
  }
1957
2221
  createDom() {
1958
2222
  this._update();
2223
+ if (!this.measurer.createDom) {
2224
+ throw new Error("current measurer does not support createDom()");
2225
+ }
1959
2226
  return this.measurer.createDom(this.paragraphs, this.computedStyle);
1960
2227
  }
1961
2228
  measure(dom = this.measureDom) {
@@ -1969,7 +2236,7 @@ class Text extends Reactivable {
1969
2236
  boundingBox: this.boundingBox
1970
2237
  };
1971
2238
  this._update();
1972
- const result = this.measurer.measure(this.paragraphs, this.computedStyle, dom);
2239
+ const result = this.measurer.measure(this.paragraphs, this.computedStyle, dom, this.fonts);
1973
2240
  this.paragraphs = result.paragraphs;
1974
2241
  this.lineBox = result.boundingBox;
1975
2242
  const characters = this.characters;
@@ -2101,7 +2368,7 @@ class Text extends Reactivable {
2101
2368
  options.onContext?.(ctx);
2102
2369
  }
2103
2370
  dispose() {
2104
- this.measurer.dispose();
2371
+ this.measurer.dispose?.();
2105
2372
  this._renderer = void 0;
2106
2373
  this._rendererCtx = void 0;
2107
2374
  }
@@ -2127,6 +2394,9 @@ __decorateClass([
2127
2394
  __decorateClass([
2128
2395
  property()
2129
2396
  ], Text.prototype, "outline");
2397
+ __decorateClass([
2398
+ property()
2399
+ ], Text.prototype, "deformation");
2130
2400
  __decorateClass([
2131
2401
  property({ internal: true })
2132
2402
  ], Text.prototype, "measureDom");
@@ -2134,4 +2404,4 @@ __decorateClass([
2134
2404
  property({ internal: true })
2135
2405
  ], Text.prototype, "fonts");
2136
2406
 
2137
- export { Canvas2DRenderer as C, Fragment as F, Measurer as M, Paragraph as P, Text as T, Character as a, backgroundPlugin as b, createSvgLoader as c, createSvgParser as d, definePlugin as e, getHighlightStyle as f, getEffectTransform2D as g, highlightPlugin as h, isEqualObject as i, isEqualValue as j, parseTransformOrigin as k, listStylePlugin as l, parseValueNumber as m, textDefaultStyle as n, outlinePlugin as o, parseColormap as p, renderPlugin as r, textDecorationPlugin as t };
2407
+ export { Canvas2DRenderer as C, DomMeasurer as D, FontMeasurer as F, Paragraph as P, Text as T, Character as a, Fragment as b, backgroundPlugin as c, createSvgLoader as d, createSvgParser as e, getHighlightStyle as f, getEffectTransform2D as g, highlightPlugin as h, isEqualObject as i, isEqualValue as j, parseTransformOrigin as k, listStylePlugin as l, parseValueNumber as m, textDefaultStyle as n, outlinePlugin as o, parseColormap as p, renderPlugin as r, textDecorationPlugin as t };
@@ -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;
@@ -207,6 +240,7 @@ declare class Text$1 extends Reactivable {
207
240
  effects?: NormalizedText['effects'];
208
241
  fill?: NormalizedText['fill'];
209
242
  outline?: NormalizedText['outline'];
243
+ deformation?: NormalizedText['deformation'];
210
244
  measureDom?: HTMLElement;
211
245
  fonts?: Fonts;
212
246
  needsUpdate: boolean;
@@ -220,7 +254,7 @@ declare class Text$1 extends Reactivable {
220
254
  glyphBox: BoundingBox;
221
255
  pathBox: BoundingBox;
222
256
  boundingBox: BoundingBox;
223
- measurer: Measurer;
257
+ measurer: TextMeasurer;
224
258
  plugins: Map<string, Plugin>;
225
259
  pathSets: Path2DSet[];
226
260
  protected _paragraphs: Paragraph[];
@@ -303,5 +337,5 @@ declare class Canvas2DRenderer {
303
337
  drawCharacter: (character: Character, effect?: NormalizedEffect) => void;
304
338
  }
305
339
 
306
- export { Canvas2DRenderer as C, Fragment as F, Paragraph as P, Text$1 as T, Character as a, Measurer as g, textDefaultStyle as t };
307
- 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 };
340
+ export { Canvas2DRenderer as C, DomMeasurer as D, Fragment as F, Paragraph as P, Text$1 as T, Character as a, textDefaultStyle as t };
341
+ 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;
@@ -207,6 +240,7 @@ declare class Text$1 extends Reactivable {
207
240
  effects?: NormalizedText['effects'];
208
241
  fill?: NormalizedText['fill'];
209
242
  outline?: NormalizedText['outline'];
243
+ deformation?: NormalizedText['deformation'];
210
244
  measureDom?: HTMLElement;
211
245
  fonts?: Fonts;
212
246
  needsUpdate: boolean;
@@ -220,7 +254,7 @@ declare class Text$1 extends Reactivable {
220
254
  glyphBox: BoundingBox;
221
255
  pathBox: BoundingBox;
222
256
  boundingBox: BoundingBox;
223
- measurer: Measurer;
257
+ measurer: TextMeasurer;
224
258
  plugins: Map<string, Plugin>;
225
259
  pathSets: Path2DSet[];
226
260
  protected _paragraphs: Paragraph[];
@@ -303,5 +337,5 @@ declare class Canvas2DRenderer {
303
337
  drawCharacter: (character: Character, effect?: NormalizedEffect) => void;
304
338
  }
305
339
 
306
- export { Canvas2DRenderer as C, Fragment as F, Paragraph as P, Text$1 as T, Character as a, Measurer as g, textDefaultStyle as t };
307
- 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 };
340
+ export { Canvas2DRenderer as C, DomMeasurer as D, Fragment as F, Paragraph as P, Text$1 as T, Character as a, textDefaultStyle as t };
341
+ 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 };