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
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)