modern-text 1.12.0 → 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.
package/README.md CHANGED
@@ -18,35 +18,296 @@
18
18
  </a>
19
19
  </p>
20
20
 
21
- ## Usage
21
+ `modern-text` measures and renders rich text on Canvas with a layout model that
22
+ mirrors the browser's. It has no React/Vue dependency, ships ESM + CJS, and can
23
+ run **either in the browser (using the DOM as ground truth) or fully DOM-free in
24
+ Node / SSR / Web Workers**.
25
+
26
+ ## Features
27
+
28
+ - 📐 **DOM-accurate layout** — paragraphs, line wrapping, baselines, alignment.
29
+ - 🧩 **Two interchangeable layout backends**
30
+ - `DomMeasurer` — measures via a hidden DOM tree + `getBoundingClientRect()`.
31
+ - `FontMeasurer` — pure-JS, computes layout from font glyph metrics; runs with
32
+ no `document`, so it works in **Node / SSR / Workers** and is deterministic.
33
+ - ↔️ **Horizontal & vertical** writing modes (`horizontal-tb`, `vertical-rl`).
34
+ - 🅰️ **Rich inline styling** — per-fragment font size/family/weight/style, color,
35
+ letter-spacing, line-height, text-indent, text-align, vertical-align,
36
+ text-decoration (underline / line-through / overline), text-transform,
37
+ text-stroke, padding / margin.
38
+ - 🎨 **Fills & strokes** — solid colors and linear gradients per fragment.
39
+ - 🖍️ **Highlights** — draw an image/SVG behind selected fragments.
40
+ - 🔵 **List markers** — `disc` / `none` / custom image bullets.
41
+ - 🌑 **Effects** — stacked translate / skew / color layers (shadows, offsets).
42
+ - 🌀 **Text deformation** — 34 opt-in presets (arch, bend, wave, trapezoid,
43
+ ellipse, heart, …).
44
+ - ✏️ **`<text-editor>` web component** — cursor, selection and keyboard editing.
45
+
46
+ ## Install
47
+
48
+ ```bash
49
+ npm i modern-text modern-font
50
+ ```
51
+
52
+ `modern-font` provides the font parsing/loading used for measuring and drawing
53
+ glyphs.
54
+
55
+ ## Quick start
22
56
 
23
57
  ```ts
24
58
  import { fonts } from 'modern-font'
25
59
  import { renderText } from 'modern-text'
26
60
 
27
- fonts.loadFallbackFont('/fallback.woff').then(() => {
28
- const view = document.createElement('canvas')
29
- document.body.append(view)
30
-
31
- renderText({
32
- view,
33
- style: {
34
- width: 100,
35
- height: 200,
36
- fontSize: 22,
37
- textDecoration: 'underline',
61
+ await fonts.loadFallbackFont('/fallback.woff')
62
+
63
+ const view = document.createElement('canvas')
64
+ document.body.append(view)
65
+
66
+ renderText({
67
+ view,
68
+ fonts,
69
+ style: { width: 300, fontSize: 22, textDecoration: 'underline' },
70
+ content: [
71
+ {
72
+ letterSpacing: 3,
73
+ fragments: [
74
+ { content: 'He', color: 'red', fontSize: 12 },
75
+ { content: 'llo', color: 'black' },
76
+ ],
38
77
  },
39
- content: [
40
- {
41
- letterSpacing: 3,
42
- fragments: [
43
- { content: 'He', color: 'red', fontSize: 12 },
44
- { content: 'llo', color: 'black' },
78
+ { content: ', ', color: 'grey' },
79
+ { content: 'World!', color: 'black' },
80
+ ],
81
+ })
82
+ ```
83
+
84
+ ## Layout backends
85
+
86
+ By default `modern-text` picks **`'font'`** (the pure-JS `FontMeasurer`) when
87
+ `fonts` are provided, otherwise **`'dom'`** (the browser-based `DomMeasurer`).
88
+ You can force either, or pass a custom `TextMeasurer`:
89
+
90
+ ```ts
91
+ new Text({ fonts, measurer: 'font' }) // pure-JS, DOM-free (default with fonts)
92
+ new Text({ fonts, measurer: 'dom' }) // browser ground truth
93
+ new Text({ measurer: myCustomMeasurer }) // any object implementing TextMeasurer
94
+ ```
95
+
96
+ ### Node / SSR / Workers
97
+
98
+ `FontMeasurer` needs no `document`, so the whole measure → render pipeline runs
99
+ outside the browser. Register fonts from a buffer with `modern-font`:
100
+
101
+ ```ts
102
+ import { readFileSync } from 'node:fs'
103
+ import { Fonts, parseFont } from 'modern-font'
104
+ import { Text } from 'modern-text'
105
+
106
+ const buffer = readFileSync('./fonts/NotoSansSC.woff').buffer
107
+ const font = parseFont(buffer)
108
+ const sfnt = font.createSFNT() // .woff → SFNT
109
+ const fonts = new Fonts()
110
+ const entry = { src: '', familySet: new Set(['Noto']), buffer, getFont: () => font, getSFNT: () => sfnt } as any
111
+ fonts.set('Noto', entry)
112
+ fonts.setFallbackFont(entry)
113
+
114
+ const text = new Text({ fonts, content: '你好世界', style: { fontFamily: 'Noto', fontSize: 32 } })
115
+ const result = text.measure() // → boxes for every paragraph / fragment / character
116
+ ```
117
+
118
+ ## Content model
119
+
120
+ Content is a hierarchy: **Text → Paragraph → Fragment → Character**. Each level
121
+ inherits and merges style downward. `content` accepts several shapes that are
122
+ normalized by [`modern-idoc`](https://github.com/qq15725/modern-idoc):
123
+
124
+ ```ts
125
+ // a plain string (single paragraph)
126
+ content: 'Hello World'
127
+
128
+ // an array of paragraphs, each a string or { content, ...paragraphStyle }
129
+ content: [
130
+ { content: 'Title', fontSize: 40, textAlign: 'center' },
131
+ { content: 'Body text', color: '#333' },
132
+ ]
133
+
134
+ // per-fragment styling inside a paragraph
135
+ content: [
136
+ {
137
+ textAlign: 'center',
138
+ fragments: [
139
+ { content: 'red ', color: 'red' },
140
+ { content: 'bold', fontWeight: 'bold' },
141
+ ],
142
+ },
143
+ ]
144
+ ```
145
+
146
+ A newline (`\n`) splits into a new paragraph.
147
+
148
+ ## Styling
149
+
150
+ Style can be set at the text (root), paragraph, or fragment level.
151
+
152
+ ```ts
153
+ style: {
154
+ // box
155
+ width: 400, height: 200, padding: 16,
156
+ // font
157
+ fontSize: 24, fontFamily: 'Arial', fontWeight: 700, fontStyle: 'italic',
158
+ // text
159
+ color: '#222', lineHeight: 1.4, letterSpacing: 1, textIndent: 24,
160
+ textAlign: 'center', // start | left | center | end | right
161
+ verticalAlign: 'middle', // top | middle | bottom
162
+ writingMode: 'vertical-rl', // horizontal-tb | vertical-rl
163
+ textDecoration: 'underline', // underline | line-through | overline | none
164
+ textTransform: 'uppercase', // uppercase | lowercase
165
+ textStrokeWidth: 2, textStrokeColor: '#000', // outline stroke
166
+ }
167
+ ```
168
+
169
+ ### Gradient fills
170
+
171
+ ```ts
172
+ content: [{
173
+ fragments: [{
174
+ content: 'Gradient',
175
+ fill: {
176
+ linearGradient: {
177
+ angle: 180,
178
+ stops: [
179
+ { color: '#c7f1ff', offset: 0 },
180
+ { color: '#ffffff', offset: 1 },
45
181
  ],
46
182
  },
47
- { content: ', ', color: 'grey' },
48
- { content: 'World!', color: 'black' },
49
- ],
50
- })
183
+ },
184
+ }],
185
+ }]
186
+ ```
187
+
188
+ ### Highlights & list markers
189
+
190
+ ```ts
191
+ content: [
192
+ // image drawn behind the fragment
193
+ { fragments: [{ content: 'highlighted', highlightImage: '/brush.svg' }] },
194
+ // list bullet
195
+ { content: 'a bullet item', listStyleType: 'disc' },
196
+ { content: 'a custom bullet', listStyleImage: '/dot.svg' },
197
+ ]
198
+ ```
199
+
200
+ ## Effects
201
+
202
+ `effects` is an ordered stack of transform/color layers drawn behind the main
203
+ glyphs — useful for shadows, 3D offsets and outlines. `translateX/Y` are
204
+ fractions of the font size; `skewX/Y` are degrees.
205
+
206
+ ```ts
207
+ renderText({
208
+ view,
209
+ fonts,
210
+ content: 'Effect',
211
+ style: { fontSize: 80, color: '#FEE90C' },
212
+ effects: [
213
+ { translateX: 0.05, translateY: 0.05, skewY: -5, color: '#000' }, // shadow
214
+ { skewY: -5, color: '#FEE90C' }, // face
215
+ ],
51
216
  })
52
217
  ```
218
+
219
+ ## Text deformation
220
+
221
+ Deformation presets are an opt-in subpath. Register them once, then set
222
+ `deformation.type`:
223
+
224
+ ```ts
225
+ import { registerDeformations } from 'modern-text/deformations'
226
+ import { renderText } from 'modern-text'
227
+
228
+ registerDeformations()
229
+
230
+ renderText({
231
+ view,
232
+ fonts,
233
+ content: 'Deformation',
234
+ style: { fontSize: 100 },
235
+ deformation: { type: 'arch-curve' },
236
+ })
237
+ ```
238
+
239
+ <details>
240
+ <summary>Available presets (34)</summary>
241
+
242
+ `bend` · `bend-vertical` · `arch-curve` · `concave-curve` · `upper-arch-curve` ·
243
+ `lower-arch-curve` · `bulb-curve` · `skew` · `flag-curve` · `trapezoid` ·
244
+ `lower-trapezoid` · `top-trapezoid` · `horizontal-trapezoid` · `bevel` ·
245
+ `upper-roof` · `lower-roof` · `angled-projection` · `folded-corner` ·
246
+ `lateral-stretching` · `vertical-stretching` · `patchwork-by-word` ·
247
+ `step-by-word` · `arch2-by-word` · `wave-by-word` · `step-far-and-near-by-word` ·
248
+ `arch-far-and-near-by-word` · `horizontal-rotate-by-word` ·
249
+ `arbitrary-offset-rotate-by-word` · `horizontal-curved-rotate-by-word` ·
250
+ `ellipse-by-word` · `triangle-by-word` · `pentagon-by-word` ·
251
+ `rectangular-by-word` · `heart-by-word`
252
+
253
+ Register your own with `defineDeformation(name, preset)`.
254
+ </details>
255
+
256
+ ## `Text` API
257
+
258
+ For finer control, drive a `Text` instance directly:
259
+
260
+ ```ts
261
+ import { Text } from 'modern-text'
262
+
263
+ const text = new Text({ fonts, content: 'Hello', style: { fontSize: 24 } })
264
+
265
+ text.on('update', () => text.render({ view })) // re-render on any change
266
+ await text.load() // load async resources (fonts, plugin assets)
267
+ text.update() // measure + commit + emit 'update'
268
+ text.render({ view, pixelRatio: 2 })
269
+
270
+ text.boundingBox // overall box after measuring
271
+ text.characters // flat list of measured Character (inlineBox / lineBox / path)
272
+
273
+ text.dispose() // release the cached measurer / renderer
274
+ ```
275
+
276
+ - `measure()` returns a non-destructive snapshot of all boxes.
277
+ - `update()` measures and commits the result onto the instance.
278
+ - `render({ view })` updates if needed, then draws.
279
+ - Events: `update`, `measure`, `render`.
280
+
281
+ ### One-shot helpers
282
+
283
+ ```ts
284
+ import { measureText, renderText } from 'modern-text'
285
+
286
+ const result = measureText(options) // sync
287
+ const result = await measureText(options, true) // load fonts first
288
+
289
+ renderText({ view, ...options }) // sync
290
+ await renderText({ view, ...options }, true) // load fonts first
291
+ ```
292
+
293
+ ## `<text-editor>` web component
294
+
295
+ ```ts
296
+ import { TextEditor } from 'modern-text/web-components'
297
+
298
+ TextEditor.register()
299
+ ```
300
+
301
+ ```html
302
+ <text-editor></text-editor>
303
+ ```
304
+
305
+ ```ts
306
+ const editor = document.querySelector('text-editor')
307
+ editor.moveToDom(canvas) // overlay the editor on a rendered canvas
308
+ editor.set(text) // bind a Text instance — provides cursor, selection, typing
309
+ ```
310
+
311
+ ## License
312
+
313
+ [MIT](./LICENSE)
@@ -1,6 +1,6 @@
1
- import { c as DeformationPreset } from '../shared/modern-text.d-RdVkZg.cjs';
1
+ import { c as DeformationPreset } from '../shared/modern-text.BD7PBYt7.cjs';
2
2
  import 'modern-path2d';
3
- import '../shared/modern-text.C8g4NZgY.cjs';
3
+ import '../shared/modern-text.D4WopQCu.cjs';
4
4
  import 'modern-idoc';
5
5
  import 'modern-font';
6
6
 
@@ -1,6 +1,6 @@
1
- import { c as DeformationPreset } from '../shared/modern-text.w5MRHZDB.mjs';
1
+ import { c as DeformationPreset } from '../shared/modern-text.CYa4lfoG.mjs';
2
2
  import 'modern-path2d';
3
- import '../shared/modern-text.C8g4NZgY.mjs';
3
+ import '../shared/modern-text.D4WopQCu.mjs';
4
4
  import 'modern-idoc';
5
5
  import 'modern-font';
6
6
 
@@ -1,6 +1,6 @@
1
- import { c as DeformationPreset } from '../shared/modern-text.Bo2MzSls.js';
1
+ import { c as DeformationPreset } from '../shared/modern-text.BxijkspX.js';
2
2
  import 'modern-path2d';
3
- import '../shared/modern-text.C8g4NZgY.js';
3
+ import '../shared/modern-text.D4WopQCu.js';
4
4
  import 'modern-idoc';
5
5
  import 'modern-font';
6
6
 
package/dist/index.cjs CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const Text = require('./shared/modern-text.DR1hbjgA.cjs');
3
+ const Text = require('./shared/modern-text.CBgc-cQ1.cjs');
4
4
  const deformation = require('./shared/modern-text.B2xfrqDc.cjs');
5
5
  const curves = require('./shared/modern-text.MC5bIC9E.cjs');
6
6
  require('modern-idoc');
@@ -29,8 +29,9 @@ function renderText(options, load) {
29
29
 
30
30
  exports.Canvas2DRenderer = Text.Canvas2DRenderer;
31
31
  exports.Character = Text.Character;
32
+ exports.DomMeasurer = Text.DomMeasurer;
33
+ exports.FontMeasurer = Text.FontMeasurer;
32
34
  exports.Fragment = Text.Fragment;
33
- exports.Measurer = Text.Measurer;
34
35
  exports.Paragraph = Text.Paragraph;
35
36
  exports.Text = Text.Text;
36
37
  exports.backgroundPlugin = Text.backgroundPlugin;
package/dist/index.d.cts CHANGED
@@ -1,13 +1,72 @@
1
- import { h as Plugin, O as Options, b as MeasureResult, R as RenderOptions, T as Text } from './shared/modern-text.C8g4NZgY.cjs';
2
- export { C as Canvas2DRenderer, a as Character, D as DrawShapePathsOptions, F as Fragment, M as MeasureDomResult, c as MeasuredCharacter, d as MeasuredCharacterRect, e as MeasuredFragment, f as MeasuredParagraph, g as Measurer, P as Paragraph, i as TextEvents, t as textDefaultStyle } from './shared/modern-text.C8g4NZgY.cjs';
3
- import { c as DeformationPreset } from './shared/modern-text.d-RdVkZg.cjs';
4
- export { B as BendContext, a as BendPreset, C as CurvePreset, D as DeformationCharInfo, b as DeformationCurve, F as FfdContext, d as FfdPreset, O as OffsetPreset, V as VerbatimContext } from './shared/modern-text.d-RdVkZg.cjs';
5
- import { NormalizedStyle, NormalizedHighlight, NormalizedEffect } from 'modern-idoc';
6
- import { Vector2, Path2DSet, Transform2D } from 'modern-path2d';
7
- import 'modern-font';
1
+ import { i as Plugin, k as TextMeasurer, P as Paragraph, M as MeasureDomResult, a as Character, O as Options, c as MeasureResult, R as RenderOptions, T as Text } from './shared/modern-text.D4WopQCu.cjs';
2
+ export { C as Canvas2DRenderer, D as DomMeasurer, b as DrawShapePathsOptions, F as Fragment, d as MeasuredCharacter, e as MeasuredCharacterRect, f as MeasuredFragment, g as MeasuredParagraph, h as MeasurerKind, j as TextEvents, t as textDefaultStyle } from './shared/modern-text.D4WopQCu.cjs';
3
+ import { Fonts } from 'modern-font';
4
+ import { FullStyle, NormalizedStyle, NormalizedHighlight, NormalizedEffect } from 'modern-idoc';
5
+ import { BoundingBox, Vector2, Path2DSet, Transform2D } from 'modern-path2d';
6
+ import { c as DeformationPreset } from './shared/modern-text.BD7PBYt7.cjs';
7
+ export { B as BendContext, a as BendPreset, C as CurvePreset, D as DeformationCharInfo, b as DeformationCurve, F as FfdContext, d as FfdPreset, O as OffsetPreset, V as VerbatimContext } from './shared/modern-text.BD7PBYt7.cjs';
8
8
 
9
9
  declare function definePlugin(options: Plugin): Plugin;
10
10
 
11
+ /**
12
+ * Pure-JS, DOM-free text measurer — a drop-in alternative to {@link DomMeasurer}.
13
+ *
14
+ * Instead of mounting a `<section>/<ul>/<li>/<span>` tree and reading
15
+ * `getBoundingClientRect()`, it computes the same four-level boxes
16
+ * (`character.inlineBox`/`lineBox`, `fragment.inlineBox`, `paragraph.lineBox`)
17
+ * from `modern-font` glyph advances, so the downstream
18
+ * measure → glyph → plugin pipeline is unchanged and it runs in Node/SSR/Worker.
19
+ *
20
+ * ## Scope
21
+ * - `horizontal-tb` (LTR) and `vertical-rl` (columns right-to-left, glyphs top↓)
22
+ * - line breaking: `word-break: break-all` (greedy, break anywhere) + explicit `\n`
23
+ * - per-line `text-align` (start/left/center/end/right), `text-indent` (first line)
24
+ * - box model: root + paragraph `padding`/`margin` (horizontal), `letter-spacing`,
25
+ * `line-height`; block `vertical-align` (top/middle/bottom) at fixed height
26
+ *
27
+ * ## Not yet implemented (TODO)
28
+ * - UAX#14 `word-break: normal` + 避头尾/kinsoku (`line-break`)
29
+ * - BiDi, per-fragment inline `vertical-align`, borders
30
+ * - vertical: per-paragraph margin/padding and block alignment
31
+ * - kerning/ligatures (GSUB/GPOS) — fine for CJK, diverges for proportional Latin
32
+ *
33
+ * Coordinates are relative to the root border-box top-left (matching the DOM
34
+ * measurer, whose rects are taken relative to `section.getBoundingClientRect()`).
35
+ */
36
+ declare class FontMeasurer implements TextMeasurer {
37
+ measure(paragraphs: Paragraph[], rootStyle: FullStyle, _dom?: HTMLElement, fonts?: Fonts): MeasureDomResult;
38
+ protected _rootPadding(rootStyle: FullStyle): {
39
+ top: number;
40
+ right: number;
41
+ bottom: number;
42
+ left: number;
43
+ };
44
+ protected _measureHorizontal(paragraphs: Paragraph[], rootStyle: FullStyle): MeasureDomResult;
45
+ /**
46
+ * Vertical writing-mode (`vertical-rl`): columns stack right-to-left, glyphs
47
+ * flow top→bottom. It is the horizontal layout with the inline and block axes
48
+ * swapped — the inline (down-column) advance is `advanceWidth` (CJK upright = em;
49
+ * Latin is rotated, so its advance ≈ its width), the cross-axis content box is
50
+ * `advanceHeight`, and the column thickness is `fontHeight`. The lineBox is
51
+ * derived exactly as DomMeasurer.measureParagraphDom's vertical branch, so it
52
+ * stays accurate even though inlineBox carries the content-box rounding residual.
53
+ *
54
+ * v1: `vertical-rl` only; no per-paragraph margin/padding or block alignment.
55
+ */
56
+ protected _measureVertical(paragraphs: Paragraph[], rootStyle: FullStyle): MeasureDomResult;
57
+ /** Advance step including CSS letter-spacing (px). */
58
+ protected _advance(character: Character): number;
59
+ /**
60
+ * Break a paragraph's characters into visual lines.
61
+ * v1: `word-break: break-all` (break before any character that would overflow)
62
+ * plus explicit `\n`/`\r` hard breaks. The newline itself occupies no line box.
63
+ */
64
+ protected _breakLines(paragraph: Paragraph, avail: number): Character[][];
65
+ protected _unionInto(target: BoundingBox, boxes: BoundingBox[]): void;
66
+ protected _shiftAll(paragraphs: Paragraph[], dy: number): void;
67
+ dispose(): void;
68
+ }
69
+
11
70
  declare function measureText(options: Options, load: true): Promise<MeasureResult>;
12
71
  declare function measureText(options: Options): MeasureResult;
13
72
 
@@ -181,5 +240,5 @@ declare function parseColormap(colormap: 'none' | Record<string, string>): Recor
181
240
  declare function isEqualObject(obj1: Record<string, any>, obj2: Record<string, any>): boolean;
182
241
  declare function isEqualValue(val1: any, val2: any): boolean;
183
242
 
184
- export { CircleCurve, DeformationPreset, EllipseCurve, HeartCurve, MeasureResult, Options, Plugin, PolygonCurve, RectangularCurve, RenderOptions, Text, backgroundPlugin, createSvgLoader, createSvgParser, defineDeformation, definePlugin, deformationPlugin, getDeformationNames, getEffectTransform2D, getHighlightStyle, highlightPlugin, isEqualObject, isEqualValue, listStylePlugin, measureText, outlinePlugin, parseColormap, parseTransformOrigin, parseValueNumber, removeDeformation, renderPlugin, renderText, textDecorationPlugin };
243
+ export { Character, CircleCurve, DeformationPreset, EllipseCurve, FontMeasurer, HeartCurve, MeasureDomResult, MeasureResult, Options, Paragraph, Plugin, PolygonCurve, RectangularCurve, RenderOptions, Text, TextMeasurer, backgroundPlugin, createSvgLoader, createSvgParser, defineDeformation, definePlugin, deformationPlugin, getDeformationNames, getEffectTransform2D, getHighlightStyle, highlightPlugin, isEqualObject, isEqualValue, listStylePlugin, measureText, outlinePlugin, parseColormap, parseTransformOrigin, parseValueNumber, removeDeformation, renderPlugin, renderText, textDecorationPlugin };
185
244
  export type { RenderTextOptions, SvgLoader, SvgParser };
package/dist/index.d.mts CHANGED
@@ -1,13 +1,72 @@
1
- import { h as Plugin, O as Options, b as MeasureResult, R as RenderOptions, T as Text } from './shared/modern-text.C8g4NZgY.mjs';
2
- export { C as Canvas2DRenderer, a as Character, D as DrawShapePathsOptions, F as Fragment, M as MeasureDomResult, c as MeasuredCharacter, d as MeasuredCharacterRect, e as MeasuredFragment, f as MeasuredParagraph, g as Measurer, P as Paragraph, i as TextEvents, t as textDefaultStyle } from './shared/modern-text.C8g4NZgY.mjs';
3
- import { c as DeformationPreset } from './shared/modern-text.w5MRHZDB.mjs';
4
- export { B as BendContext, a as BendPreset, C as CurvePreset, D as DeformationCharInfo, b as DeformationCurve, F as FfdContext, d as FfdPreset, O as OffsetPreset, V as VerbatimContext } from './shared/modern-text.w5MRHZDB.mjs';
5
- import { NormalizedStyle, NormalizedHighlight, NormalizedEffect } from 'modern-idoc';
6
- import { Vector2, Path2DSet, Transform2D } from 'modern-path2d';
7
- import 'modern-font';
1
+ import { i as Plugin, k as TextMeasurer, P as Paragraph, M as MeasureDomResult, a as Character, O as Options, c as MeasureResult, R as RenderOptions, T as Text } from './shared/modern-text.D4WopQCu.mjs';
2
+ export { C as Canvas2DRenderer, D as DomMeasurer, b as DrawShapePathsOptions, F as Fragment, d as MeasuredCharacter, e as MeasuredCharacterRect, f as MeasuredFragment, g as MeasuredParagraph, h as MeasurerKind, j as TextEvents, t as textDefaultStyle } from './shared/modern-text.D4WopQCu.mjs';
3
+ import { Fonts } from 'modern-font';
4
+ import { FullStyle, NormalizedStyle, NormalizedHighlight, NormalizedEffect } from 'modern-idoc';
5
+ import { BoundingBox, Vector2, Path2DSet, Transform2D } from 'modern-path2d';
6
+ import { c as DeformationPreset } from './shared/modern-text.CYa4lfoG.mjs';
7
+ export { B as BendContext, a as BendPreset, C as CurvePreset, D as DeformationCharInfo, b as DeformationCurve, F as FfdContext, d as FfdPreset, O as OffsetPreset, V as VerbatimContext } from './shared/modern-text.CYa4lfoG.mjs';
8
8
 
9
9
  declare function definePlugin(options: Plugin): Plugin;
10
10
 
11
+ /**
12
+ * Pure-JS, DOM-free text measurer — a drop-in alternative to {@link DomMeasurer}.
13
+ *
14
+ * Instead of mounting a `<section>/<ul>/<li>/<span>` tree and reading
15
+ * `getBoundingClientRect()`, it computes the same four-level boxes
16
+ * (`character.inlineBox`/`lineBox`, `fragment.inlineBox`, `paragraph.lineBox`)
17
+ * from `modern-font` glyph advances, so the downstream
18
+ * measure → glyph → plugin pipeline is unchanged and it runs in Node/SSR/Worker.
19
+ *
20
+ * ## Scope
21
+ * - `horizontal-tb` (LTR) and `vertical-rl` (columns right-to-left, glyphs top↓)
22
+ * - line breaking: `word-break: break-all` (greedy, break anywhere) + explicit `\n`
23
+ * - per-line `text-align` (start/left/center/end/right), `text-indent` (first line)
24
+ * - box model: root + paragraph `padding`/`margin` (horizontal), `letter-spacing`,
25
+ * `line-height`; block `vertical-align` (top/middle/bottom) at fixed height
26
+ *
27
+ * ## Not yet implemented (TODO)
28
+ * - UAX#14 `word-break: normal` + 避头尾/kinsoku (`line-break`)
29
+ * - BiDi, per-fragment inline `vertical-align`, borders
30
+ * - vertical: per-paragraph margin/padding and block alignment
31
+ * - kerning/ligatures (GSUB/GPOS) — fine for CJK, diverges for proportional Latin
32
+ *
33
+ * Coordinates are relative to the root border-box top-left (matching the DOM
34
+ * measurer, whose rects are taken relative to `section.getBoundingClientRect()`).
35
+ */
36
+ declare class FontMeasurer implements TextMeasurer {
37
+ measure(paragraphs: Paragraph[], rootStyle: FullStyle, _dom?: HTMLElement, fonts?: Fonts): MeasureDomResult;
38
+ protected _rootPadding(rootStyle: FullStyle): {
39
+ top: number;
40
+ right: number;
41
+ bottom: number;
42
+ left: number;
43
+ };
44
+ protected _measureHorizontal(paragraphs: Paragraph[], rootStyle: FullStyle): MeasureDomResult;
45
+ /**
46
+ * Vertical writing-mode (`vertical-rl`): columns stack right-to-left, glyphs
47
+ * flow top→bottom. It is the horizontal layout with the inline and block axes
48
+ * swapped — the inline (down-column) advance is `advanceWidth` (CJK upright = em;
49
+ * Latin is rotated, so its advance ≈ its width), the cross-axis content box is
50
+ * `advanceHeight`, and the column thickness is `fontHeight`. The lineBox is
51
+ * derived exactly as DomMeasurer.measureParagraphDom's vertical branch, so it
52
+ * stays accurate even though inlineBox carries the content-box rounding residual.
53
+ *
54
+ * v1: `vertical-rl` only; no per-paragraph margin/padding or block alignment.
55
+ */
56
+ protected _measureVertical(paragraphs: Paragraph[], rootStyle: FullStyle): MeasureDomResult;
57
+ /** Advance step including CSS letter-spacing (px). */
58
+ protected _advance(character: Character): number;
59
+ /**
60
+ * Break a paragraph's characters into visual lines.
61
+ * v1: `word-break: break-all` (break before any character that would overflow)
62
+ * plus explicit `\n`/`\r` hard breaks. The newline itself occupies no line box.
63
+ */
64
+ protected _breakLines(paragraph: Paragraph, avail: number): Character[][];
65
+ protected _unionInto(target: BoundingBox, boxes: BoundingBox[]): void;
66
+ protected _shiftAll(paragraphs: Paragraph[], dy: number): void;
67
+ dispose(): void;
68
+ }
69
+
11
70
  declare function measureText(options: Options, load: true): Promise<MeasureResult>;
12
71
  declare function measureText(options: Options): MeasureResult;
13
72
 
@@ -181,5 +240,5 @@ declare function parseColormap(colormap: 'none' | Record<string, string>): Recor
181
240
  declare function isEqualObject(obj1: Record<string, any>, obj2: Record<string, any>): boolean;
182
241
  declare function isEqualValue(val1: any, val2: any): boolean;
183
242
 
184
- export { CircleCurve, DeformationPreset, EllipseCurve, HeartCurve, MeasureResult, Options, Plugin, PolygonCurve, RectangularCurve, RenderOptions, Text, backgroundPlugin, createSvgLoader, createSvgParser, defineDeformation, definePlugin, deformationPlugin, getDeformationNames, getEffectTransform2D, getHighlightStyle, highlightPlugin, isEqualObject, isEqualValue, listStylePlugin, measureText, outlinePlugin, parseColormap, parseTransformOrigin, parseValueNumber, removeDeformation, renderPlugin, renderText, textDecorationPlugin };
243
+ export { Character, CircleCurve, DeformationPreset, EllipseCurve, FontMeasurer, HeartCurve, MeasureDomResult, MeasureResult, Options, Paragraph, Plugin, PolygonCurve, RectangularCurve, RenderOptions, Text, TextMeasurer, backgroundPlugin, createSvgLoader, createSvgParser, defineDeformation, definePlugin, deformationPlugin, getDeformationNames, getEffectTransform2D, getHighlightStyle, highlightPlugin, isEqualObject, isEqualValue, listStylePlugin, measureText, outlinePlugin, parseColormap, parseTransformOrigin, parseValueNumber, removeDeformation, renderPlugin, renderText, textDecorationPlugin };
185
244
  export type { RenderTextOptions, SvgLoader, SvgParser };
package/dist/index.d.ts CHANGED
@@ -1,13 +1,72 @@
1
- import { h as Plugin, O as Options, b as MeasureResult, R as RenderOptions, T as Text } from './shared/modern-text.C8g4NZgY.js';
2
- export { C as Canvas2DRenderer, a as Character, D as DrawShapePathsOptions, F as Fragment, M as MeasureDomResult, c as MeasuredCharacter, d as MeasuredCharacterRect, e as MeasuredFragment, f as MeasuredParagraph, g as Measurer, P as Paragraph, i as TextEvents, t as textDefaultStyle } from './shared/modern-text.C8g4NZgY.js';
3
- import { c as DeformationPreset } from './shared/modern-text.Bo2MzSls.js';
4
- export { B as BendContext, a as BendPreset, C as CurvePreset, D as DeformationCharInfo, b as DeformationCurve, F as FfdContext, d as FfdPreset, O as OffsetPreset, V as VerbatimContext } from './shared/modern-text.Bo2MzSls.js';
5
- import { NormalizedStyle, NormalizedHighlight, NormalizedEffect } from 'modern-idoc';
6
- import { Vector2, Path2DSet, Transform2D } from 'modern-path2d';
7
- import 'modern-font';
1
+ import { i as Plugin, k as TextMeasurer, P as Paragraph, M as MeasureDomResult, a as Character, O as Options, c as MeasureResult, R as RenderOptions, T as Text } from './shared/modern-text.D4WopQCu.js';
2
+ export { C as Canvas2DRenderer, D as DomMeasurer, b as DrawShapePathsOptions, F as Fragment, d as MeasuredCharacter, e as MeasuredCharacterRect, f as MeasuredFragment, g as MeasuredParagraph, h as MeasurerKind, j as TextEvents, t as textDefaultStyle } from './shared/modern-text.D4WopQCu.js';
3
+ import { Fonts } from 'modern-font';
4
+ import { FullStyle, NormalizedStyle, NormalizedHighlight, NormalizedEffect } from 'modern-idoc';
5
+ import { BoundingBox, Vector2, Path2DSet, Transform2D } from 'modern-path2d';
6
+ import { c as DeformationPreset } from './shared/modern-text.BxijkspX.js';
7
+ export { B as BendContext, a as BendPreset, C as CurvePreset, D as DeformationCharInfo, b as DeformationCurve, F as FfdContext, d as FfdPreset, O as OffsetPreset, V as VerbatimContext } from './shared/modern-text.BxijkspX.js';
8
8
 
9
9
  declare function definePlugin(options: Plugin): Plugin;
10
10
 
11
+ /**
12
+ * Pure-JS, DOM-free text measurer — a drop-in alternative to {@link DomMeasurer}.
13
+ *
14
+ * Instead of mounting a `<section>/<ul>/<li>/<span>` tree and reading
15
+ * `getBoundingClientRect()`, it computes the same four-level boxes
16
+ * (`character.inlineBox`/`lineBox`, `fragment.inlineBox`, `paragraph.lineBox`)
17
+ * from `modern-font` glyph advances, so the downstream
18
+ * measure → glyph → plugin pipeline is unchanged and it runs in Node/SSR/Worker.
19
+ *
20
+ * ## Scope
21
+ * - `horizontal-tb` (LTR) and `vertical-rl` (columns right-to-left, glyphs top↓)
22
+ * - line breaking: `word-break: break-all` (greedy, break anywhere) + explicit `\n`
23
+ * - per-line `text-align` (start/left/center/end/right), `text-indent` (first line)
24
+ * - box model: root + paragraph `padding`/`margin` (horizontal), `letter-spacing`,
25
+ * `line-height`; block `vertical-align` (top/middle/bottom) at fixed height
26
+ *
27
+ * ## Not yet implemented (TODO)
28
+ * - UAX#14 `word-break: normal` + 避头尾/kinsoku (`line-break`)
29
+ * - BiDi, per-fragment inline `vertical-align`, borders
30
+ * - vertical: per-paragraph margin/padding and block alignment
31
+ * - kerning/ligatures (GSUB/GPOS) — fine for CJK, diverges for proportional Latin
32
+ *
33
+ * Coordinates are relative to the root border-box top-left (matching the DOM
34
+ * measurer, whose rects are taken relative to `section.getBoundingClientRect()`).
35
+ */
36
+ declare class FontMeasurer implements TextMeasurer {
37
+ measure(paragraphs: Paragraph[], rootStyle: FullStyle, _dom?: HTMLElement, fonts?: Fonts): MeasureDomResult;
38
+ protected _rootPadding(rootStyle: FullStyle): {
39
+ top: number;
40
+ right: number;
41
+ bottom: number;
42
+ left: number;
43
+ };
44
+ protected _measureHorizontal(paragraphs: Paragraph[], rootStyle: FullStyle): MeasureDomResult;
45
+ /**
46
+ * Vertical writing-mode (`vertical-rl`): columns stack right-to-left, glyphs
47
+ * flow top→bottom. It is the horizontal layout with the inline and block axes
48
+ * swapped — the inline (down-column) advance is `advanceWidth` (CJK upright = em;
49
+ * Latin is rotated, so its advance ≈ its width), the cross-axis content box is
50
+ * `advanceHeight`, and the column thickness is `fontHeight`. The lineBox is
51
+ * derived exactly as DomMeasurer.measureParagraphDom's vertical branch, so it
52
+ * stays accurate even though inlineBox carries the content-box rounding residual.
53
+ *
54
+ * v1: `vertical-rl` only; no per-paragraph margin/padding or block alignment.
55
+ */
56
+ protected _measureVertical(paragraphs: Paragraph[], rootStyle: FullStyle): MeasureDomResult;
57
+ /** Advance step including CSS letter-spacing (px). */
58
+ protected _advance(character: Character): number;
59
+ /**
60
+ * Break a paragraph's characters into visual lines.
61
+ * v1: `word-break: break-all` (break before any character that would overflow)
62
+ * plus explicit `\n`/`\r` hard breaks. The newline itself occupies no line box.
63
+ */
64
+ protected _breakLines(paragraph: Paragraph, avail: number): Character[][];
65
+ protected _unionInto(target: BoundingBox, boxes: BoundingBox[]): void;
66
+ protected _shiftAll(paragraphs: Paragraph[], dy: number): void;
67
+ dispose(): void;
68
+ }
69
+
11
70
  declare function measureText(options: Options, load: true): Promise<MeasureResult>;
12
71
  declare function measureText(options: Options): MeasureResult;
13
72
 
@@ -181,5 +240,5 @@ declare function parseColormap(colormap: 'none' | Record<string, string>): Recor
181
240
  declare function isEqualObject(obj1: Record<string, any>, obj2: Record<string, any>): boolean;
182
241
  declare function isEqualValue(val1: any, val2: any): boolean;
183
242
 
184
- export { CircleCurve, DeformationPreset, EllipseCurve, HeartCurve, MeasureResult, Options, Plugin, PolygonCurve, RectangularCurve, RenderOptions, Text, backgroundPlugin, createSvgLoader, createSvgParser, defineDeformation, definePlugin, deformationPlugin, getDeformationNames, getEffectTransform2D, getHighlightStyle, highlightPlugin, isEqualObject, isEqualValue, listStylePlugin, measureText, outlinePlugin, parseColormap, parseTransformOrigin, parseValueNumber, removeDeformation, renderPlugin, renderText, textDecorationPlugin };
243
+ export { Character, CircleCurve, DeformationPreset, EllipseCurve, FontMeasurer, HeartCurve, MeasureDomResult, MeasureResult, Options, Paragraph, Plugin, PolygonCurve, RectangularCurve, RenderOptions, Text, TextMeasurer, backgroundPlugin, createSvgLoader, createSvgParser, defineDeformation, definePlugin, deformationPlugin, getDeformationNames, getEffectTransform2D, getHighlightStyle, highlightPlugin, isEqualObject, isEqualValue, listStylePlugin, measureText, outlinePlugin, parseColormap, parseTransformOrigin, parseValueNumber, removeDeformation, renderPlugin, renderText, textDecorationPlugin };
185
244
  export type { RenderTextOptions, SvgLoader, SvgParser };