@weasel-js/text 1.2.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/LICENSE +21 -0
- package/README.md +44 -0
- package/dist/chunk-4OGXTESY.js +531 -0
- package/dist/chunk-4OGXTESY.js.map +1 -0
- package/dist/index.d.ts +189 -0
- package/dist/index.js +436 -0
- package/dist/index.js.map +1 -0
- package/dist/test-seams-nUU2u_Pt.d.ts +589 -0
- package/dist/test-seams.d.ts +2 -0
- package/dist/test-seams.js +3 -0
- package/dist/test-seams.js.map +1 -0
- package/package.json +53 -0
|
@@ -0,0 +1,589 @@
|
|
|
1
|
+
import { FillStyle, Stroke } from '@weasel-js/paint';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Canonical inline-styling primitive for text nodes. A node's text is
|
|
5
|
+
* either a plain `string` (treated as a single-run, default-styled fragment)
|
|
6
|
+
* or `StyledRun[]` for rich content. `toRuns` is the funnel that normalizes
|
|
7
|
+
* either form into the array shape used by the renderer.
|
|
8
|
+
*
|
|
9
|
+
* Every field except `text` is optional; missing fields fall back to the
|
|
10
|
+
* node-level `TextStyle`. `bold`/`italic` are toggles; richer weight axes
|
|
11
|
+
* (300/500/900) are out of scope for slice 1.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** A span of text with its own styling, as authored. Fields left absent
|
|
15
|
+
* inherit from the node's text style — this is the difference between a run
|
|
16
|
+
* and a fully resolved one. */
|
|
17
|
+
interface StyledRun {
|
|
18
|
+
text: string;
|
|
19
|
+
bold?: boolean;
|
|
20
|
+
italic?: boolean;
|
|
21
|
+
fontFamily?: string;
|
|
22
|
+
fontSize?: number;
|
|
23
|
+
fill?: FillStyle;
|
|
24
|
+
/** Outline over this run's glyphs. Overrides the node style's stroke;
|
|
25
|
+
* absent inherits it. Only painted on the outline tier — see
|
|
26
|
+
* {@link TextStyle.stroke}. */
|
|
27
|
+
stroke?: Stroke;
|
|
28
|
+
letterSpacing?: number;
|
|
29
|
+
underline?: boolean;
|
|
30
|
+
strikethrough?: boolean;
|
|
31
|
+
/** Draw a rule above this run's ascent. Additive over the node style. */
|
|
32
|
+
overline?: boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Set this run as a superscript or subscript — a raised or lowered baseline
|
|
35
|
+
* and a smaller size together, which is the pair `<sup>` and `<sub>` imply.
|
|
36
|
+
*
|
|
37
|
+
* A preset over the two primitives below, not a third mechanism: it supplies
|
|
38
|
+
* a `baselineShift` and a `fontScale`, and naming either of those directly
|
|
39
|
+
* overrides that half while leaving the other alone. The numbers are in
|
|
40
|
+
* {@link SCRIPT_METRICS}.
|
|
41
|
+
*
|
|
42
|
+
* There is no node-level counterpart. A whole text node set as a superscript
|
|
43
|
+
* is a smaller node moved up, which the pose already says better.
|
|
44
|
+
*/
|
|
45
|
+
script?: 'super' | 'sub';
|
|
46
|
+
/**
|
|
47
|
+
* Raise (positive) or lower (negative) this run off the line's shared
|
|
48
|
+
* baseline, in ems of the *inherited* font size — so a run's rise does not
|
|
49
|
+
* shrink along with the run when `fontScale` also applies.
|
|
50
|
+
*/
|
|
51
|
+
baselineShift?: number;
|
|
52
|
+
/**
|
|
53
|
+
* Multiplier on the inherited font size. The relative counterpart to
|
|
54
|
+
* `fontSize`, which pins an absolute size and wins over this when both are
|
|
55
|
+
* present.
|
|
56
|
+
*/
|
|
57
|
+
fontScale?: number;
|
|
58
|
+
}
|
|
59
|
+
/** Normalize the two accepted spellings of text content — a plain string or
|
|
60
|
+
* an array of runs — to runs. Throws on a run with no string `text`. */
|
|
61
|
+
declare function toRuns(input: string | StyledRun[]): StyledRun[];
|
|
62
|
+
/** Concatenate runs, dropping all styling. */
|
|
63
|
+
declare function runsToPlainText(runs: readonly StyledRun[]): string;
|
|
64
|
+
/** A run flag an inline marker can toggle. The additive style toggles on
|
|
65
|
+
* `StyledRun`; the valued fields (family, size, paint, `script`) have no
|
|
66
|
+
* inline spelling and are not addressable this way. */
|
|
67
|
+
type RunFlag = 'bold' | 'italic' | 'underline' | 'strikethrough' | 'overline';
|
|
68
|
+
/** One inline marker: a delimiter repeated `repeat` times, and the flags it
|
|
69
|
+
* turns on between its opening and closing occurrence. */
|
|
70
|
+
interface RunMarker {
|
|
71
|
+
delimiter: string;
|
|
72
|
+
repeat: number;
|
|
73
|
+
flags: readonly RunFlag[];
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* The inline grammar `runsToMarkdown` writes and `markdownToRuns` reads.
|
|
77
|
+
*
|
|
78
|
+
* Markers sharing a delimiter are matched longest-first, which is what lets
|
|
79
|
+
* `***` mean something other than `**` followed by `*`. A backslash escapes
|
|
80
|
+
* any delimiter character and itself, in both directions.
|
|
81
|
+
*/
|
|
82
|
+
interface RunGrammar {
|
|
83
|
+
markers: readonly RunMarker[];
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* The markdown subset the kit reads and writes: `**bold**`, `*italic*`,
|
|
87
|
+
* `***both***`.
|
|
88
|
+
*
|
|
89
|
+
* Deliberately not the whole of markdown, and deliberately silent on
|
|
90
|
+
* `underline` / `strikethrough` — the two run flags with no entry here are
|
|
91
|
+
* dropped by `runsToMarkdown`, as they always have been. A grammar that wants
|
|
92
|
+
* `~~struck~~` adds one marker; nothing else has to change.
|
|
93
|
+
*/
|
|
94
|
+
declare const MARKDOWN_RUN_GRAMMAR: RunGrammar;
|
|
95
|
+
/**
|
|
96
|
+
* Render runs in `grammar`, defaulting to the markdown subset. The flavor
|
|
97
|
+
* written to the clipboard alongside the plain-text one.
|
|
98
|
+
*
|
|
99
|
+
* A run whose flags exactly match one marker takes it; otherwise the markers
|
|
100
|
+
* that cover them nest. Flags no marker spells are dropped — the text still
|
|
101
|
+
* round-trips, without that styling.
|
|
102
|
+
*/
|
|
103
|
+
declare function runsToMarkdown(runs: readonly StyledRun[], grammar?: RunGrammar): string;
|
|
104
|
+
/**
|
|
105
|
+
* Parse `input` in `grammar`, defaulting to the markdown subset, into styled
|
|
106
|
+
* runs. Newlines are preserved as literal characters inside a run — they are
|
|
107
|
+
* not run-boundary markers in this format.
|
|
108
|
+
*/
|
|
109
|
+
declare function markdownToRuns(input: string, grammar?: RunGrammar): StyledRun[];
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Typography for `TextPose` and friends. Every field is optional; consumers
|
|
113
|
+
* pass `{}` or override the few they care about. Defaults live in
|
|
114
|
+
* `DEFAULT_TEXT_STYLE` and are applied at render/measure time, never written
|
|
115
|
+
* back to the pose.
|
|
116
|
+
*
|
|
117
|
+
* Paint is not typography and does not live here. A text node carries its
|
|
118
|
+
* fill and stroke in `data.fill` / `data.stroke`, the slots every other node
|
|
119
|
+
* kind uses; `resolveTextStyle` takes them as its second argument and
|
|
120
|
+
* `StyledRun.fill` / `.stroke` override them per range.
|
|
121
|
+
*/
|
|
122
|
+
|
|
123
|
+
/** User-facing text style. All fields optional; defaults applied at render time via `resolveTextStyle`. */
|
|
124
|
+
interface TextStyle {
|
|
125
|
+
/** Font size in world units. Default 16. */
|
|
126
|
+
fontSize?: number;
|
|
127
|
+
/** Default `'sans-serif'`. */
|
|
128
|
+
fontFamily?: string;
|
|
129
|
+
/** Default 400. */
|
|
130
|
+
fontWeight?: number | string;
|
|
131
|
+
/** Default `'normal'`. */
|
|
132
|
+
fontStyle?: 'normal' | 'italic';
|
|
133
|
+
/** Default `'left'`. */
|
|
134
|
+
align?: 'left' | 'center' | 'right';
|
|
135
|
+
/** Multiplier applied to `fontSize`. Default 1.2. */
|
|
136
|
+
lineHeight?: number;
|
|
137
|
+
/**
|
|
138
|
+
* Caret color used by the edit overlay. Defaults to the node's fill when
|
|
139
|
+
* that fill is solid; falls back to `#000` for non-solid paints.
|
|
140
|
+
*/
|
|
141
|
+
caretColor?: string;
|
|
142
|
+
/**
|
|
143
|
+
* Selection background color used by the edit overlay's `::selection`
|
|
144
|
+
* pseudo-element. Defaults to a 25%-opacity tint of `caretColor` via CSS
|
|
145
|
+
* `color-mix`. Pass `'none'` to fall back to the browser-native highlight.
|
|
146
|
+
*/
|
|
147
|
+
selectionBackground?: string;
|
|
148
|
+
/** Selection text color paired with `selectionBackground`. Default: inherits text color. */
|
|
149
|
+
selectionColor?: string;
|
|
150
|
+
/** Extra advance added after each glyph, in world units. Default 0. */
|
|
151
|
+
letterSpacing?: number;
|
|
152
|
+
/** Default `false`. */
|
|
153
|
+
underline?: boolean;
|
|
154
|
+
/** Default `false`. */
|
|
155
|
+
strikethrough?: boolean;
|
|
156
|
+
/** Default `false`. */
|
|
157
|
+
overline?: boolean;
|
|
158
|
+
}
|
|
159
|
+
/** `TextStyle` with all fields filled in from defaults — what the renderer actually consumes. */
|
|
160
|
+
interface ResolvedTextStyle {
|
|
161
|
+
fontSize: number;
|
|
162
|
+
fontFamily: string;
|
|
163
|
+
fontWeight: number | string;
|
|
164
|
+
fontStyle: 'normal' | 'italic';
|
|
165
|
+
align: 'left' | 'center' | 'right';
|
|
166
|
+
lineHeight: number;
|
|
167
|
+
fill: FillStyle;
|
|
168
|
+
caretColor: string;
|
|
169
|
+
selectionBackground: string | null;
|
|
170
|
+
selectionColor: string | null;
|
|
171
|
+
letterSpacing: number;
|
|
172
|
+
underline: boolean;
|
|
173
|
+
strikethrough: boolean;
|
|
174
|
+
overline: boolean;
|
|
175
|
+
/** Absent means no outline — unlike the other fields, this one has no
|
|
176
|
+
* default to fall back to. See {@link TextPaint.stroke}. */
|
|
177
|
+
stroke?: Stroke;
|
|
178
|
+
}
|
|
179
|
+
/** Default resolved style used when a `TextPose` omits `style`. */
|
|
180
|
+
declare const DEFAULT_TEXT_STYLE: ResolvedTextStyle;
|
|
181
|
+
/**
|
|
182
|
+
* The paint a text node hands its glyphs — `data.fill` and `data.stroke`,
|
|
183
|
+
* read straight off the node. Runs inherit these when they name none of
|
|
184
|
+
* their own.
|
|
185
|
+
*
|
|
186
|
+
* `fill: null` is not yet distinguishable from absent: a `ResolvedRun` must
|
|
187
|
+
* name a concrete fill, so unfilled-but-stroked text has nowhere to say so
|
|
188
|
+
* and falls back to the default black. See `docs/TODO.md`.
|
|
189
|
+
*/
|
|
190
|
+
interface TextPaint {
|
|
191
|
+
fill?: FillStyle | null;
|
|
192
|
+
/**
|
|
193
|
+
* Outline painted over the glyph fill. Absent means no outline — there is
|
|
194
|
+
* no such thing as a default text stroke.
|
|
195
|
+
*
|
|
196
|
+
* Only glyphs on the outline tier are stroked: above
|
|
197
|
+
* `textOutlineMinScreenSize` a glyph is a real `PolygonPath`, so it gets
|
|
198
|
+
* the ordinary tessellated ribbon with real joins, caps and miters, in any
|
|
199
|
+
* paint. Below it a glyph is a sampled distance field with no geometry to
|
|
200
|
+
* stroke, and it renders unstroked rather than approximated. `width` is in
|
|
201
|
+
* world units, like every other stroke in the kit — it does not scale with
|
|
202
|
+
* `fontSize`.
|
|
203
|
+
*/
|
|
204
|
+
stroke?: Stroke | null;
|
|
205
|
+
}
|
|
206
|
+
/** Fill in a partial `TextStyle` with defaults from `DEFAULT_TEXT_STYLE`,
|
|
207
|
+
* taking the glyph paint from the node rather than from the style. */
|
|
208
|
+
declare function resolveTextStyle(style?: TextStyle, paint?: TextPaint): ResolvedTextStyle;
|
|
209
|
+
/** Build a CSS `font` shorthand suitable for `ctx.font`. */
|
|
210
|
+
declare function fontString(s: ResolvedTextStyle): string;
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Apply node-level `ResolvedTextStyle` defaults to each `StyledRun`,
|
|
214
|
+
* producing a fully-resolved run with every styling field set. Downstream
|
|
215
|
+
* layout and draw never re-resolve defaults — `ResolvedRun` is the
|
|
216
|
+
* canonical shape the renderer consumes.
|
|
217
|
+
*
|
|
218
|
+
* `bold`/`italic` toggles on a run are folded into `fontWeight`/`fontStyle`:
|
|
219
|
+
* `bold: true` → fontWeight 700, `italic: true` → fontStyle 'italic'.
|
|
220
|
+
* Explicit `fontFamily` / `fontSize` / `fill` / `letterSpacing` on the run
|
|
221
|
+
* override the node-level value (`letterSpacing: 0` on a run is an override,
|
|
222
|
+
* not an absence — it zeroes inherited tracking).
|
|
223
|
+
*
|
|
224
|
+
* `underline` / `strikethrough` / `overline` are *additive*, like
|
|
225
|
+
* `bold`/`italic`: a run can turn a decoration on but never off, so they
|
|
226
|
+
* resolve as `run.x || style.x` and not `run.x ?? style.x`. See the header of
|
|
227
|
+
* `runs/rangeStyle.ts` for why the model collapses the tri-state.
|
|
228
|
+
*
|
|
229
|
+
* `script` is folded the same way the toggles are, into the two primitives it
|
|
230
|
+
* is a preset over: `baselineShift` and `fontScale`. Both come out as one
|
|
231
|
+
* world-unit `baselineShift` and a final `fontSize`, so layout never learns
|
|
232
|
+
* that superscripts exist — it places a run against a baseline and an offset.
|
|
233
|
+
*/
|
|
234
|
+
|
|
235
|
+
/** A run with every style resolved against the node's text style — no
|
|
236
|
+
* optional inheritance left. This is what layout and painting consume. */
|
|
237
|
+
interface ResolvedRun {
|
|
238
|
+
text: string;
|
|
239
|
+
fontFamily: string;
|
|
240
|
+
fontSize: number;
|
|
241
|
+
fontWeight: number;
|
|
242
|
+
fontStyle: 'normal' | 'italic';
|
|
243
|
+
fill: FillStyle;
|
|
244
|
+
/** Outline over this run's glyphs, or absent for none. Painted only on the
|
|
245
|
+
* outline tier — a distance field has no geometry to stroke. */
|
|
246
|
+
stroke?: Stroke;
|
|
247
|
+
/** Extra advance added after each glyph of this run, in world units. */
|
|
248
|
+
letterSpacing: number;
|
|
249
|
+
/** Draw a rule below this run's baseline. Additive over the node style. */
|
|
250
|
+
underline: boolean;
|
|
251
|
+
/** Draw a rule through this run's x-height. Additive over the node style. */
|
|
252
|
+
strikethrough: boolean;
|
|
253
|
+
/** Draw a rule above this run's ascent. Additive over the node style. */
|
|
254
|
+
overline: boolean;
|
|
255
|
+
/**
|
|
256
|
+
* How far this run sits off the line's shared baseline, in world units;
|
|
257
|
+
* positive raises. 0 for ordinary text.
|
|
258
|
+
*
|
|
259
|
+
* Already multiplied out against the inherited font size, and already
|
|
260
|
+
* carrying whatever `script` asked for — layout adds it to a baseline and
|
|
261
|
+
* asks nothing about where it came from.
|
|
262
|
+
*/
|
|
263
|
+
baselineShift: number;
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* What `script: 'super'` and `script: 'sub'` expand to, as fractions of the
|
|
267
|
+
* inherited font size: `size` scales it, `shift` raises (positive) or lowers
|
|
268
|
+
* (negative) the baseline.
|
|
269
|
+
*
|
|
270
|
+
* These are Adobe's defaults — InDesign and Illustrator ship 58.3% size and
|
|
271
|
+
* 33.3% position for both — chosen because a drawing tool's users have those
|
|
272
|
+
* numbers in their fingers already. They are not read from the font: the
|
|
273
|
+
* `OS/2` table carries real `ySuperscript*` / `ySubscript*` metrics, but the
|
|
274
|
+
* baked atlas tier has no slot for them (see `layoutRuns`'s note on the
|
|
275
|
+
* decoration constants, which are derived for the same reason), and metrics
|
|
276
|
+
* that applied on one glyph tier and not the other would reflow text as it
|
|
277
|
+
* crossed the size threshold.
|
|
278
|
+
*
|
|
279
|
+
* Exported so a consumer building a character panel can show the percentages
|
|
280
|
+
* it is about to apply. Override either half per run with `baselineShift` /
|
|
281
|
+
* `fontScale`.
|
|
282
|
+
*/
|
|
283
|
+
declare const SCRIPT_METRICS: Readonly<Record<'super' | 'sub', Readonly<{
|
|
284
|
+
size: number;
|
|
285
|
+
shift: number;
|
|
286
|
+
}>>>;
|
|
287
|
+
/** Resolve each run's styling against the node's text style, filling in
|
|
288
|
+
* everything the run left inherited. */
|
|
289
|
+
declare function resolveRuns(runs: readonly StyledRun[], style: ResolvedTextStyle): ResolvedRun[];
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Runs-aware MSDF layout. Walks `ResolvedRun[]` codepoint-by-codepoint,
|
|
293
|
+
* switching atlas per run via `resolveFontVariant`, applying kerning
|
|
294
|
+
* (using the left glyph's atlas table and size scaling, including across
|
|
295
|
+
* run boundaries), and bucketing emitted quads by
|
|
296
|
+
* `(family, resolvedWeight, resolvedStyle, syntheticBold, syntheticItalic, fillKey)`
|
|
297
|
+
* so the renderer issues one draw call per atlas+color group.
|
|
298
|
+
*
|
|
299
|
+
* A run's `letterSpacing` (world units, so it does not scale with `fontSize`)
|
|
300
|
+
* is added to the advance *after* every character of that run, including the
|
|
301
|
+
* last one on a line — the CSS `letter-spacing` rule, chosen so a DOM overlay
|
|
302
|
+
* rendering the same text can be made to agree. Trailing tracking therefore
|
|
303
|
+
* widens the measured line width and counts toward wrapping. Spaces are
|
|
304
|
+
* tracked like any other character; a newline is not (it consumes no advance).
|
|
305
|
+
* One caveat against CSS: tracking is applied per *code point*, not per
|
|
306
|
+
* grapheme cluster, so `e` + U+0301 takes tracking twice where CSS would
|
|
307
|
+
* space the cluster once.
|
|
308
|
+
*
|
|
309
|
+
* Word wrap is applied when `maxWidth` is finite: words are committed to
|
|
310
|
+
* a new line when they would exceed the current line width. Forced line
|
|
311
|
+
* breaks are emitted for `\n` codepoints. Every run on a line shares one
|
|
312
|
+
* baseline, sunk to clear the tallest run's ascent, so mixing sizes or faces
|
|
313
|
+
* aligns them the way inline text aligns everywhere else; line height is
|
|
314
|
+
* `max(fontSize * lineHeight)` across the line.
|
|
315
|
+
*
|
|
316
|
+
* A run may also sit off that shared baseline: `ResolvedRun.baselineShift`
|
|
317
|
+
* displaces it, which is what `script: 'super' | 'sub'` resolves to. The shift
|
|
318
|
+
* moves the run's glyphs, its outline geometry and its own decoration rules
|
|
319
|
+
* together, and deliberately does not feed back into the line's baseline or
|
|
320
|
+
* its height — a superscript rides on the line rather than reflowing it.
|
|
321
|
+
*
|
|
322
|
+
* Underline, strikethrough and overline come out on a second channel,
|
|
323
|
+
* `decorations` —
|
|
324
|
+
* solid rectangles, not textured glyphs, so they cannot ride in a group's
|
|
325
|
+
* `quads` (which upload UVs into an MSDF program). They are accumulated
|
|
326
|
+
* during the same per-line pen walk that emits quads, *not* reconstructed
|
|
327
|
+
* from quad extents afterwards: quads exist only for glyphs with ink, so a
|
|
328
|
+
* decorated span's spaces would punch holes in a rule derived from them.
|
|
329
|
+
*
|
|
330
|
+
* ### Coordinates are origin-relative
|
|
331
|
+
*
|
|
332
|
+
* Every coordinate out of here is measured from the text's own top-left, not
|
|
333
|
+
* from anywhere on the page: the caller translates. That is what lets a text
|
|
334
|
+
* node be dragged without re-laying out — `layoutCache` would otherwise need
|
|
335
|
+
* the position in its key and miss on every frame of the drag — and it costs
|
|
336
|
+
* the caller one addition per vertex, inside loops already walking every one.
|
|
337
|
+
* Alignment, wrapping, tracking and decoration placement all read widths and
|
|
338
|
+
* pen deltas, never an absolute coordinate, so the translation is exact.
|
|
339
|
+
*/
|
|
340
|
+
|
|
341
|
+
/** One textured glyph quad, origin-relative — see the header. */
|
|
342
|
+
interface LaidOutQuad {
|
|
343
|
+
x0: number;
|
|
344
|
+
y0: number;
|
|
345
|
+
x1: number;
|
|
346
|
+
y1: number;
|
|
347
|
+
u0: number;
|
|
348
|
+
v0: number;
|
|
349
|
+
u1: number;
|
|
350
|
+
v1: number;
|
|
351
|
+
/** Y coordinate of the line's baseline (penY at quad emission). Used by the
|
|
352
|
+
* synthetic-italic vertex skew so above-baseline vertices lean right. */
|
|
353
|
+
baselineY: number;
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* One glyph the renderer should tessellate rather than sample: real font
|
|
357
|
+
* outline geometry, for text large enough that a distance field shows the
|
|
358
|
+
* raster it was reconstructed from.
|
|
359
|
+
*
|
|
360
|
+
* `d` is em-space SVG path data straight from `@weasel-js/font`'s outline
|
|
361
|
+
* registry — 1 unit is 1 em, y grows down, origin on the baseline at the pen.
|
|
362
|
+
* Placing it is therefore a uniform scale by `scale` (world units per em)
|
|
363
|
+
* followed by a translate to `(x, baselineY)`, and nothing here depends on
|
|
364
|
+
* zoom: the same glyph at the same size in two places is the same geometry
|
|
365
|
+
* twice, which is what lets the renderer tessellate it once and cache it.
|
|
366
|
+
*
|
|
367
|
+
* `key` identifies that cached tessellation — face identity plus codepoint,
|
|
368
|
+
* assembled here because layout is where both are in hand.
|
|
369
|
+
*/
|
|
370
|
+
interface LaidOutOutlineGlyph {
|
|
371
|
+
d: string;
|
|
372
|
+
key: string;
|
|
373
|
+
/** Pen position: where em-space x = 0 lands, in world units. */
|
|
374
|
+
x: number;
|
|
375
|
+
/** The line's baseline: where em-space y = 0 lands, in world units. */
|
|
376
|
+
baselineY: number;
|
|
377
|
+
/** World units per em — the run's `fontSize`, since em space is unit-scale. */
|
|
378
|
+
scale: number;
|
|
379
|
+
}
|
|
380
|
+
interface LaidOutGroup {
|
|
381
|
+
/** Resolved atlas family — may differ from the requested family when the
|
|
382
|
+
* cross-family fallback policy substituted a default. This is what the
|
|
383
|
+
* renderer looks the atlas up by, so it must be the one that resolves. */
|
|
384
|
+
family: string;
|
|
385
|
+
/** Resolved variant — matches the registered atlas and the texture-cache key. */
|
|
386
|
+
weight: number;
|
|
387
|
+
style: 'normal' | 'italic';
|
|
388
|
+
/** Gap between the request and the resolved match. Drives shader uniforms. */
|
|
389
|
+
synthetic: {
|
|
390
|
+
bold: boolean;
|
|
391
|
+
italic: boolean;
|
|
392
|
+
};
|
|
393
|
+
/** Which glyph source (and therefore shader/texture) this group binds:
|
|
394
|
+
* baked MSDF atlas, the runtime canvas-SDF dynamic atlas, or tessellated
|
|
395
|
+
* font outlines. */
|
|
396
|
+
source: 'atlas' | 'canvas' | 'outline';
|
|
397
|
+
/** Dynamic-atlas page index for 'canvas' groups; 0 for the others. */
|
|
398
|
+
page: number;
|
|
399
|
+
fill: FillStyle;
|
|
400
|
+
/** Outline painted over the glyphs, or absent for none. Only ever set on
|
|
401
|
+
* an `'outline'` group — the SDF tiers have no geometry to stroke, which
|
|
402
|
+
* is why a stroke pulls its run onto the outline tier at any size. A run
|
|
403
|
+
* the tier cannot serve at all (no registered outlines, synthetic bold)
|
|
404
|
+
* stays on the atlas and carries the request no further. */
|
|
405
|
+
stroke?: Stroke;
|
|
406
|
+
/** Textured glyph quads. Always empty for an `'outline'` group. */
|
|
407
|
+
quads: LaidOutQuad[];
|
|
408
|
+
/** Outline geometry. Always empty for an `'atlas'` / `'canvas'` group —
|
|
409
|
+
* the two channels are exclusive, since the group is also the draw call
|
|
410
|
+
* and one draw call binds one program. */
|
|
411
|
+
glyphs: LaidOutOutlineGlyph[];
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* One decoration rule: an axis-aligned solid rectangle in the same world
|
|
415
|
+
* space as `LaidOutQuad`. Never spans a line break, and carries the fill of
|
|
416
|
+
* the run(s) it decorates so the rule follows the text colour.
|
|
417
|
+
*/
|
|
418
|
+
interface LaidOutDecoration {
|
|
419
|
+
kind: 'underline' | 'strikethrough' | 'overline';
|
|
420
|
+
x0: number;
|
|
421
|
+
y0: number;
|
|
422
|
+
x1: number;
|
|
423
|
+
y1: number;
|
|
424
|
+
fill: FillStyle;
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* One laid-out line's box, in the same world space as `LaidOutQuad`.
|
|
428
|
+
*
|
|
429
|
+
* `[x0, x1]` is the line's own advance width *after* alignment — not the
|
|
430
|
+
* wrap box — so a short centered line reports the span it actually occupies.
|
|
431
|
+
* `[y0, y1]` is the full line box: `y0` is the pen's line top, `y1` is
|
|
432
|
+
* `y0 + max(fontSize * lineHeight)` over the line's runs. Ink can escape it
|
|
433
|
+
* vertically (a tall accent, a deep descender at a lineHeight below ~1.06 —
|
|
434
|
+
* see the `bounds` note at the end of `layoutRuns`); this is the typographic
|
|
435
|
+
* box, not an ink bounding box.
|
|
436
|
+
*
|
|
437
|
+
* Emitted for every line including empty ones, so indices line up with the
|
|
438
|
+
* wrap. An empty line has `x0 === x1`.
|
|
439
|
+
*/
|
|
440
|
+
interface LaidOutLineBox {
|
|
441
|
+
x0: number;
|
|
442
|
+
y0: number;
|
|
443
|
+
x1: number;
|
|
444
|
+
y1: number;
|
|
445
|
+
/** The line's baseline, for callers placing carets or rules against it. */
|
|
446
|
+
baselineY: number;
|
|
447
|
+
/**
|
|
448
|
+
* Caret stops along this line, left to right: `caretXs[i]` is the left edge
|
|
449
|
+
* of the i-th advance cell, and the last entry is the line's right edge, so
|
|
450
|
+
* there is always one more stop than cell. Post-alignment, in the same
|
|
451
|
+
* origin-relative space as `x0`/`x1`.
|
|
452
|
+
*
|
|
453
|
+
* A cell spans one code point *plus* the kerning that precedes the next
|
|
454
|
+
* one, which is why a caret snapped to these midpoints lands where the
|
|
455
|
+
* glyphs actually were painted rather than where an unkerned re-measure
|
|
456
|
+
* would put them.
|
|
457
|
+
*/
|
|
458
|
+
caretXs: number[];
|
|
459
|
+
/**
|
|
460
|
+
* Source offset for each stop in `caretXs`, as a UTF-16 index into the
|
|
461
|
+
* runs' concatenated text. Not contiguous: a code point the face cannot
|
|
462
|
+
* serve occupies no cell, and the wrap swallows the break between lines.
|
|
463
|
+
*/
|
|
464
|
+
caretIndices: number[];
|
|
465
|
+
}
|
|
466
|
+
interface LaidOutRuns {
|
|
467
|
+
groups: LaidOutGroup[];
|
|
468
|
+
/** Decoration rules, in line order; within a span, underline then
|
|
469
|
+
* strikethrough then overline. Empty when nothing is decorated. */
|
|
470
|
+
decorations: LaidOutDecoration[];
|
|
471
|
+
/** Per-line boxes in layout order. Lets a caller reason about where the
|
|
472
|
+
* text actually sits inside its wrap box without re-running the wrap —
|
|
473
|
+
* `textLineBoxes` builds the text silhouette from these, so picking and
|
|
474
|
+
* painting cannot drift apart. */
|
|
475
|
+
lines: LaidOutLineBox[];
|
|
476
|
+
bounds: {
|
|
477
|
+
width: number;
|
|
478
|
+
height: number;
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
interface LayoutRunsOpts {
|
|
482
|
+
maxWidth: number;
|
|
483
|
+
lineHeight: number;
|
|
484
|
+
align: 'left' | 'center' | 'right';
|
|
485
|
+
/**
|
|
486
|
+
* World-space `fontSize` at or above which glyphs are emitted as outline
|
|
487
|
+
* geometry, when the resolved face has outlines registered. Omit (the
|
|
488
|
+
* default) to keep every glyph on its SDF tier.
|
|
489
|
+
*
|
|
490
|
+
* A *world* size rather than a screen size because layout knows nothing
|
|
491
|
+
* about the view; `drawText` divides its screen-pixel threshold by the view
|
|
492
|
+
* scale before calling, so zooming in lowers the world size that qualifies.
|
|
493
|
+
*
|
|
494
|
+
* This is a rendering switch only. Advances, kerning, wrapping and
|
|
495
|
+
* baselines are identical either way — the outline tier changes what a
|
|
496
|
+
* glyph looks like, never where it sits — so measurement callers
|
|
497
|
+
* (`measureTextBounds`, `textLineBoxes`) leave it unset and still agree
|
|
498
|
+
* with the paint, and crossing the threshold cannot reflow text.
|
|
499
|
+
*
|
|
500
|
+
* Ignored for a family that resolved to the outline tier because it has no
|
|
501
|
+
* atlas: there the geometry is not an upgrade over another rendering, it is
|
|
502
|
+
* the only one, so the threshold has nothing to choose between.
|
|
503
|
+
*/
|
|
504
|
+
outlineMinSize?: number;
|
|
505
|
+
}
|
|
506
|
+
declare function layoutRuns(runs: readonly ResolvedRun[], opts: LayoutRunsOpts): LaidOutRuns;
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Laid-out text, memoized. The one entry point every caller of `layoutRuns`
|
|
510
|
+
* should reach for — the renderer's paint path, the silhouette measurement
|
|
511
|
+
* behind picking, and caret hit-testing all answer from this.
|
|
512
|
+
*
|
|
513
|
+
* `layoutRuns` is the most expensive derivation in the kit — it walks every
|
|
514
|
+
* codepoint, resolves a face per run, measures, wraps, aligns, and places a
|
|
515
|
+
* quad or an outline glyph for each — and `drawText` ran it per text command
|
|
516
|
+
* per frame. Measured: 2.0 ms/frame for 200 short labels, 9.3 for 1000, and
|
|
517
|
+
* 25.9 for 200 wrapped paragraphs. That last figure is the entire frame
|
|
518
|
+
* budget spent before a single triangle is drawn.
|
|
519
|
+
*
|
|
520
|
+
* ### Two keys, tried in order
|
|
521
|
+
*
|
|
522
|
+
* **Array identity**, in a `WeakMap`, the same contract as the renderer's
|
|
523
|
+
* `WeakMap<Path, Mesh>` — and for the same reason: it makes the entry
|
|
524
|
+
* collectable with whatever owns the runs, and the lookup is a pointer
|
|
525
|
+
* compare. `kit:text`'s memoized `paint` hands back a stable array per node,
|
|
526
|
+
* so the renderer stays on this path.
|
|
527
|
+
*
|
|
528
|
+
* **Structure**, in a bounded `Map`, for every caller that cannot. Measuring
|
|
529
|
+
* a pose resolves its style and allocates a fresh `ResolvedRun[]` each call,
|
|
530
|
+
* so an identity-keyed cache could never hold anything for it — and those
|
|
531
|
+
* callers run on the drag path, once per pose change. Serializing the runs is
|
|
532
|
+
* real work, unlike the pointer compare above, which is exactly why it is
|
|
533
|
+
* second: an identity hit never pays for it.
|
|
534
|
+
*
|
|
535
|
+
* Under either, one entry per distinct `(maxWidth, lineHeight, align, outline
|
|
536
|
+
* threshold)`. Position is deliberately absent: `layoutRuns` emits
|
|
537
|
+
* origin-relative geometry and `drawText` translates at upload, so a text node
|
|
538
|
+
* dragged across the page keeps hitting the same entry.
|
|
539
|
+
*
|
|
540
|
+
* ### The outline threshold
|
|
541
|
+
*
|
|
542
|
+
* `outlineMinSize` is derived from the view scale, so recording it literally
|
|
543
|
+
* would miss on every frame of a zoom. It reaches `layoutRuns` through exactly
|
|
544
|
+
* one comparison (`run.fontSize < min`, in `outlineFor`), so what the layout
|
|
545
|
+
* actually depends on is *which runs clear the bar* — and since the test is a
|
|
546
|
+
* plain `>=` on size, that set is pinned by how many of the distinct run sizes
|
|
547
|
+
* clear it. Recording that count is exact, not a quantization: the entry stays
|
|
548
|
+
* valid across every zoom that doesn't cross a glyph size, and is dropped the
|
|
549
|
+
* moment one does.
|
|
550
|
+
*
|
|
551
|
+
* ### What the keys cannot see
|
|
552
|
+
*
|
|
553
|
+
* The font set. A face landing changes metrics with no change to the runs, so
|
|
554
|
+
* every lookup compares `glyphGeneration()` — advanced by the dynamic SDF
|
|
555
|
+
* atlas, by the outline registry, and by `registerFont` — and drops
|
|
556
|
+
* everything when it moves. That is the same escape hatch `nodeMemo`'s
|
|
557
|
+
* generation counter provides, for the same class of failure. Polled rather
|
|
558
|
+
* than subscribed: a module-load subscription is a cross-package side effect
|
|
559
|
+
* that makes importing this package require the font package's whole surface.
|
|
560
|
+
*
|
|
561
|
+
* ### Bounds
|
|
562
|
+
*
|
|
563
|
+
* Variants per runs array are capped and evicted wholesale, matching
|
|
564
|
+
* `outlineMeshCache`: the refill cost is one layout, and the bookkeeping would
|
|
565
|
+
* cost more than the misses it avoids. The structural map cannot ride an
|
|
566
|
+
* array's lifetime the way the `WeakMap` does — nothing collects its keys —
|
|
567
|
+
* so it is a real LRU with a hard entry count.
|
|
568
|
+
*/
|
|
569
|
+
|
|
570
|
+
/** Distinct option combinations held for one runs array before the whole set
|
|
571
|
+
* is dropped. Sized for "a few views onto the same text". */
|
|
572
|
+
declare const LAYOUT_CACHE_VARIANT_LIMIT = 8;
|
|
573
|
+
/** Layouts held against the structural key before the least recently used one
|
|
574
|
+
* is dropped. Sized for "every text node visible on a page, plus slack". */
|
|
575
|
+
declare const LAYOUT_CACHE_STRUCTURAL_LIMIT = 64;
|
|
576
|
+
/**
|
|
577
|
+
* Lay `runs` out, reusing the previous result when nothing it depends on has
|
|
578
|
+
* changed. Drop-in for `layoutRuns` — same arguments, same return value.
|
|
579
|
+
*
|
|
580
|
+
* **The result is shared and must be treated as immutable.** `drawText`
|
|
581
|
+
* applies position and `verticalAlign` while packing vertices rather than by
|
|
582
|
+
* shifting the layout, precisely so it can be handed the same object every
|
|
583
|
+
* frame.
|
|
584
|
+
*/
|
|
585
|
+
declare function cachedLayoutRuns(runs: readonly ResolvedRun[], opts: LayoutRunsOpts): LaidOutRuns;
|
|
586
|
+
/** Test helper. Do not call from product code. */
|
|
587
|
+
declare function _resetLayoutCacheForTests(): void;
|
|
588
|
+
|
|
589
|
+
export { DEFAULT_TEXT_STYLE as D, LAYOUT_CACHE_STRUCTURAL_LIMIT as L, MARKDOWN_RUN_GRAMMAR as M, type ResolvedTextStyle as R, type StyledRun as S, type TextStyle as T, _resetLayoutCacheForTests as _, LAYOUT_CACHE_VARIANT_LIMIT as a, type LaidOutDecoration as b, type LaidOutGroup as c, type LaidOutLineBox as d, type LaidOutOutlineGlyph as e, type LaidOutQuad as f, type LaidOutRuns as g, type LayoutRunsOpts as h, type ResolvedRun as i, type RunFlag as j, type RunGrammar as k, type RunMarker as l, SCRIPT_METRICS as m, type TextPaint as n, cachedLayoutRuns as o, fontString as p, layoutRuns as q, markdownToRuns as r, resolveRuns as s, resolveTextStyle as t, runsToMarkdown as u, runsToPlainText as v, toRuns as w };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"test-seams.js"}
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@weasel-js/text",
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "Typography for weasel: styled runs, style resolution, kerned glyph layout, wrap and measurement. No scene graph, no React.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"main": "./dist/index.js",
|
|
9
|
+
"module": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"import": "./dist/index.js",
|
|
14
|
+
"types": "./dist/index.d.ts"
|
|
15
|
+
},
|
|
16
|
+
"./test-seams": {
|
|
17
|
+
"import": "./dist/test-seams.js",
|
|
18
|
+
"types": "./dist/test-seams.d.ts"
|
|
19
|
+
},
|
|
20
|
+
"./package.json": "./package.json"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@weasel-js/font": "1.2.0",
|
|
24
|
+
"@weasel-js/geom": "1.2.0",
|
|
25
|
+
"@weasel-js/paint": "1.2.0"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"test": "vitest run",
|
|
29
|
+
"build": "tsup"
|
|
30
|
+
},
|
|
31
|
+
"author": "orochi235",
|
|
32
|
+
"homepage": "https://orochi235.github.io/weasel/",
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "git+https://github.com/orochi235/weasel.git",
|
|
36
|
+
"directory": "packages/text"
|
|
37
|
+
},
|
|
38
|
+
"bugs": {
|
|
39
|
+
"url": "https://github.com/orochi235/weasel/issues"
|
|
40
|
+
},
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=22"
|
|
43
|
+
},
|
|
44
|
+
"files": [
|
|
45
|
+
"dist",
|
|
46
|
+
"README.md",
|
|
47
|
+
"LICENSE"
|
|
48
|
+
],
|
|
49
|
+
"publishConfig": {
|
|
50
|
+
"access": "public",
|
|
51
|
+
"provenance": true
|
|
52
|
+
}
|
|
53
|
+
}
|