@effing/canvas 0.22.2 → 0.23.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 +21 -18
- package/dist/index.js +421 -254
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -23,6 +23,292 @@ function renderLottieFrame(ctx, animation, frame) {
|
|
|
23
23
|
// src/jsx/draw/index.ts
|
|
24
24
|
import { loadImage as loadImage3 } from "@napi-rs/canvas";
|
|
25
25
|
|
|
26
|
+
// src/jsx/style/compute.ts
|
|
27
|
+
var ROOT_FONT_SIZE = 16;
|
|
28
|
+
var DEFAULT_STYLE = {
|
|
29
|
+
display: "flex",
|
|
30
|
+
flexDirection: "row",
|
|
31
|
+
flexWrap: "nowrap",
|
|
32
|
+
flexGrow: 0,
|
|
33
|
+
flexShrink: 0,
|
|
34
|
+
alignItems: "stretch",
|
|
35
|
+
justifyContent: "flex-start",
|
|
36
|
+
position: "relative",
|
|
37
|
+
fontSize: ROOT_FONT_SIZE,
|
|
38
|
+
fontWeight: 400,
|
|
39
|
+
fontStyle: "normal",
|
|
40
|
+
color: "black",
|
|
41
|
+
lineHeight: "normal",
|
|
42
|
+
textAlign: "left",
|
|
43
|
+
whiteSpace: "normal",
|
|
44
|
+
wordBreak: "normal",
|
|
45
|
+
textOverflow: "clip",
|
|
46
|
+
opacity: 1,
|
|
47
|
+
overflow: "visible"
|
|
48
|
+
};
|
|
49
|
+
var INHERITABLE_PROPS = [
|
|
50
|
+
"color",
|
|
51
|
+
"fontSize",
|
|
52
|
+
"fontFamily",
|
|
53
|
+
"fontWeight",
|
|
54
|
+
"fontStyle",
|
|
55
|
+
"textAlign",
|
|
56
|
+
"textTransform",
|
|
57
|
+
"textDecoration",
|
|
58
|
+
"lineHeight",
|
|
59
|
+
"letterSpacing",
|
|
60
|
+
"whiteSpace",
|
|
61
|
+
"wordBreak",
|
|
62
|
+
"textOverflow",
|
|
63
|
+
"lineClamp",
|
|
64
|
+
"textBoxTrim",
|
|
65
|
+
"textBoxEdge",
|
|
66
|
+
"WebkitTextStrokeWidth",
|
|
67
|
+
"WebkitTextStrokeColor"
|
|
68
|
+
];
|
|
69
|
+
function resolveStyle(rawStyle, parentStyle) {
|
|
70
|
+
const style = { ...rawStyle };
|
|
71
|
+
for (const prop of INHERITABLE_PROPS) {
|
|
72
|
+
if (style[prop] === void 0 && parentStyle[prop] !== void 0) {
|
|
73
|
+
style[prop] = parentStyle[prop];
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (typeof style.fontSize === "string") {
|
|
77
|
+
const parentFontSize = typeof parentStyle.fontSize === "number" ? parentStyle.fontSize : ROOT_FONT_SIZE;
|
|
78
|
+
const resolved = resolveUnit(
|
|
79
|
+
style.fontSize,
|
|
80
|
+
0,
|
|
81
|
+
0,
|
|
82
|
+
parentFontSize,
|
|
83
|
+
ROOT_FONT_SIZE
|
|
84
|
+
);
|
|
85
|
+
style.fontSize = typeof resolved === "number" ? resolved : parentFontSize;
|
|
86
|
+
}
|
|
87
|
+
if (typeof style.lineHeight === "string") {
|
|
88
|
+
const str = style.lineHeight;
|
|
89
|
+
if (str.endsWith("%")) {
|
|
90
|
+
style.lineHeight = parseFloat(str) / 100;
|
|
91
|
+
} else {
|
|
92
|
+
const parsed = parseFloat(str);
|
|
93
|
+
if (!isNaN(parsed)) {
|
|
94
|
+
style.lineHeight = parsed;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (typeof style.letterSpacing === "string") {
|
|
99
|
+
const currentFontSize = typeof style.fontSize === "number" ? style.fontSize : typeof parentStyle.fontSize === "number" ? parentStyle.fontSize : ROOT_FONT_SIZE;
|
|
100
|
+
const resolved = resolveUnit(
|
|
101
|
+
style.letterSpacing,
|
|
102
|
+
0,
|
|
103
|
+
0,
|
|
104
|
+
currentFontSize,
|
|
105
|
+
ROOT_FONT_SIZE
|
|
106
|
+
);
|
|
107
|
+
if (typeof resolved === "number") {
|
|
108
|
+
style.letterSpacing = resolved;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return style;
|
|
112
|
+
}
|
|
113
|
+
var DIMENSION_PROPS = [
|
|
114
|
+
"width",
|
|
115
|
+
"height",
|
|
116
|
+
"minWidth",
|
|
117
|
+
"minHeight",
|
|
118
|
+
"maxWidth",
|
|
119
|
+
"maxHeight",
|
|
120
|
+
"top",
|
|
121
|
+
"right",
|
|
122
|
+
"bottom",
|
|
123
|
+
"left",
|
|
124
|
+
"marginTop",
|
|
125
|
+
"marginRight",
|
|
126
|
+
"marginBottom",
|
|
127
|
+
"marginLeft",
|
|
128
|
+
"paddingTop",
|
|
129
|
+
"paddingRight",
|
|
130
|
+
"paddingBottom",
|
|
131
|
+
"paddingLeft",
|
|
132
|
+
"rowGap",
|
|
133
|
+
"columnGap",
|
|
134
|
+
"flexBasis",
|
|
135
|
+
"borderTopWidth",
|
|
136
|
+
"borderRightWidth",
|
|
137
|
+
"borderBottomWidth",
|
|
138
|
+
"borderLeftWidth",
|
|
139
|
+
"borderTopLeftRadius",
|
|
140
|
+
"borderTopRightRadius",
|
|
141
|
+
"borderBottomRightRadius",
|
|
142
|
+
"borderBottomLeftRadius",
|
|
143
|
+
"WebkitTextStrokeWidth"
|
|
144
|
+
];
|
|
145
|
+
function resolveUnit(value, viewportWidth, viewportHeight, fontSize, rootFontSize) {
|
|
146
|
+
if (value.endsWith("%") || value === "auto") return value;
|
|
147
|
+
if (value.endsWith("vmin")) {
|
|
148
|
+
const n = parseFloat(value);
|
|
149
|
+
return isNaN(n) ? value : n / 100 * Math.min(viewportWidth, viewportHeight);
|
|
150
|
+
}
|
|
151
|
+
if (value.endsWith("vmax")) {
|
|
152
|
+
const n = parseFloat(value);
|
|
153
|
+
return isNaN(n) ? value : n / 100 * Math.max(viewportWidth, viewportHeight);
|
|
154
|
+
}
|
|
155
|
+
if (value.endsWith("vw")) {
|
|
156
|
+
const n = parseFloat(value);
|
|
157
|
+
return isNaN(n) ? value : n / 100 * viewportWidth;
|
|
158
|
+
}
|
|
159
|
+
if (value.endsWith("vh")) {
|
|
160
|
+
const n = parseFloat(value);
|
|
161
|
+
return isNaN(n) ? value : n / 100 * viewportHeight;
|
|
162
|
+
}
|
|
163
|
+
if (value.endsWith("rem")) {
|
|
164
|
+
const n = parseFloat(value);
|
|
165
|
+
return isNaN(n) ? value : n * rootFontSize;
|
|
166
|
+
}
|
|
167
|
+
if (value.endsWith("em")) {
|
|
168
|
+
const n = parseFloat(value);
|
|
169
|
+
return isNaN(n) ? value : n * fontSize;
|
|
170
|
+
}
|
|
171
|
+
if (value.endsWith("px")) {
|
|
172
|
+
const n = parseFloat(value);
|
|
173
|
+
return isNaN(n) ? value : n;
|
|
174
|
+
}
|
|
175
|
+
if (value.endsWith("pt")) {
|
|
176
|
+
const n = parseFloat(value);
|
|
177
|
+
return isNaN(n) ? value : n * (96 / 72);
|
|
178
|
+
}
|
|
179
|
+
if (value.endsWith("pc")) {
|
|
180
|
+
const n = parseFloat(value);
|
|
181
|
+
return isNaN(n) ? value : n * 16;
|
|
182
|
+
}
|
|
183
|
+
if (value.endsWith("in")) {
|
|
184
|
+
const n = parseFloat(value);
|
|
185
|
+
return isNaN(n) ? value : n * 96;
|
|
186
|
+
}
|
|
187
|
+
if (value.endsWith("cm")) {
|
|
188
|
+
const n = parseFloat(value);
|
|
189
|
+
return isNaN(n) ? value : n * (96 / 2.54);
|
|
190
|
+
}
|
|
191
|
+
if (value.endsWith("mm")) {
|
|
192
|
+
const n = parseFloat(value);
|
|
193
|
+
return isNaN(n) ? value : n * (96 / 25.4);
|
|
194
|
+
}
|
|
195
|
+
return value;
|
|
196
|
+
}
|
|
197
|
+
function resolveUnits(style, viewportWidth, viewportHeight, rootFontSize = ROOT_FONT_SIZE) {
|
|
198
|
+
const fontSize = typeof style.fontSize === "number" ? style.fontSize : rootFontSize;
|
|
199
|
+
for (const prop of DIMENSION_PROPS) {
|
|
200
|
+
const value = style[prop];
|
|
201
|
+
if (typeof value !== "string") continue;
|
|
202
|
+
const resolved = resolveUnit(
|
|
203
|
+
value,
|
|
204
|
+
viewportWidth,
|
|
205
|
+
viewportHeight,
|
|
206
|
+
fontSize,
|
|
207
|
+
rootFontSize
|
|
208
|
+
);
|
|
209
|
+
if (resolved !== value) {
|
|
210
|
+
style[prop] = resolved;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (style.transform) {
|
|
214
|
+
style.transform = resolveTransformUnits(
|
|
215
|
+
style.transform,
|
|
216
|
+
viewportWidth,
|
|
217
|
+
viewportHeight,
|
|
218
|
+
fontSize,
|
|
219
|
+
rootFontSize
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
if (style.transformOrigin) {
|
|
223
|
+
style.transformOrigin = resolveTransformUnits(
|
|
224
|
+
style.transformOrigin,
|
|
225
|
+
viewportWidth,
|
|
226
|
+
viewportHeight,
|
|
227
|
+
fontSize,
|
|
228
|
+
rootFontSize
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
return style;
|
|
232
|
+
}
|
|
233
|
+
function resolveTransformUnits(transform, viewportWidth, viewportHeight, fontSize, rootFontSize) {
|
|
234
|
+
return transform.replace(
|
|
235
|
+
/(-?\d*\.?\d+)(vw|vh|vmin|vmax|em|rem|px|pt|pc|in|cm|mm)\b/g,
|
|
236
|
+
(match) => {
|
|
237
|
+
const resolved = resolveUnit(
|
|
238
|
+
match,
|
|
239
|
+
viewportWidth,
|
|
240
|
+
viewportHeight,
|
|
241
|
+
fontSize,
|
|
242
|
+
rootFontSize
|
|
243
|
+
);
|
|
244
|
+
return typeof resolved === "number" ? String(resolved) : match;
|
|
245
|
+
}
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// src/jsx/font.ts
|
|
250
|
+
import { GlobalFonts } from "@napi-rs/canvas";
|
|
251
|
+
|
|
252
|
+
// src/jsx/font-metrics.ts
|
|
253
|
+
function parseFontMetrics(data) {
|
|
254
|
+
const buf = data instanceof ArrayBuffer ? data : data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
|
|
255
|
+
const view = new DataView(buf);
|
|
256
|
+
if (buf.byteLength < 12) return null;
|
|
257
|
+
const numTables = view.getUint16(4);
|
|
258
|
+
if (buf.byteLength < 12 + numTables * 16) return null;
|
|
259
|
+
let headOffset = -1;
|
|
260
|
+
let os2Offset = -1;
|
|
261
|
+
for (let i = 0; i < numTables; i++) {
|
|
262
|
+
const recordOffset = 12 + i * 16;
|
|
263
|
+
const tag = String.fromCharCode(view.getUint8(recordOffset)) + String.fromCharCode(view.getUint8(recordOffset + 1)) + String.fromCharCode(view.getUint8(recordOffset + 2)) + String.fromCharCode(view.getUint8(recordOffset + 3));
|
|
264
|
+
const offset = view.getUint32(recordOffset + 8);
|
|
265
|
+
if (tag === "head") headOffset = offset;
|
|
266
|
+
else if (tag === "OS/2") os2Offset = offset;
|
|
267
|
+
}
|
|
268
|
+
if (headOffset < 0 || os2Offset < 0) return null;
|
|
269
|
+
if (buf.byteLength < headOffset + 20) return null;
|
|
270
|
+
const unitsPerEm = view.getUint16(headOffset + 18);
|
|
271
|
+
if (buf.byteLength < os2Offset + 74) return null;
|
|
272
|
+
const sTypoAscender = view.getInt16(os2Offset + 68);
|
|
273
|
+
const sTypoDescender = view.getInt16(os2Offset + 70);
|
|
274
|
+
const sTypoLineGap = view.getInt16(os2Offset + 72);
|
|
275
|
+
return { unitsPerEm, sTypoAscender, sTypoDescender, sTypoLineGap };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// src/jsx/font.ts
|
|
279
|
+
var registeredFonts = /* @__PURE__ */ new Set();
|
|
280
|
+
var metricsCache = /* @__PURE__ */ new Map();
|
|
281
|
+
function registerFont(font) {
|
|
282
|
+
const key = `${font.name}:${font.weight}:${font.style}`;
|
|
283
|
+
if (registeredFonts.has(key)) return;
|
|
284
|
+
const buffer = Buffer.isBuffer(font.data) ? font.data : Buffer.from(font.data);
|
|
285
|
+
GlobalFonts.register(buffer, font.name);
|
|
286
|
+
const metrics = parseFontMetrics(font.data);
|
|
287
|
+
if (metrics) {
|
|
288
|
+
metricsCache.set(key, metrics);
|
|
289
|
+
}
|
|
290
|
+
registeredFonts.add(key);
|
|
291
|
+
}
|
|
292
|
+
function getFontMetrics(family, weight, style) {
|
|
293
|
+
const exact = metricsCache.get(`${family}:${weight}:${style}`);
|
|
294
|
+
if (exact) return exact;
|
|
295
|
+
for (const [key, metrics] of metricsCache) {
|
|
296
|
+
if (key.startsWith(`${family}:`)) return metrics;
|
|
297
|
+
}
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
function registerFontFromPath(path, nameAlias) {
|
|
301
|
+
GlobalFonts.registerFromPath(path, nameAlias ?? "");
|
|
302
|
+
}
|
|
303
|
+
function registeredFamilies() {
|
|
304
|
+
return GlobalFonts.families.map((f) => f.family);
|
|
305
|
+
}
|
|
306
|
+
function ensureFontsRegistered(fonts) {
|
|
307
|
+
for (const font of fonts) {
|
|
308
|
+
registerFont(font);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
26
312
|
// src/jsx/language.ts
|
|
27
313
|
function isEmoji(char) {
|
|
28
314
|
const cp = char.codePointAt(0);
|
|
@@ -110,12 +396,19 @@ function measureText(text, fontSize, fontFamily, fontWeight = 400, fontStyle = "
|
|
|
110
396
|
height: ascent + descent
|
|
111
397
|
};
|
|
112
398
|
}
|
|
113
|
-
function measureTrimMetrics(fontSize, fontFamily, fontWeight, fontStyle, lineHeight, edge, ctx) {
|
|
399
|
+
function measureTrimMetrics(fontSize, fontFamily, fontWeight, fontStyle, lineHeight, edge, ctx, fontMetrics) {
|
|
114
400
|
const c = ctx ?? getScratchCtx();
|
|
115
401
|
setFont(c, fontSize, fontFamily, fontWeight, fontStyle);
|
|
116
402
|
const refMetrics = c.measureText("M");
|
|
117
|
-
|
|
118
|
-
|
|
403
|
+
let fontAscent;
|
|
404
|
+
let fontDescent;
|
|
405
|
+
if (fontMetrics) {
|
|
406
|
+
fontAscent = fontMetrics.sTypoAscender / fontMetrics.unitsPerEm * fontSize;
|
|
407
|
+
fontDescent = -fontMetrics.sTypoDescender / fontMetrics.unitsPerEm * fontSize;
|
|
408
|
+
} else {
|
|
409
|
+
fontAscent = refMetrics.fontBoundingBoxAscent ?? refMetrics.actualBoundingBoxAscent ?? fontSize * 0.8;
|
|
410
|
+
fontDescent = refMetrics.fontBoundingBoxDescent ?? refMetrics.actualBoundingBoxDescent ?? fontSize * 0.2;
|
|
411
|
+
}
|
|
119
412
|
const parts = edge.trim().split(/\s+/);
|
|
120
413
|
const overEdge = parts[0] ?? "text";
|
|
121
414
|
const underEdge = parts.length > 1 ? parts[1] : overEdge;
|
|
@@ -230,16 +523,29 @@ function layoutText(text, style, maxWidth, ctx, emojiEnabled) {
|
|
|
230
523
|
fontStyle,
|
|
231
524
|
ctx
|
|
232
525
|
);
|
|
526
|
+
const fontMetrics = getFontMetrics(fontFamily, fontWeight, fontStyle);
|
|
233
527
|
const lineHeightPx = resolveLineHeight(
|
|
234
528
|
style.lineHeight,
|
|
235
529
|
fontSize,
|
|
236
|
-
refMetrics
|
|
530
|
+
refMetrics,
|
|
531
|
+
fontMetrics
|
|
237
532
|
);
|
|
238
533
|
const letterSpacing = typeof style.letterSpacing === "number" ? style.letterSpacing : 0;
|
|
239
534
|
const whiteSpace = style.whiteSpace ?? "normal";
|
|
240
535
|
const wordBreak = style.wordBreak ?? "normal";
|
|
241
536
|
const textOverflow = style.textOverflow ?? "clip";
|
|
242
537
|
const textDecoration = style.textDecoration;
|
|
538
|
+
let textStrokeWidth;
|
|
539
|
+
const rawStrokeWidth = style.WebkitTextStrokeWidth;
|
|
540
|
+
if (rawStrokeWidth !== void 0) {
|
|
541
|
+
if (typeof rawStrokeWidth === "number") {
|
|
542
|
+
textStrokeWidth = rawStrokeWidth;
|
|
543
|
+
} else {
|
|
544
|
+
const resolved = resolveUnit(String(rawStrokeWidth), 0, 0, fontSize, 16);
|
|
545
|
+
textStrokeWidth = typeof resolved === "number" ? resolved : void 0;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
const textStrokeColor = style.WebkitTextStrokeColor;
|
|
243
549
|
const measure = emojiEnabled ? (word, ls) => emojiAwareMeasureWord(
|
|
244
550
|
word,
|
|
245
551
|
fontSize,
|
|
@@ -338,10 +644,18 @@ function layoutText(text, style, maxWidth, ctx, emojiEnabled) {
|
|
|
338
644
|
fontStyle,
|
|
339
645
|
ctx
|
|
340
646
|
);
|
|
647
|
+
let baselineY;
|
|
648
|
+
if (fontMetrics) {
|
|
649
|
+
const typoAscent = fontMetrics.sTypoAscender / fontMetrics.unitsPerEm * fontSize;
|
|
650
|
+
const typoDescent = -fontMetrics.sTypoDescender / fontMetrics.unitsPerEm * fontSize;
|
|
651
|
+
baselineY = totalHeight + (lineHeightPx + typoAscent - typoDescent) / 2;
|
|
652
|
+
} else {
|
|
653
|
+
baselineY = totalHeight + (lineHeightPx + metrics.ascent - metrics.descent) / 2;
|
|
654
|
+
}
|
|
341
655
|
segments.push({
|
|
342
656
|
text: line,
|
|
343
657
|
x,
|
|
344
|
-
y:
|
|
658
|
+
y: baselineY,
|
|
345
659
|
width: lineWidth,
|
|
346
660
|
height: lineHeightPx,
|
|
347
661
|
fontSize,
|
|
@@ -352,7 +666,9 @@ function layoutText(text, style, maxWidth, ctx, emojiEnabled) {
|
|
|
352
666
|
ascent: metrics.ascent,
|
|
353
667
|
textDecoration,
|
|
354
668
|
letterSpacing,
|
|
355
|
-
lineIndex: i
|
|
669
|
+
lineIndex: i,
|
|
670
|
+
textStrokeWidth,
|
|
671
|
+
textStrokeColor
|
|
356
672
|
});
|
|
357
673
|
totalHeight += lineHeightPx;
|
|
358
674
|
maxLineWidth = Math.max(maxLineWidth, lineWidth);
|
|
@@ -367,7 +683,8 @@ function layoutText(text, style, maxWidth, ctx, emojiEnabled) {
|
|
|
367
683
|
fontStyle,
|
|
368
684
|
lineHeightPx,
|
|
369
685
|
textBoxEdge,
|
|
370
|
-
ctx
|
|
686
|
+
ctx,
|
|
687
|
+
fontMetrics
|
|
371
688
|
);
|
|
372
689
|
if (textBoxTrim === "trim-start" || textBoxTrim === "trim-both") {
|
|
373
690
|
for (const seg of segments) {
|
|
@@ -385,9 +702,13 @@ function layoutText(text, style, maxWidth, ctx, emojiEnabled) {
|
|
|
385
702
|
height: totalHeight
|
|
386
703
|
};
|
|
387
704
|
}
|
|
388
|
-
function resolveLineHeight(lineHeight, fontSize,
|
|
705
|
+
function resolveLineHeight(lineHeight, fontSize, canvasMetrics, fontMetrics) {
|
|
389
706
|
if (lineHeight === void 0 || lineHeight === "normal") {
|
|
390
|
-
|
|
707
|
+
if (fontMetrics) {
|
|
708
|
+
const { sTypoAscender, sTypoDescender, sTypoLineGap, unitsPerEm } = fontMetrics;
|
|
709
|
+
return (sTypoAscender - sTypoDescender + sTypoLineGap) / unitsPerEm * fontSize;
|
|
710
|
+
}
|
|
711
|
+
return canvasMetrics ? canvasMetrics.ascent + canvasMetrics.descent : fontSize * 1.2;
|
|
391
712
|
}
|
|
392
713
|
if (typeof lineHeight === "number") {
|
|
393
714
|
return lineHeight > 5 ? lineHeight : lineHeight * fontSize;
|
|
@@ -2083,14 +2404,42 @@ async function drawText(ctx, segments, offsetX, offsetY, textShadow, emojiStyle)
|
|
|
2083
2404
|
ctx.fillStyle = seg.color;
|
|
2084
2405
|
const x = offsetX + seg.x;
|
|
2085
2406
|
const y = offsetY + seg.y;
|
|
2407
|
+
const hasStroke = seg.textStrokeWidth !== void 0 && seg.textStrokeWidth > 0;
|
|
2086
2408
|
if (emojiStyle) {
|
|
2087
|
-
await drawSegmentWithEmoji(
|
|
2409
|
+
await drawSegmentWithEmoji(
|
|
2410
|
+
ctx,
|
|
2411
|
+
seg,
|
|
2412
|
+
x,
|
|
2413
|
+
y,
|
|
2414
|
+
textShadow,
|
|
2415
|
+
emojiStyle,
|
|
2416
|
+
hasStroke
|
|
2417
|
+
);
|
|
2088
2418
|
} else if (seg.letterSpacing && seg.letterSpacing !== 0) {
|
|
2419
|
+
if (hasStroke) {
|
|
2420
|
+
drawStrokeWithLetterSpacing(
|
|
2421
|
+
ctx,
|
|
2422
|
+
seg.text,
|
|
2423
|
+
x,
|
|
2424
|
+
y,
|
|
2425
|
+
seg.letterSpacing,
|
|
2426
|
+
seg.textStrokeWidth,
|
|
2427
|
+
seg.textStrokeColor ?? seg.color
|
|
2428
|
+
);
|
|
2429
|
+
}
|
|
2089
2430
|
drawTextWithLetterSpacing(ctx, seg.text, x, y, seg.letterSpacing);
|
|
2090
2431
|
} else {
|
|
2091
2432
|
if (textShadow) {
|
|
2092
2433
|
drawTextShadow(ctx, seg.text, x, y, textShadow);
|
|
2093
2434
|
}
|
|
2435
|
+
if (hasStroke) {
|
|
2436
|
+
ctx.save();
|
|
2437
|
+
ctx.lineWidth = seg.textStrokeWidth;
|
|
2438
|
+
ctx.strokeStyle = seg.textStrokeColor ?? seg.color;
|
|
2439
|
+
ctx.lineJoin = "round";
|
|
2440
|
+
ctx.strokeText(seg.text, x, y);
|
|
2441
|
+
ctx.restore();
|
|
2442
|
+
}
|
|
2094
2443
|
ctx.fillText(seg.text, x, y);
|
|
2095
2444
|
}
|
|
2096
2445
|
if (seg.textDecoration) {
|
|
@@ -2098,7 +2447,7 @@ async function drawText(ctx, segments, offsetX, offsetY, textShadow, emojiStyle)
|
|
|
2098
2447
|
}
|
|
2099
2448
|
}
|
|
2100
2449
|
}
|
|
2101
|
-
async function drawSegmentWithEmoji(ctx, seg, x, y, textShadow, emojiStyle) {
|
|
2450
|
+
async function drawSegmentWithEmoji(ctx, seg, x, y, textShadow, emojiStyle, hasStroke) {
|
|
2102
2451
|
const letterSpacing = seg.letterSpacing ?? 0;
|
|
2103
2452
|
const runs = splitTextIntoRuns(
|
|
2104
2453
|
seg.text,
|
|
@@ -2114,6 +2463,26 @@ async function drawSegmentWithEmoji(ctx, seg, x, y, textShadow, emojiStyle) {
|
|
|
2114
2463
|
if (textShadow) {
|
|
2115
2464
|
drawTextShadow(ctx, run.text, x + run.x, y, textShadow);
|
|
2116
2465
|
}
|
|
2466
|
+
if (hasStroke) {
|
|
2467
|
+
if (letterSpacing !== 0) {
|
|
2468
|
+
drawStrokeWithLetterSpacing(
|
|
2469
|
+
ctx,
|
|
2470
|
+
run.text,
|
|
2471
|
+
x + run.x,
|
|
2472
|
+
y,
|
|
2473
|
+
letterSpacing,
|
|
2474
|
+
seg.textStrokeWidth,
|
|
2475
|
+
seg.textStrokeColor ?? seg.color
|
|
2476
|
+
);
|
|
2477
|
+
} else {
|
|
2478
|
+
ctx.save();
|
|
2479
|
+
ctx.lineWidth = seg.textStrokeWidth;
|
|
2480
|
+
ctx.strokeStyle = seg.textStrokeColor ?? seg.color;
|
|
2481
|
+
ctx.lineJoin = "round";
|
|
2482
|
+
ctx.strokeText(run.text, x + run.x, y);
|
|
2483
|
+
ctx.restore();
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2117
2486
|
if (letterSpacing !== 0) {
|
|
2118
2487
|
drawTextWithLetterSpacing(ctx, run.text, x + run.x, y, letterSpacing);
|
|
2119
2488
|
} else {
|
|
@@ -2139,6 +2508,19 @@ function drawTextWithLetterSpacing(ctx, text, x, y, letterSpacing) {
|
|
|
2139
2508
|
currentX += metrics.width + letterSpacing;
|
|
2140
2509
|
}
|
|
2141
2510
|
}
|
|
2511
|
+
function drawStrokeWithLetterSpacing(ctx, text, x, y, letterSpacing, strokeWidth, strokeColor) {
|
|
2512
|
+
ctx.save();
|
|
2513
|
+
ctx.lineWidth = strokeWidth;
|
|
2514
|
+
ctx.strokeStyle = strokeColor;
|
|
2515
|
+
ctx.lineJoin = "round";
|
|
2516
|
+
let currentX = x;
|
|
2517
|
+
for (const char of text) {
|
|
2518
|
+
ctx.strokeText(char, currentX, y);
|
|
2519
|
+
const metrics = ctx.measureText(char);
|
|
2520
|
+
currentX += metrics.width + letterSpacing;
|
|
2521
|
+
}
|
|
2522
|
+
ctx.restore();
|
|
2523
|
+
}
|
|
2142
2524
|
function drawTextShadow(ctx, text, x, y, shadow) {
|
|
2143
2525
|
const parts = shadow.match(
|
|
2144
2526
|
/(-?\d+(?:\.\d+)?)\s*px?\s+(-?\d+(?:\.\d+)?)\s*px?\s+(-?\d+(?:\.\d+)?)\s*px?\s+(.*)/
|
|
@@ -2524,28 +2906,6 @@ function parseAngle(value) {
|
|
|
2524
2906
|
return parseFloat(value);
|
|
2525
2907
|
}
|
|
2526
2908
|
|
|
2527
|
-
// src/jsx/font.ts
|
|
2528
|
-
import { GlobalFonts } from "@napi-rs/canvas";
|
|
2529
|
-
var registeredFonts = /* @__PURE__ */ new Set();
|
|
2530
|
-
function registerFont(font) {
|
|
2531
|
-
const key = `${font.name}:${font.weight}:${font.style}`;
|
|
2532
|
-
if (registeredFonts.has(key)) return;
|
|
2533
|
-
const buffer = Buffer.isBuffer(font.data) ? font.data : Buffer.from(font.data);
|
|
2534
|
-
GlobalFonts.register(buffer, font.name);
|
|
2535
|
-
registeredFonts.add(key);
|
|
2536
|
-
}
|
|
2537
|
-
function registerFontFromPath(path, nameAlias) {
|
|
2538
|
-
GlobalFonts.registerFromPath(path, nameAlias ?? "");
|
|
2539
|
-
}
|
|
2540
|
-
function registeredFamilies() {
|
|
2541
|
-
return GlobalFonts.families.map((f) => f.family);
|
|
2542
|
-
}
|
|
2543
|
-
function ensureFontsRegistered(fonts) {
|
|
2544
|
-
for (const font of fonts) {
|
|
2545
|
-
registerFont(font);
|
|
2546
|
-
}
|
|
2547
|
-
}
|
|
2548
|
-
|
|
2549
2909
|
// src/jsx/layout.ts
|
|
2550
2910
|
import { loadImage as loadImage4 } from "@napi-rs/canvas";
|
|
2551
2911
|
|
|
@@ -2720,6 +3080,20 @@ function expandStyle(raw, fontFamilies) {
|
|
|
2720
3080
|
}
|
|
2721
3081
|
delete style.textBox;
|
|
2722
3082
|
}
|
|
3083
|
+
if (style.WebkitTextStroke !== void 0) {
|
|
3084
|
+
const val = String(style.WebkitTextStroke).trim();
|
|
3085
|
+
const match = val.match(/^(\S+)\s+(.+)$/);
|
|
3086
|
+
if (match) {
|
|
3087
|
+
if (style.WebkitTextStrokeWidth === void 0)
|
|
3088
|
+
style.WebkitTextStrokeWidth = parseValue(match[1]);
|
|
3089
|
+
if (style.WebkitTextStrokeColor === void 0)
|
|
3090
|
+
style.WebkitTextStrokeColor = match[2];
|
|
3091
|
+
} else {
|
|
3092
|
+
if (style.WebkitTextStrokeWidth === void 0)
|
|
3093
|
+
style.WebkitTextStrokeWidth = parseValue(val);
|
|
3094
|
+
}
|
|
3095
|
+
delete style.WebkitTextStroke;
|
|
3096
|
+
}
|
|
2723
3097
|
if (typeof style.fontFamily === "string") {
|
|
2724
3098
|
const families = style.fontFamily.split(",").map((s) => s.trim().replace(/^['"]|['"]$/g, "")).filter(Boolean);
|
|
2725
3099
|
if (fontFamilies) {
|
|
@@ -2733,226 +3107,6 @@ function expandStyle(raw, fontFamilies) {
|
|
|
2733
3107
|
return style;
|
|
2734
3108
|
}
|
|
2735
3109
|
|
|
2736
|
-
// src/jsx/style/compute.ts
|
|
2737
|
-
var ROOT_FONT_SIZE = 16;
|
|
2738
|
-
var DEFAULT_STYLE = {
|
|
2739
|
-
display: "flex",
|
|
2740
|
-
flexDirection: "row",
|
|
2741
|
-
flexWrap: "nowrap",
|
|
2742
|
-
flexGrow: 0,
|
|
2743
|
-
flexShrink: 0,
|
|
2744
|
-
alignItems: "stretch",
|
|
2745
|
-
justifyContent: "flex-start",
|
|
2746
|
-
position: "relative",
|
|
2747
|
-
fontSize: ROOT_FONT_SIZE,
|
|
2748
|
-
fontWeight: 400,
|
|
2749
|
-
fontStyle: "normal",
|
|
2750
|
-
color: "black",
|
|
2751
|
-
lineHeight: "normal",
|
|
2752
|
-
textAlign: "left",
|
|
2753
|
-
whiteSpace: "normal",
|
|
2754
|
-
wordBreak: "normal",
|
|
2755
|
-
textOverflow: "clip",
|
|
2756
|
-
opacity: 1,
|
|
2757
|
-
overflow: "visible"
|
|
2758
|
-
};
|
|
2759
|
-
var INHERITABLE_PROPS = [
|
|
2760
|
-
"color",
|
|
2761
|
-
"fontSize",
|
|
2762
|
-
"fontFamily",
|
|
2763
|
-
"fontWeight",
|
|
2764
|
-
"fontStyle",
|
|
2765
|
-
"textAlign",
|
|
2766
|
-
"textTransform",
|
|
2767
|
-
"textDecoration",
|
|
2768
|
-
"lineHeight",
|
|
2769
|
-
"letterSpacing",
|
|
2770
|
-
"whiteSpace",
|
|
2771
|
-
"wordBreak",
|
|
2772
|
-
"textOverflow",
|
|
2773
|
-
"lineClamp",
|
|
2774
|
-
"textBoxTrim",
|
|
2775
|
-
"textBoxEdge"
|
|
2776
|
-
];
|
|
2777
|
-
function resolveStyle(rawStyle, parentStyle) {
|
|
2778
|
-
const style = { ...rawStyle };
|
|
2779
|
-
for (const prop of INHERITABLE_PROPS) {
|
|
2780
|
-
if (style[prop] === void 0 && parentStyle[prop] !== void 0) {
|
|
2781
|
-
style[prop] = parentStyle[prop];
|
|
2782
|
-
}
|
|
2783
|
-
}
|
|
2784
|
-
if (typeof style.fontSize === "string") {
|
|
2785
|
-
const parentFontSize = typeof parentStyle.fontSize === "number" ? parentStyle.fontSize : ROOT_FONT_SIZE;
|
|
2786
|
-
const resolved = resolveUnit(
|
|
2787
|
-
style.fontSize,
|
|
2788
|
-
0,
|
|
2789
|
-
0,
|
|
2790
|
-
parentFontSize,
|
|
2791
|
-
ROOT_FONT_SIZE
|
|
2792
|
-
);
|
|
2793
|
-
style.fontSize = typeof resolved === "number" ? resolved : parentFontSize;
|
|
2794
|
-
}
|
|
2795
|
-
if (typeof style.lineHeight === "string") {
|
|
2796
|
-
const str = style.lineHeight;
|
|
2797
|
-
if (str.endsWith("%")) {
|
|
2798
|
-
style.lineHeight = parseFloat(str) / 100;
|
|
2799
|
-
} else {
|
|
2800
|
-
const parsed = parseFloat(str);
|
|
2801
|
-
if (!isNaN(parsed)) {
|
|
2802
|
-
style.lineHeight = parsed;
|
|
2803
|
-
}
|
|
2804
|
-
}
|
|
2805
|
-
}
|
|
2806
|
-
if (typeof style.letterSpacing === "string") {
|
|
2807
|
-
const currentFontSize = typeof style.fontSize === "number" ? style.fontSize : typeof parentStyle.fontSize === "number" ? parentStyle.fontSize : ROOT_FONT_SIZE;
|
|
2808
|
-
const resolved = resolveUnit(
|
|
2809
|
-
style.letterSpacing,
|
|
2810
|
-
0,
|
|
2811
|
-
0,
|
|
2812
|
-
currentFontSize,
|
|
2813
|
-
ROOT_FONT_SIZE
|
|
2814
|
-
);
|
|
2815
|
-
if (typeof resolved === "number") {
|
|
2816
|
-
style.letterSpacing = resolved;
|
|
2817
|
-
}
|
|
2818
|
-
}
|
|
2819
|
-
return style;
|
|
2820
|
-
}
|
|
2821
|
-
var DIMENSION_PROPS = [
|
|
2822
|
-
"width",
|
|
2823
|
-
"height",
|
|
2824
|
-
"minWidth",
|
|
2825
|
-
"minHeight",
|
|
2826
|
-
"maxWidth",
|
|
2827
|
-
"maxHeight",
|
|
2828
|
-
"top",
|
|
2829
|
-
"right",
|
|
2830
|
-
"bottom",
|
|
2831
|
-
"left",
|
|
2832
|
-
"marginTop",
|
|
2833
|
-
"marginRight",
|
|
2834
|
-
"marginBottom",
|
|
2835
|
-
"marginLeft",
|
|
2836
|
-
"paddingTop",
|
|
2837
|
-
"paddingRight",
|
|
2838
|
-
"paddingBottom",
|
|
2839
|
-
"paddingLeft",
|
|
2840
|
-
"rowGap",
|
|
2841
|
-
"columnGap",
|
|
2842
|
-
"flexBasis",
|
|
2843
|
-
"borderTopWidth",
|
|
2844
|
-
"borderRightWidth",
|
|
2845
|
-
"borderBottomWidth",
|
|
2846
|
-
"borderLeftWidth",
|
|
2847
|
-
"borderTopLeftRadius",
|
|
2848
|
-
"borderTopRightRadius",
|
|
2849
|
-
"borderBottomRightRadius",
|
|
2850
|
-
"borderBottomLeftRadius"
|
|
2851
|
-
];
|
|
2852
|
-
function resolveUnit(value, viewportWidth, viewportHeight, fontSize, rootFontSize) {
|
|
2853
|
-
if (value.endsWith("%") || value === "auto") return value;
|
|
2854
|
-
if (value.endsWith("vmin")) {
|
|
2855
|
-
const n = parseFloat(value);
|
|
2856
|
-
return isNaN(n) ? value : n / 100 * Math.min(viewportWidth, viewportHeight);
|
|
2857
|
-
}
|
|
2858
|
-
if (value.endsWith("vmax")) {
|
|
2859
|
-
const n = parseFloat(value);
|
|
2860
|
-
return isNaN(n) ? value : n / 100 * Math.max(viewportWidth, viewportHeight);
|
|
2861
|
-
}
|
|
2862
|
-
if (value.endsWith("vw")) {
|
|
2863
|
-
const n = parseFloat(value);
|
|
2864
|
-
return isNaN(n) ? value : n / 100 * viewportWidth;
|
|
2865
|
-
}
|
|
2866
|
-
if (value.endsWith("vh")) {
|
|
2867
|
-
const n = parseFloat(value);
|
|
2868
|
-
return isNaN(n) ? value : n / 100 * viewportHeight;
|
|
2869
|
-
}
|
|
2870
|
-
if (value.endsWith("rem")) {
|
|
2871
|
-
const n = parseFloat(value);
|
|
2872
|
-
return isNaN(n) ? value : n * rootFontSize;
|
|
2873
|
-
}
|
|
2874
|
-
if (value.endsWith("em")) {
|
|
2875
|
-
const n = parseFloat(value);
|
|
2876
|
-
return isNaN(n) ? value : n * fontSize;
|
|
2877
|
-
}
|
|
2878
|
-
if (value.endsWith("px")) {
|
|
2879
|
-
const n = parseFloat(value);
|
|
2880
|
-
return isNaN(n) ? value : n;
|
|
2881
|
-
}
|
|
2882
|
-
if (value.endsWith("pt")) {
|
|
2883
|
-
const n = parseFloat(value);
|
|
2884
|
-
return isNaN(n) ? value : n * (96 / 72);
|
|
2885
|
-
}
|
|
2886
|
-
if (value.endsWith("pc")) {
|
|
2887
|
-
const n = parseFloat(value);
|
|
2888
|
-
return isNaN(n) ? value : n * 16;
|
|
2889
|
-
}
|
|
2890
|
-
if (value.endsWith("in")) {
|
|
2891
|
-
const n = parseFloat(value);
|
|
2892
|
-
return isNaN(n) ? value : n * 96;
|
|
2893
|
-
}
|
|
2894
|
-
if (value.endsWith("cm")) {
|
|
2895
|
-
const n = parseFloat(value);
|
|
2896
|
-
return isNaN(n) ? value : n * (96 / 2.54);
|
|
2897
|
-
}
|
|
2898
|
-
if (value.endsWith("mm")) {
|
|
2899
|
-
const n = parseFloat(value);
|
|
2900
|
-
return isNaN(n) ? value : n * (96 / 25.4);
|
|
2901
|
-
}
|
|
2902
|
-
return value;
|
|
2903
|
-
}
|
|
2904
|
-
function resolveUnits(style, viewportWidth, viewportHeight, rootFontSize = ROOT_FONT_SIZE) {
|
|
2905
|
-
const fontSize = typeof style.fontSize === "number" ? style.fontSize : rootFontSize;
|
|
2906
|
-
for (const prop of DIMENSION_PROPS) {
|
|
2907
|
-
const value = style[prop];
|
|
2908
|
-
if (typeof value !== "string") continue;
|
|
2909
|
-
const resolved = resolveUnit(
|
|
2910
|
-
value,
|
|
2911
|
-
viewportWidth,
|
|
2912
|
-
viewportHeight,
|
|
2913
|
-
fontSize,
|
|
2914
|
-
rootFontSize
|
|
2915
|
-
);
|
|
2916
|
-
if (resolved !== value) {
|
|
2917
|
-
style[prop] = resolved;
|
|
2918
|
-
}
|
|
2919
|
-
}
|
|
2920
|
-
if (style.transform) {
|
|
2921
|
-
style.transform = resolveTransformUnits(
|
|
2922
|
-
style.transform,
|
|
2923
|
-
viewportWidth,
|
|
2924
|
-
viewportHeight,
|
|
2925
|
-
fontSize,
|
|
2926
|
-
rootFontSize
|
|
2927
|
-
);
|
|
2928
|
-
}
|
|
2929
|
-
if (style.transformOrigin) {
|
|
2930
|
-
style.transformOrigin = resolveTransformUnits(
|
|
2931
|
-
style.transformOrigin,
|
|
2932
|
-
viewportWidth,
|
|
2933
|
-
viewportHeight,
|
|
2934
|
-
fontSize,
|
|
2935
|
-
rootFontSize
|
|
2936
|
-
);
|
|
2937
|
-
}
|
|
2938
|
-
return style;
|
|
2939
|
-
}
|
|
2940
|
-
function resolveTransformUnits(transform, viewportWidth, viewportHeight, fontSize, rootFontSize) {
|
|
2941
|
-
return transform.replace(
|
|
2942
|
-
/(-?\d*\.?\d+)(vw|vh|vmin|vmax|em|rem|px|pt|pc|in|cm|mm)\b/g,
|
|
2943
|
-
(match) => {
|
|
2944
|
-
const resolved = resolveUnit(
|
|
2945
|
-
match,
|
|
2946
|
-
viewportWidth,
|
|
2947
|
-
viewportHeight,
|
|
2948
|
-
fontSize,
|
|
2949
|
-
rootFontSize
|
|
2950
|
-
);
|
|
2951
|
-
return typeof resolved === "number" ? String(resolved) : match;
|
|
2952
|
-
}
|
|
2953
|
-
);
|
|
2954
|
-
}
|
|
2955
|
-
|
|
2956
3110
|
// src/jsx/yoga.ts
|
|
2957
3111
|
import Yoga, {
|
|
2958
3112
|
Align,
|
|
@@ -3336,14 +3490,22 @@ async function buildNode(element, parentStyle, yogaNode, viewportWidth, viewport
|
|
|
3336
3490
|
const rawChildren = props.children;
|
|
3337
3491
|
if (rawChildren !== void 0 && rawChildren !== null) {
|
|
3338
3492
|
const childArray = Array.isArray(rawChildren) ? rawChildren.flat(Infinity) : [rawChildren];
|
|
3493
|
+
const ws = style.whiteSpace;
|
|
3494
|
+
const collapsesWhitespace = ws === void 0 || ws === "normal" || ws === "nowrap" || ws === "pre-line";
|
|
3495
|
+
let prevWasBr = false;
|
|
3339
3496
|
for (const child of childArray) {
|
|
3340
3497
|
if (child === null || child === void 0 || typeof child === "boolean")
|
|
3341
3498
|
continue;
|
|
3499
|
+
let processedChild = child;
|
|
3500
|
+
if (prevWasBr && collapsesWhitespace && typeof child === "string") {
|
|
3501
|
+
processedChild = child.trimStart();
|
|
3502
|
+
}
|
|
3503
|
+
prevWasBr = isBrElement(child);
|
|
3342
3504
|
const childYogaNode = createYogaNode();
|
|
3343
3505
|
yogaNode.insertChild(childYogaNode, children.length);
|
|
3344
3506
|
children.push(
|
|
3345
3507
|
await buildNode(
|
|
3346
|
-
|
|
3508
|
+
processedChild,
|
|
3347
3509
|
style,
|
|
3348
3510
|
childYogaNode,
|
|
3349
3511
|
viewportWidth,
|
|
@@ -3380,6 +3542,11 @@ function extractLayout(node, yogaNode) {
|
|
|
3380
3542
|
height: layout.height
|
|
3381
3543
|
};
|
|
3382
3544
|
}
|
|
3545
|
+
function isBrElement(child) {
|
|
3546
|
+
if (child === null || child === void 0 || typeof child !== "object")
|
|
3547
|
+
return false;
|
|
3548
|
+
return child.type === "br";
|
|
3549
|
+
}
|
|
3383
3550
|
function extractTextContent(children) {
|
|
3384
3551
|
if (children === void 0 || children === null) return void 0;
|
|
3385
3552
|
if (typeof children === "string") return children;
|