@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
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 orochi235
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# @weasel-js/text
|
|
2
|
+
|
|
3
|
+
Typography for weasel: styled runs, style resolution, kerned glyph layout,
|
|
4
|
+
wrap and measurement. Glyphs come from
|
|
5
|
+
[`@weasel-js/font`](https://www.npmjs.com/package/@weasel-js/font); nothing
|
|
6
|
+
here knows about a scene graph, a renderer or React.
|
|
7
|
+
|
|
8
|
+
Part of [weasel](https://github.com/orochi235/weasel), a domain-agnostic 2D
|
|
9
|
+
scene-graph canvas kit for React. See the
|
|
10
|
+
[API reference](https://orochi235.github.io/weasel/api/).
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
npm install @weasel-js/text
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
`layoutRuns` is the glyph walk: it takes fully-resolved runs and returns
|
|
21
|
+
positioned geometry, grouped so that one group is one draw call.
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { toRuns, resolveTextStyle, resolveRuns, layoutRuns } from '@weasel-js/text';
|
|
25
|
+
|
|
26
|
+
const style = resolveTextStyle({ fontFamily: 'inter', fontSize: 64 });
|
|
27
|
+
const { groups, lines, bounds } = layoutRuns(resolveRuns(toRuns('Hello'), style), {
|
|
28
|
+
maxWidth: 900,
|
|
29
|
+
lineHeight: 1.2,
|
|
30
|
+
align: 'center',
|
|
31
|
+
});
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
A group carries either textured quads (from a baked MSDF atlas) or outline
|
|
35
|
+
glyphs — em-space SVG path data plus the pen position, baseline and scale to
|
|
36
|
+
place it — for a consumer that tessellates its own geometry.
|
|
37
|
+
|
|
38
|
+
Metrics come from whichever tier resolved the family: a baked MSDF atlas, or —
|
|
39
|
+
for a family registered with `registerFontOutlines` and no atlas — the parsed
|
|
40
|
+
face itself, so font bytes alone are enough to lay text out.
|
|
41
|
+
|
|
42
|
+
The inline grammar `runsToMarkdown` / `markdownToRuns` speak is a parameter,
|
|
43
|
+
defaulting to `MARKDOWN_RUN_GRAMMAR` (`**bold**`, `*italic*`, `***both***`).
|
|
44
|
+
Pass a `RunGrammar` with different markers to read or write another spelling.
|
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
import { resolveFontVariant, glyphGeneration, glyphOutline, resolveGlyphFallback } from '@weasel-js/font';
|
|
2
|
+
|
|
3
|
+
// src/layout/layoutRuns.ts
|
|
4
|
+
var UNDERLINE_OFFSET = 0.1;
|
|
5
|
+
var STRIKETHROUGH_OFFSET = -0.3;
|
|
6
|
+
var OVERLINE_OFFSET = -0.9;
|
|
7
|
+
var DECORATION_THICKNESS = 0.05;
|
|
8
|
+
function atlasMetrics(font) {
|
|
9
|
+
return {
|
|
10
|
+
size: font.info.size,
|
|
11
|
+
base: font.common.base,
|
|
12
|
+
advanceOf: (cp) => font.charMap.get(cp)?.xadvance ?? null,
|
|
13
|
+
kernOf: (l, r) => font.kerningMap.get(l)?.get(r) ?? 0
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function fillKey(p) {
|
|
17
|
+
if ("color" in p) return `s:${p.color}:${p.opacity ?? 1}`;
|
|
18
|
+
return `nx:${Math.random()}`;
|
|
19
|
+
}
|
|
20
|
+
function sameFill(a, b) {
|
|
21
|
+
if (a === b) return true;
|
|
22
|
+
if ("color" in a && "color" in b) {
|
|
23
|
+
return a.color === b.color && (a.opacity ?? 1) === (b.opacity ?? 1);
|
|
24
|
+
}
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
function strokePaints(s) {
|
|
28
|
+
if (s === void 0) return false;
|
|
29
|
+
const w = s.width ?? 1;
|
|
30
|
+
return (typeof w === "number" ? w : w.px) > 0;
|
|
31
|
+
}
|
|
32
|
+
function strokeKey(s) {
|
|
33
|
+
if (!s) return "-";
|
|
34
|
+
const width = s.width ?? 1;
|
|
35
|
+
return [
|
|
36
|
+
// A screen-pixel width keys apart from the equal world-unit number: the two
|
|
37
|
+
// resolve to different ribbons, so they must not share a draw call.
|
|
38
|
+
fillKey(s.paint),
|
|
39
|
+
typeof width === "number" ? width : `px${width.px}`,
|
|
40
|
+
s.join ?? "miter",
|
|
41
|
+
s.cap ?? "butt",
|
|
42
|
+
s.miterLimit ?? "",
|
|
43
|
+
s.align ?? "center",
|
|
44
|
+
(s.dash ?? []).join(",")
|
|
45
|
+
].join(":");
|
|
46
|
+
}
|
|
47
|
+
function groupKey(family, weight, style, synthetic, fill, stroke, source, page) {
|
|
48
|
+
return `${family}|${weight}|${style}|${synthetic.bold ? 1 : 0}${synthetic.italic ? 1 : 0}|${fillKey(fill)}|${strokeKey(stroke)}|${source}|${page}`;
|
|
49
|
+
}
|
|
50
|
+
function getOrCreateGroup(ctx, run, resolved, page, sourceOverride) {
|
|
51
|
+
const atlasFamily = resolved.resolved.family;
|
|
52
|
+
const resolvedWeight = resolved.resolved.weight;
|
|
53
|
+
const resolvedStyle = resolved.resolved.style;
|
|
54
|
+
const source = sourceOverride ?? resolved.source;
|
|
55
|
+
const key = groupKey(
|
|
56
|
+
atlasFamily,
|
|
57
|
+
resolvedWeight,
|
|
58
|
+
resolvedStyle,
|
|
59
|
+
resolved.synthetic,
|
|
60
|
+
run.fill,
|
|
61
|
+
source === "outline" ? run.stroke : void 0,
|
|
62
|
+
source,
|
|
63
|
+
page
|
|
64
|
+
);
|
|
65
|
+
let g = ctx.groups.get(key);
|
|
66
|
+
if (!g) {
|
|
67
|
+
g = {
|
|
68
|
+
family: atlasFamily,
|
|
69
|
+
weight: resolvedWeight,
|
|
70
|
+
style: resolvedStyle,
|
|
71
|
+
synthetic: { ...resolved.synthetic },
|
|
72
|
+
source,
|
|
73
|
+
page,
|
|
74
|
+
fill: run.fill,
|
|
75
|
+
// Only the outline tier can paint it, and carrying it on an SDF group
|
|
76
|
+
// would be a promise the renderer cannot keep.
|
|
77
|
+
...source === "outline" && run.stroke !== void 0 ? { stroke: run.stroke } : {},
|
|
78
|
+
quads: [],
|
|
79
|
+
glyphs: []
|
|
80
|
+
};
|
|
81
|
+
ctx.groups.set(key, g);
|
|
82
|
+
}
|
|
83
|
+
return g;
|
|
84
|
+
}
|
|
85
|
+
function resolveGlyph(run, font, resolved, cp) {
|
|
86
|
+
const direct = font.charMap.get(cp);
|
|
87
|
+
if (direct) return { glyph: direct, font, resolved };
|
|
88
|
+
const fallback = resolveGlyphFallback(run.fontFamily, run.fontWeight, run.fontStyle);
|
|
89
|
+
const face = fallback?.dynamicFace;
|
|
90
|
+
if (fallback && face) {
|
|
91
|
+
const glyph = face.requestGlyph(cp);
|
|
92
|
+
if (glyph.xadvance > 0 || glyph.width > 0) {
|
|
93
|
+
return { glyph, font: face.font, resolved: fallback };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
warnMissingGlyphOnce(resolved.resolved.family, cp);
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
var warnedMissingGlyphs = /* @__PURE__ */ new Set();
|
|
100
|
+
function warnMissingGlyphOnce(family, cp) {
|
|
101
|
+
const key = `${family}|${cp}`;
|
|
102
|
+
if (warnedMissingGlyphs.has(key)) return;
|
|
103
|
+
warnedMissingGlyphs.add(key);
|
|
104
|
+
const ch = String.fromCodePoint(cp);
|
|
105
|
+
console.warn(
|
|
106
|
+
`weasel layoutRuns: no glyph for U+${cp.toString(16).toUpperCase().padStart(4, "0")} (${JSON.stringify(ch)}) in "${family}", and the dynamic tier could not rasterize it \u2014 skipping the character. Bake it into the atlas, or call registerCanvasFont("${family}") to serve missing codepoints from installed fonts.`
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
function layoutRuns(runs, opts) {
|
|
110
|
+
const ctx = { groups: /* @__PURE__ */ new Map() };
|
|
111
|
+
const entries = [];
|
|
112
|
+
let prevCp;
|
|
113
|
+
let prevMetrics;
|
|
114
|
+
let prevFontSize;
|
|
115
|
+
let srcIndex = 0;
|
|
116
|
+
for (const run of runs) {
|
|
117
|
+
const resolved = resolveFontVariant(run.fontFamily, run.fontWeight, run.fontStyle);
|
|
118
|
+
const outlineFace = resolved.outlineFace;
|
|
119
|
+
const font = resolved.entry?.font ?? resolved.dynamicFace?.font ?? null;
|
|
120
|
+
const metrics = outlineFace ? { size: 1, base: outlineFace.ascender, advanceOf: (cp) => outlineFace.advanceOf(cp), kernOf: (l, r) => outlineFace.kernOf(l, r) } : font ? atlasMetrics(font) : void 0;
|
|
121
|
+
if (!metrics) {
|
|
122
|
+
srcIndex += run.text.length;
|
|
123
|
+
prevCp = void 0;
|
|
124
|
+
prevMetrics = void 0;
|
|
125
|
+
prevFontSize = void 0;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
const scale = run.fontSize / metrics.size;
|
|
129
|
+
const tracking = run.letterSpacing;
|
|
130
|
+
for (const ch of [...run.text]) {
|
|
131
|
+
const cp = ch.codePointAt(0);
|
|
132
|
+
const isNewline = cp === 10;
|
|
133
|
+
const isSpace = cp === 32;
|
|
134
|
+
const srcStart = srcIndex;
|
|
135
|
+
const srcEnd = srcIndex + ch.length;
|
|
136
|
+
srcIndex = srcEnd;
|
|
137
|
+
if (isNewline) {
|
|
138
|
+
entries.push({
|
|
139
|
+
run,
|
|
140
|
+
font,
|
|
141
|
+
metrics,
|
|
142
|
+
glyph: { id: cp, x: 0, y: 0, width: 0, height: 0, xoffset: 0, yoffset: 0, xadvance: 0, page: 0 },
|
|
143
|
+
// A newline consumes no advance, so it takes no tracking either.
|
|
144
|
+
cp,
|
|
145
|
+
advance: 0,
|
|
146
|
+
tracking: 0,
|
|
147
|
+
kerningBefore: 0,
|
|
148
|
+
isSpace: false,
|
|
149
|
+
isNewline: true,
|
|
150
|
+
resolved,
|
|
151
|
+
fontSize: run.fontSize,
|
|
152
|
+
srcIndex: srcStart,
|
|
153
|
+
srcEnd
|
|
154
|
+
});
|
|
155
|
+
prevCp = void 0;
|
|
156
|
+
prevMetrics = void 0;
|
|
157
|
+
prevFontSize = void 0;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (isSpace) {
|
|
161
|
+
const spaceGlyph = resolved.dynamicFace ? resolved.dynamicFace.requestGlyph(32) : font?.charMap.get(32) ?? null;
|
|
162
|
+
const spaceAdvance = spaceGlyph ? spaceGlyph.xadvance : metrics.advanceOf(32);
|
|
163
|
+
const advance = spaceAdvance !== null ? spaceAdvance * scale : run.fontSize * 0.25;
|
|
164
|
+
let kerningBefore2 = 0;
|
|
165
|
+
if (prevCp !== void 0 && prevMetrics !== void 0 && prevFontSize !== void 0) {
|
|
166
|
+
kerningBefore2 = prevMetrics.kernOf(prevCp, cp) * (prevFontSize / prevMetrics.size);
|
|
167
|
+
}
|
|
168
|
+
entries.push({
|
|
169
|
+
run,
|
|
170
|
+
font,
|
|
171
|
+
metrics,
|
|
172
|
+
glyph: spaceGlyph ?? { id: 32, x: 0, y: 0, width: 0, height: 0, xoffset: 0, yoffset: 0, xadvance: 0, page: 0 },
|
|
173
|
+
cp,
|
|
174
|
+
advance,
|
|
175
|
+
tracking,
|
|
176
|
+
kerningBefore: kerningBefore2,
|
|
177
|
+
isSpace: true,
|
|
178
|
+
isNewline: false,
|
|
179
|
+
resolved,
|
|
180
|
+
fontSize: run.fontSize,
|
|
181
|
+
srcIndex: srcStart,
|
|
182
|
+
srcEnd
|
|
183
|
+
});
|
|
184
|
+
prevCp = cp;
|
|
185
|
+
prevMetrics = metrics;
|
|
186
|
+
prevFontSize = run.fontSize;
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
if (outlineFace) {
|
|
190
|
+
const adv = metrics.advanceOf(cp);
|
|
191
|
+
if (adv === null) {
|
|
192
|
+
prevCp = cp;
|
|
193
|
+
prevMetrics = metrics;
|
|
194
|
+
prevFontSize = run.fontSize;
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
let kerningBefore2 = 0;
|
|
198
|
+
if (prevCp !== void 0 && prevMetrics !== void 0 && prevFontSize !== void 0) {
|
|
199
|
+
kerningBefore2 = prevMetrics.kernOf(prevCp, cp) * (prevFontSize / prevMetrics.size);
|
|
200
|
+
}
|
|
201
|
+
entries.push({
|
|
202
|
+
run,
|
|
203
|
+
font: null,
|
|
204
|
+
metrics,
|
|
205
|
+
glyph: null,
|
|
206
|
+
cp,
|
|
207
|
+
advance: adv * scale,
|
|
208
|
+
tracking,
|
|
209
|
+
kerningBefore: kerningBefore2,
|
|
210
|
+
isSpace,
|
|
211
|
+
isNewline: false,
|
|
212
|
+
resolved,
|
|
213
|
+
fontSize: run.fontSize,
|
|
214
|
+
srcIndex: srcStart,
|
|
215
|
+
srcEnd
|
|
216
|
+
});
|
|
217
|
+
prevCp = cp;
|
|
218
|
+
prevMetrics = metrics;
|
|
219
|
+
prevFontSize = run.fontSize;
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
const hit = resolved.dynamicFace ? { glyph: resolved.dynamicFace.requestGlyph(cp), font, resolved } : resolveGlyph(run, font, resolved, cp);
|
|
223
|
+
if (!hit) {
|
|
224
|
+
prevCp = cp;
|
|
225
|
+
prevMetrics = metrics;
|
|
226
|
+
prevFontSize = run.fontSize;
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
const glyphFont = hit.font;
|
|
230
|
+
const glyphMetrics = glyphFont === font ? metrics : atlasMetrics(glyphFont);
|
|
231
|
+
const glyphScale = run.fontSize / glyphFont.info.size;
|
|
232
|
+
let kerningBefore = 0;
|
|
233
|
+
if (prevCp !== void 0 && prevMetrics !== void 0 && prevFontSize !== void 0) {
|
|
234
|
+
kerningBefore = prevMetrics.kernOf(prevCp, cp) * (prevFontSize / prevMetrics.size);
|
|
235
|
+
}
|
|
236
|
+
entries.push({
|
|
237
|
+
run,
|
|
238
|
+
font: glyphFont,
|
|
239
|
+
metrics: glyphMetrics,
|
|
240
|
+
glyph: hit.glyph,
|
|
241
|
+
cp,
|
|
242
|
+
advance: hit.glyph.xadvance * glyphScale,
|
|
243
|
+
tracking,
|
|
244
|
+
kerningBefore,
|
|
245
|
+
isSpace,
|
|
246
|
+
isNewline: false,
|
|
247
|
+
resolved: hit.resolved,
|
|
248
|
+
fontSize: run.fontSize,
|
|
249
|
+
srcIndex: srcStart,
|
|
250
|
+
srcEnd
|
|
251
|
+
});
|
|
252
|
+
prevCp = cp;
|
|
253
|
+
prevMetrics = glyphMetrics;
|
|
254
|
+
prevFontSize = run.fontSize;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
const lines = [];
|
|
258
|
+
let cur = { entries: [], width: 0, height: 0 };
|
|
259
|
+
function commitLine() {
|
|
260
|
+
lines.push(cur);
|
|
261
|
+
cur = { entries: [], width: 0, height: 0 };
|
|
262
|
+
}
|
|
263
|
+
let i = 0;
|
|
264
|
+
while (i < entries.length) {
|
|
265
|
+
const e = entries[i];
|
|
266
|
+
if (e.isNewline) {
|
|
267
|
+
if (cur.entries.length === 0) {
|
|
268
|
+
cur.height = Math.max(cur.height, e.fontSize * opts.lineHeight);
|
|
269
|
+
cur.blank = e;
|
|
270
|
+
}
|
|
271
|
+
commitLine();
|
|
272
|
+
i++;
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
if (e.isSpace) {
|
|
276
|
+
if (cur.entries.length > 0) {
|
|
277
|
+
cur.entries.push(e);
|
|
278
|
+
cur.width += e.kerningBefore + e.advance + e.tracking;
|
|
279
|
+
cur.height = Math.max(cur.height, e.fontSize * opts.lineHeight);
|
|
280
|
+
}
|
|
281
|
+
i++;
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
let j = i;
|
|
285
|
+
let wordWidth = 0;
|
|
286
|
+
while (j < entries.length && !entries[j].isSpace && !entries[j].isNewline) {
|
|
287
|
+
const w = entries[j];
|
|
288
|
+
wordWidth += w.kerningBefore + w.advance + w.tracking;
|
|
289
|
+
j++;
|
|
290
|
+
}
|
|
291
|
+
if (Number.isFinite(opts.maxWidth) && cur.width + wordWidth > opts.maxWidth && cur.entries.length > 0) {
|
|
292
|
+
commitLine();
|
|
293
|
+
}
|
|
294
|
+
for (let k = i; k < j; k++) {
|
|
295
|
+
const w = entries[k];
|
|
296
|
+
const kerningBefore = cur.entries.length === 0 ? 0 : w.kerningBefore;
|
|
297
|
+
cur.entries.push({ ...w, kerningBefore });
|
|
298
|
+
cur.width += kerningBefore + w.advance + w.tracking;
|
|
299
|
+
cur.height = Math.max(cur.height, w.fontSize * opts.lineHeight);
|
|
300
|
+
}
|
|
301
|
+
i = j;
|
|
302
|
+
}
|
|
303
|
+
if (cur.entries.length > 0) commitLine();
|
|
304
|
+
function outlineFor(e) {
|
|
305
|
+
const r0 = e.resolved.resolved;
|
|
306
|
+
if (e.resolved.source === "outline") {
|
|
307
|
+
return glyphOutline(r0.family, r0.weight, r0.style, e.cp);
|
|
308
|
+
}
|
|
309
|
+
const min = opts.outlineMinSize;
|
|
310
|
+
if (min === void 0) return null;
|
|
311
|
+
if (e.fontSize < min && !strokePaints(e.run.stroke)) return null;
|
|
312
|
+
if (e.resolved.synthetic.bold) return null;
|
|
313
|
+
const r = e.resolved.resolved;
|
|
314
|
+
return glyphOutline(r.family, r.weight, r.style, e.cp);
|
|
315
|
+
}
|
|
316
|
+
const decorations = [];
|
|
317
|
+
let span = null;
|
|
318
|
+
function flushSpan() {
|
|
319
|
+
const s = span;
|
|
320
|
+
span = null;
|
|
321
|
+
if (!s || s.x1 <= s.x0) return;
|
|
322
|
+
const thickness = s.fontSize * DECORATION_THICKNESS;
|
|
323
|
+
if (s.underline) {
|
|
324
|
+
const y0 = s.baselineY + s.fontSize * UNDERLINE_OFFSET;
|
|
325
|
+
decorations.push({ kind: "underline", x0: s.x0, y0, x1: s.x1, y1: y0 + thickness, fill: s.fill });
|
|
326
|
+
}
|
|
327
|
+
if (s.strikethrough) {
|
|
328
|
+
const y0 = s.baselineY + s.fontSize * STRIKETHROUGH_OFFSET;
|
|
329
|
+
decorations.push({ kind: "strikethrough", x0: s.x0, y0, x1: s.x1, y1: y0 + thickness, fill: s.fill });
|
|
330
|
+
}
|
|
331
|
+
if (s.overline) {
|
|
332
|
+
const y0 = s.baselineY + s.fontSize * OVERLINE_OFFSET;
|
|
333
|
+
decorations.push({ kind: "overline", x0: s.x0, y0, x1: s.x1, y1: y0 + thickness, fill: s.fill });
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
const lineBoxes = [];
|
|
337
|
+
let penY = 0;
|
|
338
|
+
let maxLineWidth = 0;
|
|
339
|
+
const finiteWidth = Number.isFinite(opts.maxWidth) ? opts.maxWidth : 0;
|
|
340
|
+
for (const line of lines) {
|
|
341
|
+
const alignShift = (() => {
|
|
342
|
+
if (opts.align === "left") return 0;
|
|
343
|
+
if (!Number.isFinite(opts.maxWidth)) {
|
|
344
|
+
return opts.align === "center" ? -line.width / 2 : -line.width;
|
|
345
|
+
}
|
|
346
|
+
const slack = finiteWidth - line.width;
|
|
347
|
+
return opts.align === "center" ? slack / 2 : slack;
|
|
348
|
+
})();
|
|
349
|
+
const lineX0 = alignShift;
|
|
350
|
+
let lineAscent = 0;
|
|
351
|
+
for (const e of line.entries) {
|
|
352
|
+
lineAscent = Math.max(lineAscent, e.metrics.base * (e.fontSize / e.metrics.size));
|
|
353
|
+
}
|
|
354
|
+
if (line.entries.length === 0 && line.blank) {
|
|
355
|
+
lineAscent = line.blank.metrics.base * (line.blank.fontSize / line.blank.metrics.size);
|
|
356
|
+
}
|
|
357
|
+
const lineBaselineY = penY + lineAscent;
|
|
358
|
+
const caretXs = [];
|
|
359
|
+
const caretIndices = [];
|
|
360
|
+
let penX = lineX0;
|
|
361
|
+
for (const e of line.entries) {
|
|
362
|
+
penX += e.kerningBefore;
|
|
363
|
+
caretXs.push(penX);
|
|
364
|
+
caretIndices.push(e.srcIndex);
|
|
365
|
+
const step = e.advance + e.tracking;
|
|
366
|
+
const scale = e.fontSize / e.metrics.size;
|
|
367
|
+
const baselineY = lineBaselineY - e.run.baselineShift;
|
|
368
|
+
if (e.run.underline || e.run.strikethrough || e.run.overline) {
|
|
369
|
+
if (span !== null && span.underline === e.run.underline && span.strikethrough === e.run.strikethrough && span.overline === e.run.overline && span.fontSize === e.fontSize && span.baselineY === baselineY && sameFill(span.fill, e.run.fill)) {
|
|
370
|
+
span.x1 = penX + step;
|
|
371
|
+
} else {
|
|
372
|
+
flushSpan();
|
|
373
|
+
span = {
|
|
374
|
+
underline: e.run.underline,
|
|
375
|
+
strikethrough: e.run.strikethrough,
|
|
376
|
+
overline: e.run.overline,
|
|
377
|
+
fill: e.run.fill,
|
|
378
|
+
fontSize: e.fontSize,
|
|
379
|
+
baselineY,
|
|
380
|
+
x0: penX,
|
|
381
|
+
x1: penX + step
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
} else {
|
|
385
|
+
flushSpan();
|
|
386
|
+
}
|
|
387
|
+
const outlineD = outlineFor(e);
|
|
388
|
+
if (outlineD !== null) {
|
|
389
|
+
const group2 = getOrCreateGroup(ctx, e.run, e.resolved, 0, "outline");
|
|
390
|
+
group2.glyphs.push({
|
|
391
|
+
d: outlineD,
|
|
392
|
+
key: `${e.resolved.resolved.family}|${e.resolved.resolved.weight}|${e.resolved.resolved.style}|${e.cp}`,
|
|
393
|
+
x: penX,
|
|
394
|
+
baselineY,
|
|
395
|
+
// Em space is unit-scale, so world units per em is just the size.
|
|
396
|
+
scale: e.fontSize
|
|
397
|
+
});
|
|
398
|
+
penX += step;
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
if (e.font === null || e.glyph === null || e.advance === 0 || e.glyph.width === 0 || e.glyph.page < 0) {
|
|
402
|
+
penX += step;
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
const group = getOrCreateGroup(ctx, e.run, e.resolved, e.glyph.page);
|
|
406
|
+
const atlasW = e.font.common.scaleW;
|
|
407
|
+
const atlasH = e.font.common.scaleH;
|
|
408
|
+
const qx0 = penX + e.glyph.xoffset * scale;
|
|
409
|
+
const qy0 = baselineY + (e.glyph.yoffset - e.metrics.base) * scale;
|
|
410
|
+
const qx1 = qx0 + e.glyph.width * scale;
|
|
411
|
+
const qy1 = qy0 + e.glyph.height * scale;
|
|
412
|
+
const u0 = e.glyph.x / atlasW;
|
|
413
|
+
const v0 = e.glyph.y / atlasH;
|
|
414
|
+
const u1 = (e.glyph.x + e.glyph.width) / atlasW;
|
|
415
|
+
const v1 = (e.glyph.y + e.glyph.height) / atlasH;
|
|
416
|
+
group.quads.push({ x0: qx0, y0: qy0, x1: qx1, y1: qy1, u0, v0, u1, v1, baselineY });
|
|
417
|
+
penX += step;
|
|
418
|
+
}
|
|
419
|
+
flushSpan();
|
|
420
|
+
const lastCell = line.entries[line.entries.length - 1];
|
|
421
|
+
caretXs.push(penX);
|
|
422
|
+
caretIndices.push(lastCell ? lastCell.srcEnd : line.blank?.srcIndex ?? 0);
|
|
423
|
+
lineBoxes.push({
|
|
424
|
+
x0: lineX0,
|
|
425
|
+
y0: penY,
|
|
426
|
+
x1: lineX0 + line.width,
|
|
427
|
+
y1: penY + line.height,
|
|
428
|
+
baselineY: lineBaselineY,
|
|
429
|
+
caretXs,
|
|
430
|
+
caretIndices
|
|
431
|
+
});
|
|
432
|
+
maxLineWidth = Math.max(maxLineWidth, line.width);
|
|
433
|
+
penY += line.height;
|
|
434
|
+
}
|
|
435
|
+
return {
|
|
436
|
+
groups: [...ctx.groups.values()],
|
|
437
|
+
decorations,
|
|
438
|
+
lines: lineBoxes,
|
|
439
|
+
bounds: { width: maxLineWidth, height: penY }
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
var LAYOUT_CACHE_VARIANT_LIMIT = 8;
|
|
443
|
+
var LAYOUT_CACHE_STRUCTURAL_LIMIT = 64;
|
|
444
|
+
var cache = /* @__PURE__ */ new WeakMap();
|
|
445
|
+
var structural = /* @__PURE__ */ new Map();
|
|
446
|
+
var structuralGeneration = -1;
|
|
447
|
+
function outlineBucket(runs, min) {
|
|
448
|
+
if (min === void 0) return -1;
|
|
449
|
+
const sizes = /* @__PURE__ */ new Set();
|
|
450
|
+
for (const r of runs) sizes.add(r.fontSize);
|
|
451
|
+
let n = 0;
|
|
452
|
+
for (const size of sizes) if (size >= min) n++;
|
|
453
|
+
return n;
|
|
454
|
+
}
|
|
455
|
+
function variantKey(runs, opts) {
|
|
456
|
+
return `${opts.maxWidth}|${opts.lineHeight}|${opts.align}|${outlineBucket(runs, opts.outlineMinSize)}`;
|
|
457
|
+
}
|
|
458
|
+
var nextRefId = 1;
|
|
459
|
+
var refIds = /* @__PURE__ */ new WeakMap();
|
|
460
|
+
function refId(o) {
|
|
461
|
+
let id = refIds.get(o);
|
|
462
|
+
if (id === void 0) {
|
|
463
|
+
id = nextRefId++;
|
|
464
|
+
refIds.set(o, id);
|
|
465
|
+
}
|
|
466
|
+
return id;
|
|
467
|
+
}
|
|
468
|
+
function paintKey(p) {
|
|
469
|
+
return "color" in p ? `s${p.color}:${p.opacity ?? 1}` : `#${refId(p)}`;
|
|
470
|
+
}
|
|
471
|
+
function strokeKey2(s) {
|
|
472
|
+
if (!s) return "-";
|
|
473
|
+
const width = s.width ?? 1;
|
|
474
|
+
return [
|
|
475
|
+
paintKey(s.paint),
|
|
476
|
+
typeof width === "number" ? width : `px${width.px}`,
|
|
477
|
+
s.join ?? "miter",
|
|
478
|
+
s.cap ?? "butt",
|
|
479
|
+
s.miterLimit ?? "",
|
|
480
|
+
s.align ?? "center",
|
|
481
|
+
(s.dash ?? []).join(",")
|
|
482
|
+
].join(":");
|
|
483
|
+
}
|
|
484
|
+
function runsKey(runs) {
|
|
485
|
+
let out = "";
|
|
486
|
+
for (const r of runs) {
|
|
487
|
+
out += `${r.text.length}:${r.text}|${r.fontFamily.length}:${r.fontFamily}|${r.fontSize}|${r.fontWeight}|${r.fontStyle}|${r.letterSpacing}|${r.underline ? 1 : 0}${r.strikethrough ? 1 : 0}${r.overline ? 1 : 0}|${r.baselineShift}|${paintKey(r.fill)}|${strokeKey2(r.stroke)}|`;
|
|
488
|
+
}
|
|
489
|
+
return out;
|
|
490
|
+
}
|
|
491
|
+
function cachedLayoutRuns(runs, opts) {
|
|
492
|
+
const fonts = glyphGeneration();
|
|
493
|
+
let entry = cache.get(runs);
|
|
494
|
+
if (entry === void 0) {
|
|
495
|
+
entry = { generation: fonts, byVariant: /* @__PURE__ */ new Map() };
|
|
496
|
+
cache.set(runs, entry);
|
|
497
|
+
} else if (entry.generation !== fonts) {
|
|
498
|
+
entry.generation = fonts;
|
|
499
|
+
entry.byVariant.clear();
|
|
500
|
+
}
|
|
501
|
+
const key = variantKey(runs, opts);
|
|
502
|
+
const hit = entry.byVariant.get(key);
|
|
503
|
+
if (hit !== void 0) return hit;
|
|
504
|
+
if (structuralGeneration !== fonts) {
|
|
505
|
+
structural.clear();
|
|
506
|
+
structuralGeneration = fonts;
|
|
507
|
+
}
|
|
508
|
+
const structuralK = `${runsKey(runs)}\0${key}`;
|
|
509
|
+
let laid = structural.get(structuralK);
|
|
510
|
+
if (laid !== void 0) {
|
|
511
|
+
structural.delete(structuralK);
|
|
512
|
+
} else {
|
|
513
|
+
laid = layoutRuns(runs, opts);
|
|
514
|
+
if (structural.size >= LAYOUT_CACHE_STRUCTURAL_LIMIT) {
|
|
515
|
+
structural.delete(structural.keys().next().value);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
structural.set(structuralK, laid);
|
|
519
|
+
if (entry.byVariant.size >= LAYOUT_CACHE_VARIANT_LIMIT) entry.byVariant.clear();
|
|
520
|
+
entry.byVariant.set(key, laid);
|
|
521
|
+
return laid;
|
|
522
|
+
}
|
|
523
|
+
function _resetLayoutCacheForTests() {
|
|
524
|
+
cache = /* @__PURE__ */ new WeakMap();
|
|
525
|
+
structural = /* @__PURE__ */ new Map();
|
|
526
|
+
structuralGeneration = -1;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
export { LAYOUT_CACHE_STRUCTURAL_LIMIT, LAYOUT_CACHE_VARIANT_LIMIT, _resetLayoutCacheForTests, cachedLayoutRuns, layoutRuns };
|
|
530
|
+
//# sourceMappingURL=chunk-4OGXTESY.js.map
|
|
531
|
+
//# sourceMappingURL=chunk-4OGXTESY.js.map
|