@effing/canvas 0.1.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 +11 -0
- package/README.md +283 -0
- package/dist/index.d.ts +119 -0
- package/dist/index.js +2070 -0
- package/dist/index.js.map +1 -0
- package/package.json +66 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2070 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { createCanvas as _createCanvas } from "@napi-rs/canvas";
|
|
3
|
+
import {
|
|
4
|
+
Canvas,
|
|
5
|
+
GlobalFonts as GlobalFonts2,
|
|
6
|
+
loadImage as loadImage4,
|
|
7
|
+
Image
|
|
8
|
+
} from "@napi-rs/canvas";
|
|
9
|
+
|
|
10
|
+
// src/lottie.ts
|
|
11
|
+
import { LottieAnimation } from "@napi-rs/canvas";
|
|
12
|
+
function loadLottie(data, options) {
|
|
13
|
+
const jsonString = typeof data === "string" ? data : data.toString("utf-8");
|
|
14
|
+
return LottieAnimation.loadFromData(jsonString, {
|
|
15
|
+
resourcePath: options?.resourcePath
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
function renderLottieFrame(ctx, animation, frame) {
|
|
19
|
+
animation.seekFrame(frame);
|
|
20
|
+
animation.render(ctx);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// src/jsx/draw/index.ts
|
|
24
|
+
import { loadImage as loadImage3 } from "@napi-rs/canvas";
|
|
25
|
+
|
|
26
|
+
// src/jsx/language.ts
|
|
27
|
+
function isEmoji(char) {
|
|
28
|
+
const cp = char.codePointAt(0);
|
|
29
|
+
if (cp === void 0) return false;
|
|
30
|
+
if (cp >= 128512 && cp <= 128591) return true;
|
|
31
|
+
if (cp >= 127744 && cp <= 128511) return true;
|
|
32
|
+
if (cp >= 128640 && cp <= 128767) return true;
|
|
33
|
+
if (cp >= 129280 && cp <= 129535) return true;
|
|
34
|
+
if (cp >= 9728 && cp <= 9983) return true;
|
|
35
|
+
if (cp >= 9984 && cp <= 10175) return true;
|
|
36
|
+
if (cp >= 11088 && cp <= 11093) return true;
|
|
37
|
+
if (cp >= 8205 && cp <= 8205) return true;
|
|
38
|
+
if (cp >= 65024 && cp <= 65039) return true;
|
|
39
|
+
if (cp >= 129536 && cp <= 129647) return true;
|
|
40
|
+
if (cp >= 129648 && cp <= 129791) return true;
|
|
41
|
+
if (cp >= 8986 && cp <= 9203) return true;
|
|
42
|
+
if (cp >= 9193 && cp <= 9210) return true;
|
|
43
|
+
if (cp >= 9642 && cp <= 9726) return true;
|
|
44
|
+
if (cp >= 10548 && cp <= 10549) return true;
|
|
45
|
+
if (cp >= 11013 && cp <= 11015) return true;
|
|
46
|
+
if (cp >= 12336 && cp <= 12336) return true;
|
|
47
|
+
if (cp >= 12349 && cp <= 12349) return true;
|
|
48
|
+
if (cp >= 12951 && cp <= 12953) return true;
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// src/jsx/text/linebreak.ts
|
|
53
|
+
import LineBreaker from "linebreak";
|
|
54
|
+
function findBreakOpportunities(text) {
|
|
55
|
+
const breaker = new LineBreaker(text);
|
|
56
|
+
const opportunities = [];
|
|
57
|
+
let bk = breaker.nextBreak();
|
|
58
|
+
while (bk) {
|
|
59
|
+
opportunities.push({
|
|
60
|
+
position: bk.position,
|
|
61
|
+
required: bk.required ?? false
|
|
62
|
+
});
|
|
63
|
+
bk = breaker.nextBreak();
|
|
64
|
+
}
|
|
65
|
+
return opportunities;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// src/jsx/text/measure.ts
|
|
69
|
+
import { createCanvas } from "@napi-rs/canvas";
|
|
70
|
+
var scratchCtx = null;
|
|
71
|
+
function getScratchCtx() {
|
|
72
|
+
if (!scratchCtx) {
|
|
73
|
+
scratchCtx = createCanvas(1, 1).getContext("2d");
|
|
74
|
+
}
|
|
75
|
+
return scratchCtx;
|
|
76
|
+
}
|
|
77
|
+
function setFont(ctx, fontSize, fontFamily, fontWeight = 400, fontStyle = "normal") {
|
|
78
|
+
ctx.font = `${fontStyle} ${fontWeight} ${fontSize}px ${fontFamily}`;
|
|
79
|
+
}
|
|
80
|
+
function measureText(text, fontSize, fontFamily, fontWeight = 400, fontStyle = "normal", ctx) {
|
|
81
|
+
const c = ctx ?? getScratchCtx();
|
|
82
|
+
setFont(c, fontSize, fontFamily, fontWeight, fontStyle);
|
|
83
|
+
const m = c.measureText(text);
|
|
84
|
+
const ascent = m.fontBoundingBoxAscent ?? m.actualBoundingBoxAscent ?? fontSize * 0.8;
|
|
85
|
+
const descent = m.fontBoundingBoxDescent ?? m.actualBoundingBoxDescent ?? fontSize * 0.2;
|
|
86
|
+
return {
|
|
87
|
+
width: m.width,
|
|
88
|
+
ascent,
|
|
89
|
+
descent,
|
|
90
|
+
height: ascent + descent
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function measureWord(word, fontSize, fontFamily, fontWeight = 400, fontStyle = "normal", ctx, letterSpacing = 0) {
|
|
94
|
+
const base = measureText(
|
|
95
|
+
word,
|
|
96
|
+
fontSize,
|
|
97
|
+
fontFamily,
|
|
98
|
+
fontWeight,
|
|
99
|
+
fontStyle,
|
|
100
|
+
ctx
|
|
101
|
+
).width;
|
|
102
|
+
if (letterSpacing === 0 || word.length === 0) return base;
|
|
103
|
+
return base + letterSpacing * word.length;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// src/jsx/text/index.ts
|
|
107
|
+
function emojiAwareMeasureWord(word, fontSize, fontFamily, fontWeight, fontStyle, ctx, letterSpacing = 0) {
|
|
108
|
+
const segmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
|
|
109
|
+
let totalWidth = 0;
|
|
110
|
+
let textBuffer = "";
|
|
111
|
+
for (const { segment } of segmenter.segment(word)) {
|
|
112
|
+
let isEmojiSegment = false;
|
|
113
|
+
for (const char of segment) {
|
|
114
|
+
if (isEmoji(char)) {
|
|
115
|
+
isEmojiSegment = true;
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (isEmojiSegment) {
|
|
120
|
+
if (textBuffer) {
|
|
121
|
+
totalWidth += measureWord(
|
|
122
|
+
textBuffer,
|
|
123
|
+
fontSize,
|
|
124
|
+
fontFamily,
|
|
125
|
+
fontWeight,
|
|
126
|
+
fontStyle,
|
|
127
|
+
ctx,
|
|
128
|
+
letterSpacing
|
|
129
|
+
);
|
|
130
|
+
textBuffer = "";
|
|
131
|
+
}
|
|
132
|
+
totalWidth += fontSize;
|
|
133
|
+
} else {
|
|
134
|
+
textBuffer += segment;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (textBuffer) {
|
|
138
|
+
totalWidth += measureWord(
|
|
139
|
+
textBuffer,
|
|
140
|
+
fontSize,
|
|
141
|
+
fontFamily,
|
|
142
|
+
fontWeight,
|
|
143
|
+
fontStyle,
|
|
144
|
+
ctx,
|
|
145
|
+
letterSpacing
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
return totalWidth;
|
|
149
|
+
}
|
|
150
|
+
function layoutText(text, style, maxWidth, ctx, emojiEnabled) {
|
|
151
|
+
const fontSize = style.fontSize ?? 16;
|
|
152
|
+
const fontFamily = style.fontFamily ?? "sans-serif";
|
|
153
|
+
const fontWeight = style.fontWeight ?? 400;
|
|
154
|
+
const fontStyle = style.fontStyle ?? "normal";
|
|
155
|
+
const color = style.color ?? "black";
|
|
156
|
+
const textAlign = style.textAlign ?? "left";
|
|
157
|
+
const refMetrics = measureText(
|
|
158
|
+
"M",
|
|
159
|
+
fontSize,
|
|
160
|
+
fontFamily,
|
|
161
|
+
fontWeight,
|
|
162
|
+
fontStyle,
|
|
163
|
+
ctx
|
|
164
|
+
);
|
|
165
|
+
const lineHeightPx = resolveLineHeight(
|
|
166
|
+
style.lineHeight,
|
|
167
|
+
fontSize,
|
|
168
|
+
refMetrics
|
|
169
|
+
);
|
|
170
|
+
const letterSpacing = typeof style.letterSpacing === "number" ? style.letterSpacing : 0;
|
|
171
|
+
const whiteSpace = style.whiteSpace ?? "normal";
|
|
172
|
+
const wordBreak = style.wordBreak ?? "normal";
|
|
173
|
+
const textOverflow = style.textOverflow ?? "clip";
|
|
174
|
+
const textDecoration = style.textDecoration;
|
|
175
|
+
const measure = emojiEnabled ? (word, ls) => emojiAwareMeasureWord(
|
|
176
|
+
word,
|
|
177
|
+
fontSize,
|
|
178
|
+
fontFamily,
|
|
179
|
+
fontWeight,
|
|
180
|
+
fontStyle,
|
|
181
|
+
ctx,
|
|
182
|
+
ls ?? letterSpacing
|
|
183
|
+
) : (word, ls) => measureWord(
|
|
184
|
+
word,
|
|
185
|
+
fontSize,
|
|
186
|
+
fontFamily,
|
|
187
|
+
fontWeight,
|
|
188
|
+
fontStyle,
|
|
189
|
+
ctx,
|
|
190
|
+
ls ?? letterSpacing
|
|
191
|
+
);
|
|
192
|
+
let processedText = text;
|
|
193
|
+
if (style.textTransform === "uppercase") {
|
|
194
|
+
processedText = text.toUpperCase();
|
|
195
|
+
} else if (style.textTransform === "lowercase") {
|
|
196
|
+
processedText = text.toLowerCase();
|
|
197
|
+
} else if (style.textTransform === "capitalize") {
|
|
198
|
+
processedText = text.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
199
|
+
}
|
|
200
|
+
const noWrap = whiteSpace === "nowrap" || whiteSpace === "pre";
|
|
201
|
+
const preserveWhitespace = whiteSpace === "pre" || whiteSpace === "pre-wrap";
|
|
202
|
+
const paragraphs = preserveWhitespace ? processedText.split("\n") : processedText.split("\n");
|
|
203
|
+
const lines = [];
|
|
204
|
+
for (const paragraph of paragraphs) {
|
|
205
|
+
if (noWrap) {
|
|
206
|
+
lines.push(paragraph);
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
const wrapped = wrapText(
|
|
210
|
+
paragraph,
|
|
211
|
+
maxWidth,
|
|
212
|
+
fontSize,
|
|
213
|
+
fontFamily,
|
|
214
|
+
fontWeight,
|
|
215
|
+
fontStyle,
|
|
216
|
+
letterSpacing,
|
|
217
|
+
wordBreak,
|
|
218
|
+
ctx,
|
|
219
|
+
measure
|
|
220
|
+
);
|
|
221
|
+
lines.push(...wrapped);
|
|
222
|
+
}
|
|
223
|
+
if (textOverflow === "ellipsis" && noWrap && lines.length === 1) {
|
|
224
|
+
const line = lines[0];
|
|
225
|
+
const lineWidth = measure(line);
|
|
226
|
+
if (lineWidth > maxWidth) {
|
|
227
|
+
lines[0] = truncateWithEllipsis(
|
|
228
|
+
line,
|
|
229
|
+
maxWidth,
|
|
230
|
+
fontSize,
|
|
231
|
+
fontFamily,
|
|
232
|
+
fontWeight,
|
|
233
|
+
fontStyle,
|
|
234
|
+
ctx,
|
|
235
|
+
letterSpacing
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
const segments = [];
|
|
240
|
+
let totalHeight = 0;
|
|
241
|
+
let maxLineWidth = 0;
|
|
242
|
+
for (let i = 0; i < lines.length; i++) {
|
|
243
|
+
const line = lines[i];
|
|
244
|
+
const lineWidth = measure(line);
|
|
245
|
+
let x = 0;
|
|
246
|
+
if (textAlign === "center") {
|
|
247
|
+
x = (maxWidth - lineWidth) / 2;
|
|
248
|
+
} else if (textAlign === "right") {
|
|
249
|
+
x = maxWidth - lineWidth;
|
|
250
|
+
}
|
|
251
|
+
const metrics = measureText(
|
|
252
|
+
line || "M",
|
|
253
|
+
fontSize,
|
|
254
|
+
fontFamily,
|
|
255
|
+
fontWeight,
|
|
256
|
+
fontStyle,
|
|
257
|
+
ctx
|
|
258
|
+
);
|
|
259
|
+
segments.push({
|
|
260
|
+
text: line,
|
|
261
|
+
x,
|
|
262
|
+
y: totalHeight + (lineHeightPx + metrics.ascent - metrics.descent) / 2,
|
|
263
|
+
width: lineWidth,
|
|
264
|
+
height: lineHeightPx,
|
|
265
|
+
fontSize,
|
|
266
|
+
fontFamily,
|
|
267
|
+
fontWeight,
|
|
268
|
+
fontStyle,
|
|
269
|
+
color,
|
|
270
|
+
ascent: metrics.ascent,
|
|
271
|
+
textDecoration,
|
|
272
|
+
letterSpacing,
|
|
273
|
+
lineIndex: i
|
|
274
|
+
});
|
|
275
|
+
totalHeight += lineHeightPx;
|
|
276
|
+
maxLineWidth = Math.max(maxLineWidth, lineWidth);
|
|
277
|
+
}
|
|
278
|
+
return {
|
|
279
|
+
segments,
|
|
280
|
+
width: maxLineWidth,
|
|
281
|
+
height: totalHeight
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
function resolveLineHeight(lineHeight, fontSize, metrics) {
|
|
285
|
+
if (lineHeight === void 0 || lineHeight === "normal") {
|
|
286
|
+
return metrics ? metrics.ascent + metrics.descent : fontSize * 1.2;
|
|
287
|
+
}
|
|
288
|
+
if (typeof lineHeight === "number") {
|
|
289
|
+
return lineHeight > 5 ? lineHeight : lineHeight * fontSize;
|
|
290
|
+
}
|
|
291
|
+
const parsed = parseFloat(String(lineHeight));
|
|
292
|
+
return isNaN(parsed) ? fontSize * 1.2 : parsed;
|
|
293
|
+
}
|
|
294
|
+
function wrapText(text, maxWidth, fontSize, fontFamily, fontWeight, fontStyle, letterSpacing, wordBreak, ctx, measureFn) {
|
|
295
|
+
if (!text) return [""];
|
|
296
|
+
const mw = measureFn ?? ((word) => measureWord(
|
|
297
|
+
word,
|
|
298
|
+
fontSize,
|
|
299
|
+
fontFamily,
|
|
300
|
+
fontWeight,
|
|
301
|
+
fontStyle,
|
|
302
|
+
ctx,
|
|
303
|
+
letterSpacing
|
|
304
|
+
));
|
|
305
|
+
const breakOpps = findBreakOpportunities(text);
|
|
306
|
+
const lines = [];
|
|
307
|
+
let lineStart = 0;
|
|
308
|
+
let lastBreak = 0;
|
|
309
|
+
for (const opp of breakOpps) {
|
|
310
|
+
const segment = text.slice(lineStart, opp.position);
|
|
311
|
+
const segWidth = mw(segment);
|
|
312
|
+
if (segWidth > maxWidth && lastBreak > lineStart) {
|
|
313
|
+
const line = text.slice(lineStart, lastBreak).replace(/\s+$/, "");
|
|
314
|
+
lines.push(line);
|
|
315
|
+
lineStart = lastBreak;
|
|
316
|
+
} else if (segWidth > maxWidth && wordBreak === "break-all") {
|
|
317
|
+
const broken = forceBreakWord(
|
|
318
|
+
text,
|
|
319
|
+
lineStart,
|
|
320
|
+
opp.position,
|
|
321
|
+
maxWidth,
|
|
322
|
+
fontSize,
|
|
323
|
+
fontFamily,
|
|
324
|
+
fontWeight,
|
|
325
|
+
fontStyle,
|
|
326
|
+
ctx,
|
|
327
|
+
letterSpacing,
|
|
328
|
+
measureFn
|
|
329
|
+
);
|
|
330
|
+
lines.push(...broken.lines);
|
|
331
|
+
lineStart = broken.endPos;
|
|
332
|
+
}
|
|
333
|
+
if (opp.required) {
|
|
334
|
+
const line = text.slice(lineStart, opp.position).replace(/\s+$/, "");
|
|
335
|
+
lines.push(line);
|
|
336
|
+
lineStart = opp.position;
|
|
337
|
+
}
|
|
338
|
+
lastBreak = opp.position;
|
|
339
|
+
}
|
|
340
|
+
if (lineStart < text.length) {
|
|
341
|
+
const remaining = text.slice(lineStart).replace(/\s+$/, "");
|
|
342
|
+
if (remaining) {
|
|
343
|
+
const remWidth = mw(remaining);
|
|
344
|
+
if (remWidth > maxWidth && wordBreak === "break-all") {
|
|
345
|
+
const broken = forceBreakWord(
|
|
346
|
+
text,
|
|
347
|
+
lineStart,
|
|
348
|
+
text.length,
|
|
349
|
+
maxWidth,
|
|
350
|
+
fontSize,
|
|
351
|
+
fontFamily,
|
|
352
|
+
fontWeight,
|
|
353
|
+
fontStyle,
|
|
354
|
+
ctx,
|
|
355
|
+
letterSpacing,
|
|
356
|
+
measureFn
|
|
357
|
+
);
|
|
358
|
+
lines.push(...broken.lines);
|
|
359
|
+
} else {
|
|
360
|
+
lines.push(remaining);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return lines.length > 0 ? lines : [""];
|
|
365
|
+
}
|
|
366
|
+
function forceBreakWord(text, start, end, maxWidth, fontSize, fontFamily, fontWeight, fontStyle, ctx, letterSpacing = 0, measureFn) {
|
|
367
|
+
const mw = measureFn ?? ((word) => measureWord(
|
|
368
|
+
word,
|
|
369
|
+
fontSize,
|
|
370
|
+
fontFamily,
|
|
371
|
+
fontWeight,
|
|
372
|
+
fontStyle,
|
|
373
|
+
ctx,
|
|
374
|
+
letterSpacing
|
|
375
|
+
));
|
|
376
|
+
const lines = [];
|
|
377
|
+
let pos = start;
|
|
378
|
+
while (pos < end) {
|
|
379
|
+
let breakPos = pos + 1;
|
|
380
|
+
while (breakPos < end) {
|
|
381
|
+
const chunk = text.slice(pos, breakPos + 1);
|
|
382
|
+
const w = mw(chunk);
|
|
383
|
+
if (w > maxWidth) break;
|
|
384
|
+
breakPos++;
|
|
385
|
+
}
|
|
386
|
+
const line = text.slice(pos, breakPos);
|
|
387
|
+
if (line.trim()) lines.push(line);
|
|
388
|
+
pos = breakPos;
|
|
389
|
+
}
|
|
390
|
+
return { lines, endPos: end };
|
|
391
|
+
}
|
|
392
|
+
function truncateWithEllipsis(text, maxWidth, fontSize, fontFamily, fontWeight, fontStyle, ctx, letterSpacing = 0) {
|
|
393
|
+
const ellipsis = "\u2026";
|
|
394
|
+
const ellipsisWidth = measureWord(
|
|
395
|
+
ellipsis,
|
|
396
|
+
fontSize,
|
|
397
|
+
fontFamily,
|
|
398
|
+
fontWeight,
|
|
399
|
+
fontStyle,
|
|
400
|
+
ctx,
|
|
401
|
+
letterSpacing
|
|
402
|
+
);
|
|
403
|
+
const availWidth = maxWidth - ellipsisWidth;
|
|
404
|
+
for (let i = text.length - 1; i > 0; i--) {
|
|
405
|
+
const truncated = text.slice(0, i);
|
|
406
|
+
const w = measureWord(
|
|
407
|
+
truncated,
|
|
408
|
+
fontSize,
|
|
409
|
+
fontFamily,
|
|
410
|
+
fontWeight,
|
|
411
|
+
fontStyle,
|
|
412
|
+
ctx,
|
|
413
|
+
letterSpacing
|
|
414
|
+
);
|
|
415
|
+
if (w <= availWidth) {
|
|
416
|
+
return truncated + ellipsis;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return ellipsis;
|
|
420
|
+
}
|
|
421
|
+
function createTextMeasureFunc(text, style, ctx, emojiEnabled) {
|
|
422
|
+
const measureStyle = { ...style, textOverflow: "clip" };
|
|
423
|
+
return (width, _widthMode, _height, _heightMode) => {
|
|
424
|
+
const maxWidth = width > 0 ? width : Infinity;
|
|
425
|
+
const result = layoutText(text, measureStyle, maxWidth, ctx, emojiEnabled);
|
|
426
|
+
const wrapped = result.segments.length > 1;
|
|
427
|
+
const reportedWidth = wrapped ? Math.min(maxWidth, width > 0 ? width : result.width) : result.width;
|
|
428
|
+
return { width: Math.min(reportedWidth, maxWidth), height: result.height };
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// src/jsx/draw/clip.ts
|
|
433
|
+
function applyClip(ctx, x, y, width, height, borderRadius) {
|
|
434
|
+
ctx.beginPath();
|
|
435
|
+
if (borderRadius && hasRadius(borderRadius)) {
|
|
436
|
+
const { topLeft, topRight, bottomRight, bottomLeft } = borderRadius;
|
|
437
|
+
roundedRect(
|
|
438
|
+
ctx,
|
|
439
|
+
x,
|
|
440
|
+
y,
|
|
441
|
+
width,
|
|
442
|
+
height,
|
|
443
|
+
topLeft,
|
|
444
|
+
topRight,
|
|
445
|
+
bottomRight,
|
|
446
|
+
bottomLeft
|
|
447
|
+
);
|
|
448
|
+
} else {
|
|
449
|
+
ctx.rect(x, y, width, height);
|
|
450
|
+
}
|
|
451
|
+
ctx.clip();
|
|
452
|
+
}
|
|
453
|
+
function roundedRect(ctx, x, y, w, h, tl, tr, br, bl) {
|
|
454
|
+
const maxR = Math.min(w, h) / 2;
|
|
455
|
+
tl = Math.min(tl, maxR);
|
|
456
|
+
tr = Math.min(tr, maxR);
|
|
457
|
+
br = Math.min(br, maxR);
|
|
458
|
+
bl = Math.min(bl, maxR);
|
|
459
|
+
ctx.moveTo(x + tl, y);
|
|
460
|
+
ctx.lineTo(x + w - tr, y);
|
|
461
|
+
if (tr > 0) ctx.arcTo(x + w, y, x + w, y + tr, tr);
|
|
462
|
+
ctx.lineTo(x + w, y + h - br);
|
|
463
|
+
if (br > 0) ctx.arcTo(x + w, y + h, x + w - br, y + h, br);
|
|
464
|
+
ctx.lineTo(x + bl, y + h);
|
|
465
|
+
if (bl > 0) ctx.arcTo(x, y + h, x, y + h - bl, bl);
|
|
466
|
+
ctx.lineTo(x, y + tl);
|
|
467
|
+
if (tl > 0) ctx.arcTo(x, y, x + tl, y, tl);
|
|
468
|
+
ctx.closePath();
|
|
469
|
+
}
|
|
470
|
+
function hasRadius(r) {
|
|
471
|
+
return r.topLeft > 0 || r.topRight > 0 || r.bottomRight > 0 || r.bottomLeft > 0;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// src/jsx/draw/gradient.ts
|
|
475
|
+
function createGradientFromCSS(ctx, cssGradient, x, y, width, height) {
|
|
476
|
+
const trimmed = cssGradient.trim();
|
|
477
|
+
if (trimmed.startsWith("linear-gradient")) {
|
|
478
|
+
return parseLinearGradient(ctx, trimmed, x, y, width, height);
|
|
479
|
+
}
|
|
480
|
+
if (trimmed.startsWith("radial-gradient")) {
|
|
481
|
+
return parseRadialGradient(ctx, trimmed, x, y, width, height);
|
|
482
|
+
}
|
|
483
|
+
return null;
|
|
484
|
+
}
|
|
485
|
+
function parseLinearGradient(ctx, css, x, y, width, height) {
|
|
486
|
+
const match = css.match(/linear-gradient\((.*)\)/s);
|
|
487
|
+
if (!match) return null;
|
|
488
|
+
const content = match[1].trim();
|
|
489
|
+
const parts = splitGradientArgs(content);
|
|
490
|
+
let angle = 180;
|
|
491
|
+
let colorStartIdx = 0;
|
|
492
|
+
const first = parts[0]?.trim();
|
|
493
|
+
if (first) {
|
|
494
|
+
if (first.startsWith("to ")) {
|
|
495
|
+
angle = directionToAngle(first);
|
|
496
|
+
colorStartIdx = 1;
|
|
497
|
+
} else if (first.endsWith("deg")) {
|
|
498
|
+
angle = parseFloat(first);
|
|
499
|
+
colorStartIdx = 1;
|
|
500
|
+
} else if (first.endsWith("turn")) {
|
|
501
|
+
angle = parseFloat(first) * 360;
|
|
502
|
+
colorStartIdx = 1;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
const rad = (angle - 90) * Math.PI / 180;
|
|
506
|
+
const cx = x + width / 2;
|
|
507
|
+
const cy = y + height / 2;
|
|
508
|
+
const halfDiag = Math.abs(width * Math.cos(rad)) / 2 + Math.abs(height * Math.sin(rad)) / 2;
|
|
509
|
+
const x0 = cx - halfDiag * Math.cos(rad);
|
|
510
|
+
const y0 = cy - halfDiag * Math.sin(rad);
|
|
511
|
+
const x1 = cx + halfDiag * Math.cos(rad);
|
|
512
|
+
const y1 = cy + halfDiag * Math.sin(rad);
|
|
513
|
+
const gradient = ctx.createLinearGradient(x0, y0, x1, y1);
|
|
514
|
+
const stops = parts.slice(colorStartIdx);
|
|
515
|
+
addColorStops(gradient, stops);
|
|
516
|
+
return gradient;
|
|
517
|
+
}
|
|
518
|
+
function parseRadialGradient(ctx, css, x, y, width, height) {
|
|
519
|
+
const match = css.match(/radial-gradient\((.*)\)/s);
|
|
520
|
+
if (!match) return null;
|
|
521
|
+
const content = match[1].trim();
|
|
522
|
+
const parts = splitGradientArgs(content);
|
|
523
|
+
const cx = x + width / 2;
|
|
524
|
+
const cy = y + height / 2;
|
|
525
|
+
const radius = Math.max(width, height) / 2;
|
|
526
|
+
const gradient = ctx.createRadialGradient(cx, cy, 0, cx, cy, radius);
|
|
527
|
+
let colorStartIdx = 0;
|
|
528
|
+
const first = parts[0]?.trim() ?? "";
|
|
529
|
+
if (first.startsWith("circle") || first.startsWith("ellipse") || first.startsWith("closest") || first.startsWith("farthest")) {
|
|
530
|
+
colorStartIdx = 1;
|
|
531
|
+
}
|
|
532
|
+
addColorStops(gradient, parts.slice(colorStartIdx));
|
|
533
|
+
return gradient;
|
|
534
|
+
}
|
|
535
|
+
function addColorStops(gradient, stops) {
|
|
536
|
+
if (stops.length === 0) return;
|
|
537
|
+
for (let i = 0; i < stops.length; i++) {
|
|
538
|
+
const stop = stops[i].trim();
|
|
539
|
+
const percentMatch = stop.match(/^(.+?)\s+(\d+(?:\.\d+)?%?)$/);
|
|
540
|
+
if (percentMatch) {
|
|
541
|
+
const color = percentMatch[1];
|
|
542
|
+
const pos = percentMatch[2];
|
|
543
|
+
const offset = pos.endsWith("%") ? parseFloat(pos) / 100 : parseFloat(pos);
|
|
544
|
+
gradient.addColorStop(Math.max(0, Math.min(1, offset)), color);
|
|
545
|
+
} else {
|
|
546
|
+
const offset = stops.length === 1 ? 0.5 : i / (stops.length - 1);
|
|
547
|
+
gradient.addColorStop(offset, stop);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
function directionToAngle(dir) {
|
|
552
|
+
const map = {
|
|
553
|
+
"to top": 0,
|
|
554
|
+
"to right": 90,
|
|
555
|
+
"to bottom": 180,
|
|
556
|
+
"to left": 270,
|
|
557
|
+
"to top right": 45,
|
|
558
|
+
"to top left": 315,
|
|
559
|
+
"to bottom right": 135,
|
|
560
|
+
"to bottom left": 225
|
|
561
|
+
};
|
|
562
|
+
return map[dir] ?? 180;
|
|
563
|
+
}
|
|
564
|
+
function splitGradientArgs(content) {
|
|
565
|
+
const parts = [];
|
|
566
|
+
let current = "";
|
|
567
|
+
let parenDepth = 0;
|
|
568
|
+
for (const char of content) {
|
|
569
|
+
if (char === "(") parenDepth++;
|
|
570
|
+
if (char === ")") parenDepth--;
|
|
571
|
+
if (char === "," && parenDepth === 0) {
|
|
572
|
+
parts.push(current);
|
|
573
|
+
current = "";
|
|
574
|
+
} else {
|
|
575
|
+
current += char;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
if (current.trim()) parts.push(current);
|
|
579
|
+
return parts;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// src/jsx/draw/image.ts
|
|
583
|
+
import { loadImage } from "@napi-rs/canvas";
|
|
584
|
+
|
|
585
|
+
// src/jsx/draw/object-fit.ts
|
|
586
|
+
function computeCover(imgW, imgH, boxX, boxY, boxW, boxH) {
|
|
587
|
+
const imgRatio = imgW / imgH;
|
|
588
|
+
const boxRatio = boxW / boxH;
|
|
589
|
+
let sx, sy, sw, sh;
|
|
590
|
+
if (imgRatio > boxRatio) {
|
|
591
|
+
sh = imgH;
|
|
592
|
+
sw = imgH * boxRatio;
|
|
593
|
+
sx = (imgW - sw) / 2;
|
|
594
|
+
sy = 0;
|
|
595
|
+
} else {
|
|
596
|
+
sw = imgW;
|
|
597
|
+
sh = imgW / boxRatio;
|
|
598
|
+
sx = 0;
|
|
599
|
+
sy = (imgH - sh) / 2;
|
|
600
|
+
}
|
|
601
|
+
return { sx, sy, sw, sh, dx: boxX, dy: boxY, dw: boxW, dh: boxH };
|
|
602
|
+
}
|
|
603
|
+
function computeContain(imgW, imgH, boxX, boxY, boxW, boxH) {
|
|
604
|
+
const imgRatio = imgW / imgH;
|
|
605
|
+
const boxRatio = boxW / boxH;
|
|
606
|
+
let dw, dh;
|
|
607
|
+
if (imgRatio > boxRatio) {
|
|
608
|
+
dw = boxW;
|
|
609
|
+
dh = boxW / imgRatio;
|
|
610
|
+
} else {
|
|
611
|
+
dh = boxH;
|
|
612
|
+
dw = boxH * imgRatio;
|
|
613
|
+
}
|
|
614
|
+
const dx = boxX + (boxW - dw) / 2;
|
|
615
|
+
const dy = boxY + (boxH - dh) / 2;
|
|
616
|
+
return { sx: 0, sy: 0, sw: imgW, sh: imgH, dx, dy, dw, dh };
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// src/jsx/draw/image.ts
|
|
620
|
+
async function drawImage(ctx, src, x, y, width, height, style) {
|
|
621
|
+
const image = await loadImage(src);
|
|
622
|
+
const objectFit = style?.objectFit ?? "fill";
|
|
623
|
+
if (objectFit === "fill") {
|
|
624
|
+
ctx.drawImage(image, x, y, width, height);
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
const imgW = image.width;
|
|
628
|
+
const imgH = image.height;
|
|
629
|
+
if (objectFit === "contain") {
|
|
630
|
+
const r = computeContain(imgW, imgH, x, y, width, height);
|
|
631
|
+
ctx.drawImage(image, r.dx, r.dy, r.dw, r.dh);
|
|
632
|
+
} else if (objectFit === "cover") {
|
|
633
|
+
const r = computeCover(imgW, imgH, x, y, width, height);
|
|
634
|
+
ctx.drawImage(image, r.sx, r.sy, r.sw, r.sh, r.dx, r.dy, r.dw, r.dh);
|
|
635
|
+
} else if (objectFit === "none") {
|
|
636
|
+
const dx = x + (width - imgW) / 2;
|
|
637
|
+
const dy = y + (height - imgH) / 2;
|
|
638
|
+
ctx.drawImage(image, dx, dy);
|
|
639
|
+
} else if (objectFit === "scale-down") {
|
|
640
|
+
if (imgW <= width && imgH <= height) {
|
|
641
|
+
const dx = x + (width - imgW) / 2;
|
|
642
|
+
const dy = y + (height - imgH) / 2;
|
|
643
|
+
ctx.drawImage(image, dx, dy);
|
|
644
|
+
} else {
|
|
645
|
+
const r = computeContain(imgW, imgH, x, y, width, height);
|
|
646
|
+
ctx.drawImage(image, r.dx, r.dy, r.dw, r.dh);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// src/jsx/draw/rect.ts
|
|
652
|
+
function drawRect(ctx, x, y, width, height, style) {
|
|
653
|
+
const borderRadius = getBorderRadius(style);
|
|
654
|
+
const hasRoundedCorners = borderRadius.topLeft > 0 || borderRadius.topRight > 0 || borderRadius.bottomRight > 0 || borderRadius.bottomLeft > 0;
|
|
655
|
+
if (style.boxShadow) {
|
|
656
|
+
drawBoxShadow(ctx, x, y, width, height, style.boxShadow, borderRadius);
|
|
657
|
+
}
|
|
658
|
+
if (style.backgroundColor) {
|
|
659
|
+
ctx.fillStyle = style.backgroundColor;
|
|
660
|
+
if (hasRoundedCorners) {
|
|
661
|
+
ctx.beginPath();
|
|
662
|
+
roundedRect(
|
|
663
|
+
ctx,
|
|
664
|
+
x,
|
|
665
|
+
y,
|
|
666
|
+
width,
|
|
667
|
+
height,
|
|
668
|
+
borderRadius.topLeft,
|
|
669
|
+
borderRadius.topRight,
|
|
670
|
+
borderRadius.bottomRight,
|
|
671
|
+
borderRadius.bottomLeft
|
|
672
|
+
);
|
|
673
|
+
ctx.fill();
|
|
674
|
+
} else {
|
|
675
|
+
ctx.fillRect(x, y, width, height);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
drawBorders(ctx, x, y, width, height, style, borderRadius);
|
|
679
|
+
}
|
|
680
|
+
function getBorderRadius(style) {
|
|
681
|
+
return {
|
|
682
|
+
topLeft: toNumber(style.borderTopLeftRadius),
|
|
683
|
+
topRight: toNumber(style.borderTopRightRadius),
|
|
684
|
+
bottomRight: toNumber(style.borderBottomRightRadius),
|
|
685
|
+
bottomLeft: toNumber(style.borderBottomLeftRadius)
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
function getBorderRadiusFromStyle(style) {
|
|
689
|
+
return getBorderRadius(style);
|
|
690
|
+
}
|
|
691
|
+
function drawBorders(ctx, x, y, width, height, style, borderRadius) {
|
|
692
|
+
const hasRoundedCorners = borderRadius.topLeft > 0 || borderRadius.topRight > 0 || borderRadius.bottomRight > 0 || borderRadius.bottomLeft > 0;
|
|
693
|
+
const tw = toNumber(style.borderTopWidth);
|
|
694
|
+
const rw = toNumber(style.borderRightWidth);
|
|
695
|
+
const bw = toNumber(style.borderBottomWidth);
|
|
696
|
+
const lw = toNumber(style.borderLeftWidth);
|
|
697
|
+
if (tw === 0 && rw === 0 && bw === 0 && lw === 0) return;
|
|
698
|
+
const allSameWidth = tw === rw && rw === bw && bw === lw && tw > 0;
|
|
699
|
+
const tc = style.borderTopColor ?? "black";
|
|
700
|
+
const rc = style.borderRightColor ?? "black";
|
|
701
|
+
const bc = style.borderBottomColor ?? "black";
|
|
702
|
+
const lc = style.borderLeftColor ?? "black";
|
|
703
|
+
const allSameColor = tc === rc && rc === bc && bc === lc;
|
|
704
|
+
if (allSameWidth && allSameColor) {
|
|
705
|
+
ctx.strokeStyle = tc;
|
|
706
|
+
ctx.lineWidth = tw;
|
|
707
|
+
if (hasRoundedCorners) {
|
|
708
|
+
ctx.beginPath();
|
|
709
|
+
const half = tw / 2;
|
|
710
|
+
roundedRect(
|
|
711
|
+
ctx,
|
|
712
|
+
x + half,
|
|
713
|
+
y + half,
|
|
714
|
+
width - tw,
|
|
715
|
+
height - tw,
|
|
716
|
+
Math.max(0, borderRadius.topLeft - half),
|
|
717
|
+
Math.max(0, borderRadius.topRight - half),
|
|
718
|
+
Math.max(0, borderRadius.bottomRight - half),
|
|
719
|
+
Math.max(0, borderRadius.bottomLeft - half)
|
|
720
|
+
);
|
|
721
|
+
ctx.stroke();
|
|
722
|
+
} else {
|
|
723
|
+
ctx.strokeRect(x + tw / 2, y + tw / 2, width - tw, height - tw);
|
|
724
|
+
}
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
if (tw > 0) {
|
|
728
|
+
ctx.strokeStyle = tc;
|
|
729
|
+
ctx.lineWidth = tw;
|
|
730
|
+
ctx.beginPath();
|
|
731
|
+
ctx.moveTo(x, y + tw / 2);
|
|
732
|
+
ctx.lineTo(x + width, y + tw / 2);
|
|
733
|
+
ctx.stroke();
|
|
734
|
+
}
|
|
735
|
+
if (rw > 0) {
|
|
736
|
+
ctx.strokeStyle = rc;
|
|
737
|
+
ctx.lineWidth = rw;
|
|
738
|
+
ctx.beginPath();
|
|
739
|
+
ctx.moveTo(x + width - rw / 2, y);
|
|
740
|
+
ctx.lineTo(x + width - rw / 2, y + height);
|
|
741
|
+
ctx.stroke();
|
|
742
|
+
}
|
|
743
|
+
if (bw > 0) {
|
|
744
|
+
ctx.strokeStyle = bc;
|
|
745
|
+
ctx.lineWidth = bw;
|
|
746
|
+
ctx.beginPath();
|
|
747
|
+
ctx.moveTo(x, y + height - bw / 2);
|
|
748
|
+
ctx.lineTo(x + width, y + height - bw / 2);
|
|
749
|
+
ctx.stroke();
|
|
750
|
+
}
|
|
751
|
+
if (lw > 0) {
|
|
752
|
+
ctx.strokeStyle = lc;
|
|
753
|
+
ctx.lineWidth = lw;
|
|
754
|
+
ctx.beginPath();
|
|
755
|
+
ctx.moveTo(x + lw / 2, y);
|
|
756
|
+
ctx.lineTo(x + lw / 2, y + height);
|
|
757
|
+
ctx.stroke();
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
function drawBoxShadow(ctx, x, y, width, height, boxShadow, borderRadius) {
|
|
761
|
+
const parts = boxShadow.match(
|
|
762
|
+
/(-?\d+(?:\.\d+)?)\s*(?:px)?\s+(-?\d+(?:\.\d+)?)\s*(?:px)?\s+(-?\d+(?:\.\d+)?)\s*(?:px)?(?:\s+(-?\d+(?:\.\d+)?)\s*(?:px)?)?\s+(.*)/
|
|
763
|
+
);
|
|
764
|
+
if (!parts) return;
|
|
765
|
+
const offsetX = parseFloat(parts[1]);
|
|
766
|
+
const offsetY = parseFloat(parts[2]);
|
|
767
|
+
const blur = parseFloat(parts[3]);
|
|
768
|
+
const color = parts[5].trim();
|
|
769
|
+
const radii = [
|
|
770
|
+
borderRadius.topLeft,
|
|
771
|
+
borderRadius.topRight,
|
|
772
|
+
borderRadius.bottomRight,
|
|
773
|
+
borderRadius.bottomLeft
|
|
774
|
+
];
|
|
775
|
+
const margin = blur * 2 + Math.abs(offsetX) + Math.abs(offsetY);
|
|
776
|
+
ctx.save();
|
|
777
|
+
ctx.beginPath();
|
|
778
|
+
ctx.rect(x - margin, y - margin, width + margin * 2, height + margin * 2);
|
|
779
|
+
ctx.roundRect(x, y, width, height, radii);
|
|
780
|
+
ctx.clip("evenodd");
|
|
781
|
+
ctx.filter = `blur(${blur / 2}px)`;
|
|
782
|
+
ctx.translate(offsetX, offsetY);
|
|
783
|
+
ctx.fillStyle = color;
|
|
784
|
+
ctx.beginPath();
|
|
785
|
+
ctx.roundRect(x, y, width, height, radii);
|
|
786
|
+
ctx.fill();
|
|
787
|
+
ctx.restore();
|
|
788
|
+
}
|
|
789
|
+
function toNumber(v) {
|
|
790
|
+
if (typeof v === "number") return v;
|
|
791
|
+
if (v === void 0 || v === null) return 0;
|
|
792
|
+
const n = parseFloat(String(v));
|
|
793
|
+
return isNaN(n) ? 0 : n;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// src/jsx/draw/svg.ts
|
|
797
|
+
import { Path2D } from "@napi-rs/canvas";
|
|
798
|
+
function drawSvgContainer(ctx, node, x, y, width, height) {
|
|
799
|
+
ctx.save();
|
|
800
|
+
ctx.translate(x, y);
|
|
801
|
+
const viewBox = node.props.viewBox;
|
|
802
|
+
if (viewBox) {
|
|
803
|
+
const parts = viewBox.split(/[\s,]+/).map(Number);
|
|
804
|
+
if (parts.length === 4) {
|
|
805
|
+
const [vbX, vbY, vbW, vbH] = parts;
|
|
806
|
+
const scaleX = width / vbW;
|
|
807
|
+
const scaleY = height / vbH;
|
|
808
|
+
ctx.scale(scaleX, scaleY);
|
|
809
|
+
ctx.translate(-vbX, -vbY);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
const inheritedFill = node.props.fill ?? "black";
|
|
813
|
+
const children = node.props.children;
|
|
814
|
+
if (children != null) {
|
|
815
|
+
const childArray = Array.isArray(children) ? children : [children];
|
|
816
|
+
for (const child of childArray) {
|
|
817
|
+
if (child != null && typeof child === "object") {
|
|
818
|
+
drawSvgChild(ctx, child, inheritedFill);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
ctx.restore();
|
|
823
|
+
}
|
|
824
|
+
function drawSvgChild(ctx, child, inheritedFill) {
|
|
825
|
+
const { type, props } = child;
|
|
826
|
+
switch (type) {
|
|
827
|
+
case "path":
|
|
828
|
+
drawPath(ctx, props, inheritedFill);
|
|
829
|
+
break;
|
|
830
|
+
case "circle":
|
|
831
|
+
drawCircle(ctx, props, inheritedFill);
|
|
832
|
+
break;
|
|
833
|
+
case "rect":
|
|
834
|
+
drawSvgRect(ctx, props, inheritedFill);
|
|
835
|
+
break;
|
|
836
|
+
case "line":
|
|
837
|
+
drawLine(ctx, props);
|
|
838
|
+
break;
|
|
839
|
+
case "ellipse":
|
|
840
|
+
drawEllipse(ctx, props, inheritedFill);
|
|
841
|
+
break;
|
|
842
|
+
case "polygon":
|
|
843
|
+
drawPolygon(ctx, props, inheritedFill);
|
|
844
|
+
break;
|
|
845
|
+
case "polyline":
|
|
846
|
+
drawPolyline(ctx, props, inheritedFill);
|
|
847
|
+
break;
|
|
848
|
+
case "g":
|
|
849
|
+
drawGroup(ctx, child, inheritedFill);
|
|
850
|
+
break;
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
function drawPath(ctx, props, inheritedFill) {
|
|
854
|
+
const d = props.d;
|
|
855
|
+
if (!d) return;
|
|
856
|
+
const path = new Path2D(d);
|
|
857
|
+
applyFillAndStroke(ctx, props, path, inheritedFill);
|
|
858
|
+
}
|
|
859
|
+
function drawCircle(ctx, props, inheritedFill) {
|
|
860
|
+
const cx = Number(props.cx ?? 0);
|
|
861
|
+
const cy = Number(props.cy ?? 0);
|
|
862
|
+
const r = Number(props.r ?? 0);
|
|
863
|
+
if (r <= 0) return;
|
|
864
|
+
const path = new Path2D();
|
|
865
|
+
path.arc(cx, cy, r, 0, Math.PI * 2);
|
|
866
|
+
applyFillAndStroke(ctx, props, path, inheritedFill);
|
|
867
|
+
}
|
|
868
|
+
function drawSvgRect(ctx, props, inheritedFill) {
|
|
869
|
+
const rx = Number(props.x ?? 0);
|
|
870
|
+
const ry = Number(props.y ?? 0);
|
|
871
|
+
const w = Number(props.width ?? 0);
|
|
872
|
+
const h = Number(props.height ?? 0);
|
|
873
|
+
if (w <= 0 || h <= 0) return;
|
|
874
|
+
const path = new Path2D();
|
|
875
|
+
path.rect(rx, ry, w, h);
|
|
876
|
+
applyFillAndStroke(ctx, props, path, inheritedFill);
|
|
877
|
+
}
|
|
878
|
+
function drawLine(ctx, props) {
|
|
879
|
+
const x1 = Number(props.x1 ?? 0);
|
|
880
|
+
const y1 = Number(props.y1 ?? 0);
|
|
881
|
+
const x2 = Number(props.x2 ?? 0);
|
|
882
|
+
const y2 = Number(props.y2 ?? 0);
|
|
883
|
+
const path = new Path2D();
|
|
884
|
+
path.moveTo(x1, y1);
|
|
885
|
+
path.lineTo(x2, y2);
|
|
886
|
+
applyStroke(ctx, props, path);
|
|
887
|
+
}
|
|
888
|
+
function drawEllipse(ctx, props, inheritedFill) {
|
|
889
|
+
const cx = Number(props.cx ?? 0);
|
|
890
|
+
const cy = Number(props.cy ?? 0);
|
|
891
|
+
const rx = Number(props.rx ?? 0);
|
|
892
|
+
const ry = Number(props.ry ?? 0);
|
|
893
|
+
if (rx <= 0 || ry <= 0) return;
|
|
894
|
+
const path = new Path2D();
|
|
895
|
+
path.ellipse(cx, cy, rx, ry, 0, 0, Math.PI * 2);
|
|
896
|
+
applyFillAndStroke(ctx, props, path, inheritedFill);
|
|
897
|
+
}
|
|
898
|
+
function drawPolygon(ctx, props, inheritedFill) {
|
|
899
|
+
const points = parsePoints(props.points);
|
|
900
|
+
if (points.length < 2) return;
|
|
901
|
+
const path = new Path2D();
|
|
902
|
+
path.moveTo(points[0][0], points[0][1]);
|
|
903
|
+
for (let i = 1; i < points.length; i++) {
|
|
904
|
+
path.lineTo(points[i][0], points[i][1]);
|
|
905
|
+
}
|
|
906
|
+
path.closePath();
|
|
907
|
+
applyFillAndStroke(ctx, props, path, inheritedFill);
|
|
908
|
+
}
|
|
909
|
+
function drawPolyline(ctx, props, inheritedFill) {
|
|
910
|
+
const points = parsePoints(props.points);
|
|
911
|
+
if (points.length < 2) return;
|
|
912
|
+
const path = new Path2D();
|
|
913
|
+
path.moveTo(points[0][0], points[0][1]);
|
|
914
|
+
for (let i = 1; i < points.length; i++) {
|
|
915
|
+
path.lineTo(points[i][0], points[i][1]);
|
|
916
|
+
}
|
|
917
|
+
applyFillAndStroke(ctx, props, path, inheritedFill);
|
|
918
|
+
}
|
|
919
|
+
function drawGroup(ctx, node, inheritedFill) {
|
|
920
|
+
const children = node.children ?? node.props.children;
|
|
921
|
+
if (children == null) return;
|
|
922
|
+
const groupFill = node.props.fill ?? inheritedFill;
|
|
923
|
+
const childArray = Array.isArray(children) ? children : [children];
|
|
924
|
+
for (const child of childArray) {
|
|
925
|
+
if (child != null && typeof child === "object") {
|
|
926
|
+
drawSvgChild(ctx, child, groupFill);
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
function parsePoints(value) {
|
|
931
|
+
if (!value) return [];
|
|
932
|
+
const nums = value.trim().split(/[\s,]+/).map(Number);
|
|
933
|
+
const result = [];
|
|
934
|
+
for (let i = 0; i + 1 < nums.length; i += 2) {
|
|
935
|
+
result.push([nums[i], nums[i + 1]]);
|
|
936
|
+
}
|
|
937
|
+
return result;
|
|
938
|
+
}
|
|
939
|
+
function applyFillAndStroke(ctx, props, path, inheritedFill) {
|
|
940
|
+
const fill = props.fill ?? inheritedFill;
|
|
941
|
+
if (fill !== "none") {
|
|
942
|
+
ctx.fillStyle = fill;
|
|
943
|
+
ctx.fill(path);
|
|
944
|
+
}
|
|
945
|
+
applyStroke(ctx, props, path);
|
|
946
|
+
}
|
|
947
|
+
function applyStroke(ctx, props, path) {
|
|
948
|
+
const stroke = props.stroke;
|
|
949
|
+
if (!stroke || stroke === "none") return;
|
|
950
|
+
ctx.strokeStyle = stroke;
|
|
951
|
+
ctx.lineWidth = Number(props.strokeWidth ?? 1);
|
|
952
|
+
ctx.lineCap = props.strokeLinecap ?? "butt";
|
|
953
|
+
ctx.lineJoin = props.strokeLinejoin ?? "miter";
|
|
954
|
+
ctx.stroke(path);
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
// src/jsx/draw/text.ts
|
|
958
|
+
import { loadImage as loadImage2 } from "@napi-rs/canvas";
|
|
959
|
+
|
|
960
|
+
// src/jsx/emoji.ts
|
|
961
|
+
var emojiApis = {
|
|
962
|
+
twemoji: (code) => `https://cdnjs.cloudflare.com/ajax/libs/twemoji/16.0.1/svg/${code.toLowerCase()}.svg`,
|
|
963
|
+
openmoji: "https://cdn.jsdelivr.net/npm/@svgmoji/openmoji@2.0.0/svg/",
|
|
964
|
+
blobmoji: "https://cdn.jsdelivr.net/npm/@svgmoji/blob@2.0.0/svg/",
|
|
965
|
+
noto: "https://cdn.jsdelivr.net/gh/svgmoji/svgmoji/packages/svgmoji__noto/svg/",
|
|
966
|
+
fluent: (code) => `https://cdn.jsdelivr.net/gh/shuding/fluentui-emoji-unicode/assets/${code.toLowerCase()}_color.svg`,
|
|
967
|
+
fluentFlat: (code) => `https://cdn.jsdelivr.net/gh/shuding/fluentui-emoji-unicode/assets/${code.toLowerCase()}_flat.svg`
|
|
968
|
+
};
|
|
969
|
+
var U200D = String.fromCharCode(8205);
|
|
970
|
+
var UFE0Fg = /\uFE0F/g;
|
|
971
|
+
function getEmojiCode(char) {
|
|
972
|
+
return toCodePoint(char.indexOf(U200D) < 0 ? char.replace(UFE0Fg, "") : char);
|
|
973
|
+
}
|
|
974
|
+
function toCodePoint(unicodeSurrogates) {
|
|
975
|
+
const r = [];
|
|
976
|
+
let c = 0, p = 0, i = 0;
|
|
977
|
+
while (i < unicodeSurrogates.length) {
|
|
978
|
+
c = unicodeSurrogates.charCodeAt(i++);
|
|
979
|
+
if (p) {
|
|
980
|
+
r.push((65536 + (p - 55296 << 10) + (c - 56320)).toString(16));
|
|
981
|
+
p = 0;
|
|
982
|
+
} else if (55296 <= c && c <= 56319) {
|
|
983
|
+
p = c;
|
|
984
|
+
} else {
|
|
985
|
+
r.push(c.toString(16));
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
return r.join("-");
|
|
989
|
+
}
|
|
990
|
+
var emojiCache = {};
|
|
991
|
+
async function loadEmoji(type, code) {
|
|
992
|
+
const key = type + ":" + code;
|
|
993
|
+
if (key in emojiCache) return emojiCache[key];
|
|
994
|
+
const api = emojiApis[type];
|
|
995
|
+
if (typeof api === "function") {
|
|
996
|
+
return emojiCache[key] = fetch(api(code)).then((r) => r.text());
|
|
997
|
+
}
|
|
998
|
+
return emojiCache[key] = fetch(`${api}${code.toUpperCase()}.svg`).then(
|
|
999
|
+
(r) => r.text()
|
|
1000
|
+
);
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
// src/jsx/text/emoji-split.ts
|
|
1004
|
+
function splitTextIntoRuns(text, measureText2, emojiSize) {
|
|
1005
|
+
const runs = [];
|
|
1006
|
+
const segmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
|
|
1007
|
+
let currentText = "";
|
|
1008
|
+
let currentX = 0;
|
|
1009
|
+
let textStartX = 0;
|
|
1010
|
+
for (const { segment } of segmenter.segment(text)) {
|
|
1011
|
+
if (isEmojiGrapheme(segment)) {
|
|
1012
|
+
if (currentText) {
|
|
1013
|
+
const textWidth = measureText2(currentText);
|
|
1014
|
+
runs.push({
|
|
1015
|
+
kind: "text",
|
|
1016
|
+
text: currentText,
|
|
1017
|
+
x: textStartX,
|
|
1018
|
+
width: textWidth
|
|
1019
|
+
});
|
|
1020
|
+
currentX = textStartX + textWidth;
|
|
1021
|
+
currentText = "";
|
|
1022
|
+
}
|
|
1023
|
+
runs.push({
|
|
1024
|
+
kind: "emoji",
|
|
1025
|
+
char: segment,
|
|
1026
|
+
x: currentX,
|
|
1027
|
+
width: emojiSize
|
|
1028
|
+
});
|
|
1029
|
+
currentX += emojiSize;
|
|
1030
|
+
textStartX = currentX;
|
|
1031
|
+
} else {
|
|
1032
|
+
if (!currentText) textStartX = currentX;
|
|
1033
|
+
currentText += segment;
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
if (currentText) {
|
|
1037
|
+
const textWidth = measureText2(currentText);
|
|
1038
|
+
runs.push({
|
|
1039
|
+
kind: "text",
|
|
1040
|
+
text: currentText,
|
|
1041
|
+
x: textStartX,
|
|
1042
|
+
width: textWidth
|
|
1043
|
+
});
|
|
1044
|
+
}
|
|
1045
|
+
return runs;
|
|
1046
|
+
}
|
|
1047
|
+
function isEmojiGrapheme(grapheme) {
|
|
1048
|
+
for (const char of grapheme) {
|
|
1049
|
+
if (isEmoji(char)) return true;
|
|
1050
|
+
}
|
|
1051
|
+
return false;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
// src/jsx/draw/text.ts
|
|
1055
|
+
var emojiImageCache = /* @__PURE__ */ new Map();
|
|
1056
|
+
function loadEmojiImage(style, char) {
|
|
1057
|
+
const code = getEmojiCode(char);
|
|
1058
|
+
const key = style + ":" + code;
|
|
1059
|
+
let cached = emojiImageCache.get(key);
|
|
1060
|
+
if (!cached) {
|
|
1061
|
+
cached = loadEmoji(style, code).then((svgText) => {
|
|
1062
|
+
if (!svgText || !svgText.includes("<svg")) return null;
|
|
1063
|
+
const dataUri = "data:image/svg+xml;base64," + Buffer.from(svgText).toString("base64");
|
|
1064
|
+
return loadImage2(dataUri);
|
|
1065
|
+
}).catch(() => null);
|
|
1066
|
+
emojiImageCache.set(key, cached);
|
|
1067
|
+
}
|
|
1068
|
+
return cached;
|
|
1069
|
+
}
|
|
1070
|
+
async function drawText(ctx, segments, offsetX, offsetY, textShadow, emojiStyle) {
|
|
1071
|
+
for (const seg of segments) {
|
|
1072
|
+
if (!seg.text) continue;
|
|
1073
|
+
setFont(ctx, seg.fontSize, seg.fontFamily, seg.fontWeight, seg.fontStyle);
|
|
1074
|
+
ctx.fillStyle = seg.color;
|
|
1075
|
+
const x = offsetX + seg.x;
|
|
1076
|
+
const y = offsetY + seg.y;
|
|
1077
|
+
if (emojiStyle) {
|
|
1078
|
+
await drawSegmentWithEmoji(ctx, seg, x, y, textShadow, emojiStyle);
|
|
1079
|
+
} else if (seg.letterSpacing && seg.letterSpacing !== 0) {
|
|
1080
|
+
drawTextWithLetterSpacing(ctx, seg.text, x, y, seg.letterSpacing);
|
|
1081
|
+
} else {
|
|
1082
|
+
if (textShadow) {
|
|
1083
|
+
drawTextShadow(ctx, seg.text, x, y, textShadow);
|
|
1084
|
+
}
|
|
1085
|
+
ctx.fillText(seg.text, x, y);
|
|
1086
|
+
}
|
|
1087
|
+
if (seg.textDecoration) {
|
|
1088
|
+
drawTextDecoration(ctx, seg, offsetX, offsetY);
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
async function drawSegmentWithEmoji(ctx, seg, x, y, textShadow, emojiStyle) {
|
|
1093
|
+
const runs = splitTextIntoRuns(
|
|
1094
|
+
seg.text,
|
|
1095
|
+
(text) => {
|
|
1096
|
+
setFont(ctx, seg.fontSize, seg.fontFamily, seg.fontWeight, seg.fontStyle);
|
|
1097
|
+
return ctx.measureText(text).width;
|
|
1098
|
+
},
|
|
1099
|
+
seg.fontSize
|
|
1100
|
+
);
|
|
1101
|
+
for (const run of runs) {
|
|
1102
|
+
if (run.kind === "text") {
|
|
1103
|
+
if (textShadow) {
|
|
1104
|
+
drawTextShadow(ctx, run.text, x + run.x, y, textShadow);
|
|
1105
|
+
}
|
|
1106
|
+
ctx.fillText(run.text, x + run.x, y);
|
|
1107
|
+
} else {
|
|
1108
|
+
const img = await loadEmojiImage(emojiStyle, run.char);
|
|
1109
|
+
if (img) {
|
|
1110
|
+
const emojiSize = seg.fontSize;
|
|
1111
|
+
const emojiY = y - seg.ascent + (seg.height - seg.fontSize) / 2;
|
|
1112
|
+
ctx.drawImage(img, x + run.x, emojiY, emojiSize, emojiSize);
|
|
1113
|
+
} else {
|
|
1114
|
+
ctx.fillText(run.char, x + run.x, y);
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
function drawTextWithLetterSpacing(ctx, text, x, y, letterSpacing) {
|
|
1120
|
+
let currentX = x;
|
|
1121
|
+
for (const char of text) {
|
|
1122
|
+
ctx.fillText(char, currentX, y);
|
|
1123
|
+
const metrics = ctx.measureText(char);
|
|
1124
|
+
currentX += metrics.width + letterSpacing;
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
function drawTextShadow(ctx, text, x, y, shadow) {
|
|
1128
|
+
const parts = shadow.match(
|
|
1129
|
+
/(-?\d+(?:\.\d+)?)\s*px?\s+(-?\d+(?:\.\d+)?)\s*px?\s+(-?\d+(?:\.\d+)?)\s*px?\s+(.*)/
|
|
1130
|
+
);
|
|
1131
|
+
if (!parts) return;
|
|
1132
|
+
ctx.save();
|
|
1133
|
+
ctx.shadowOffsetX = parseFloat(parts[1]);
|
|
1134
|
+
ctx.shadowOffsetY = parseFloat(parts[2]);
|
|
1135
|
+
ctx.shadowBlur = parseFloat(parts[3]);
|
|
1136
|
+
ctx.shadowColor = parts[4].trim();
|
|
1137
|
+
ctx.fillText(text, x, y);
|
|
1138
|
+
ctx.restore();
|
|
1139
|
+
}
|
|
1140
|
+
function drawTextDecoration(ctx, seg, offsetX, offsetY) {
|
|
1141
|
+
const decoration = seg.textDecoration;
|
|
1142
|
+
if (!decoration || decoration === "none") return;
|
|
1143
|
+
ctx.strokeStyle = seg.color;
|
|
1144
|
+
ctx.lineWidth = Math.max(1, seg.fontSize * 0.1);
|
|
1145
|
+
const x = offsetX + seg.x;
|
|
1146
|
+
const baseY = offsetY + seg.y;
|
|
1147
|
+
if (decoration.includes("underline")) {
|
|
1148
|
+
const y = baseY + seg.ascent * 0.1;
|
|
1149
|
+
ctx.beginPath();
|
|
1150
|
+
ctx.moveTo(x, y);
|
|
1151
|
+
ctx.lineTo(x + seg.width, y);
|
|
1152
|
+
ctx.stroke();
|
|
1153
|
+
}
|
|
1154
|
+
if (decoration.includes("line-through")) {
|
|
1155
|
+
const y = baseY - seg.fontSize * 0.3;
|
|
1156
|
+
ctx.beginPath();
|
|
1157
|
+
ctx.moveTo(x, y);
|
|
1158
|
+
ctx.lineTo(x + seg.width, y);
|
|
1159
|
+
ctx.stroke();
|
|
1160
|
+
}
|
|
1161
|
+
if (decoration.includes("overline")) {
|
|
1162
|
+
const y = baseY - seg.fontSize * 0.85;
|
|
1163
|
+
ctx.beginPath();
|
|
1164
|
+
ctx.moveTo(x, y);
|
|
1165
|
+
ctx.lineTo(x + seg.width, y);
|
|
1166
|
+
ctx.stroke();
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
// src/jsx/draw/index.ts
|
|
1171
|
+
async function drawNode(ctx, node, parentX, parentY, debug, emojiStyle) {
|
|
1172
|
+
const x = parentX + node.x;
|
|
1173
|
+
const y = parentY + node.y;
|
|
1174
|
+
const { width, height, style } = node;
|
|
1175
|
+
if (style.display === "none") return;
|
|
1176
|
+
const opacity = style.opacity ?? 1;
|
|
1177
|
+
if (opacity <= 0) return;
|
|
1178
|
+
ctx.save();
|
|
1179
|
+
if (opacity < 1) {
|
|
1180
|
+
ctx.globalAlpha *= opacity;
|
|
1181
|
+
}
|
|
1182
|
+
if (style.filter) {
|
|
1183
|
+
ctx.filter = style.filter;
|
|
1184
|
+
}
|
|
1185
|
+
if (style.transform) {
|
|
1186
|
+
applyTransform(
|
|
1187
|
+
ctx,
|
|
1188
|
+
style.transform,
|
|
1189
|
+
x,
|
|
1190
|
+
y,
|
|
1191
|
+
width,
|
|
1192
|
+
height,
|
|
1193
|
+
style.transformOrigin
|
|
1194
|
+
);
|
|
1195
|
+
}
|
|
1196
|
+
const isClipped = style.overflow === "hidden" || style.overflowX === "hidden" || style.overflowY === "hidden";
|
|
1197
|
+
if (isClipped) {
|
|
1198
|
+
const borderRadius = getBorderRadiusFromStyle(style);
|
|
1199
|
+
applyClip(ctx, x, y, width, height, borderRadius);
|
|
1200
|
+
}
|
|
1201
|
+
if (style.backgroundColor || style.borderTopWidth || style.borderRightWidth || style.borderBottomWidth || style.borderLeftWidth || style.boxShadow) {
|
|
1202
|
+
drawRect(ctx, x, y, width, height, style);
|
|
1203
|
+
}
|
|
1204
|
+
if (style.backgroundImage) {
|
|
1205
|
+
const gradient = createGradientFromCSS(
|
|
1206
|
+
ctx,
|
|
1207
|
+
style.backgroundImage,
|
|
1208
|
+
x,
|
|
1209
|
+
y,
|
|
1210
|
+
width,
|
|
1211
|
+
height
|
|
1212
|
+
);
|
|
1213
|
+
if (gradient) {
|
|
1214
|
+
ctx.fillStyle = gradient;
|
|
1215
|
+
const borderRadius = getBorderRadiusFromStyle(style);
|
|
1216
|
+
if (borderRadius.topLeft > 0 || borderRadius.topRight > 0 || borderRadius.bottomRight > 0 || borderRadius.bottomLeft > 0) {
|
|
1217
|
+
ctx.beginPath();
|
|
1218
|
+
roundedRect(
|
|
1219
|
+
ctx,
|
|
1220
|
+
x,
|
|
1221
|
+
y,
|
|
1222
|
+
width,
|
|
1223
|
+
height,
|
|
1224
|
+
borderRadius.topLeft,
|
|
1225
|
+
borderRadius.topRight,
|
|
1226
|
+
borderRadius.bottomRight,
|
|
1227
|
+
borderRadius.bottomLeft
|
|
1228
|
+
);
|
|
1229
|
+
ctx.fill();
|
|
1230
|
+
} else {
|
|
1231
|
+
ctx.fillRect(x, y, width, height);
|
|
1232
|
+
}
|
|
1233
|
+
} else {
|
|
1234
|
+
const urlMatch = style.backgroundImage.match(/url\(["']?(.*?)["']?\)/);
|
|
1235
|
+
if (urlMatch) {
|
|
1236
|
+
const borderRadius = getBorderRadiusFromStyle(style);
|
|
1237
|
+
const hasRadius2 = borderRadius.topLeft > 0 || borderRadius.topRight > 0 || borderRadius.bottomRight > 0 || borderRadius.bottomLeft > 0;
|
|
1238
|
+
if (hasRadius2) {
|
|
1239
|
+
applyClip(ctx, x, y, width, height, borderRadius);
|
|
1240
|
+
}
|
|
1241
|
+
const image = await loadImage3(urlMatch[1]);
|
|
1242
|
+
const bgSize = style.backgroundSize;
|
|
1243
|
+
if (bgSize === "cover") {
|
|
1244
|
+
const r = computeCover(
|
|
1245
|
+
image.width,
|
|
1246
|
+
image.height,
|
|
1247
|
+
x,
|
|
1248
|
+
y,
|
|
1249
|
+
width,
|
|
1250
|
+
height
|
|
1251
|
+
);
|
|
1252
|
+
ctx.drawImage(image, r.sx, r.sy, r.sw, r.sh, r.dx, r.dy, r.dw, r.dh);
|
|
1253
|
+
} else {
|
|
1254
|
+
let tileW, tileH;
|
|
1255
|
+
if (bgSize === "contain") {
|
|
1256
|
+
const r = computeContain(
|
|
1257
|
+
image.width,
|
|
1258
|
+
image.height,
|
|
1259
|
+
0,
|
|
1260
|
+
0,
|
|
1261
|
+
width,
|
|
1262
|
+
height
|
|
1263
|
+
);
|
|
1264
|
+
tileW = r.dw;
|
|
1265
|
+
tileH = r.dh;
|
|
1266
|
+
} else if (bgSize === "100% 100%") {
|
|
1267
|
+
tileW = width;
|
|
1268
|
+
tileH = height;
|
|
1269
|
+
} else {
|
|
1270
|
+
tileW = image.width;
|
|
1271
|
+
tileH = image.height;
|
|
1272
|
+
}
|
|
1273
|
+
for (let ty = y; ty < y + height; ty += tileH) {
|
|
1274
|
+
for (let tx = x; tx < x + width; tx += tileW) {
|
|
1275
|
+
ctx.drawImage(image, tx, ty, tileW, tileH);
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
if (debug) {
|
|
1283
|
+
ctx.strokeStyle = "rgba(255, 0, 0, 0.5)";
|
|
1284
|
+
ctx.lineWidth = 1;
|
|
1285
|
+
ctx.strokeRect(x, y, width, height);
|
|
1286
|
+
}
|
|
1287
|
+
if (node.textContent !== void 0 && node.textContent !== "") {
|
|
1288
|
+
const paddingTop = toNumber2(style.paddingTop);
|
|
1289
|
+
const paddingLeft = toNumber2(style.paddingLeft);
|
|
1290
|
+
const paddingRight = toNumber2(style.paddingRight);
|
|
1291
|
+
const borderTopW = toNumber2(style.borderTopWidth);
|
|
1292
|
+
const borderLeftW = toNumber2(style.borderLeftWidth);
|
|
1293
|
+
const borderRightW = toNumber2(style.borderRightWidth);
|
|
1294
|
+
const contentX = x + paddingLeft + borderLeftW;
|
|
1295
|
+
const contentY = y + paddingTop + borderTopW;
|
|
1296
|
+
const contentWidth = width - paddingLeft - paddingRight - borderLeftW - borderRightW;
|
|
1297
|
+
const textLayout = layoutText(
|
|
1298
|
+
node.textContent,
|
|
1299
|
+
style,
|
|
1300
|
+
contentWidth,
|
|
1301
|
+
ctx,
|
|
1302
|
+
!!emojiStyle
|
|
1303
|
+
);
|
|
1304
|
+
await drawText(
|
|
1305
|
+
ctx,
|
|
1306
|
+
textLayout.segments,
|
|
1307
|
+
contentX,
|
|
1308
|
+
contentY,
|
|
1309
|
+
style.textShadow,
|
|
1310
|
+
emojiStyle
|
|
1311
|
+
);
|
|
1312
|
+
}
|
|
1313
|
+
if (node.type === "img" && node.props.src) {
|
|
1314
|
+
const paddingTop = toNumber2(style.paddingTop);
|
|
1315
|
+
const paddingLeft = toNumber2(style.paddingLeft);
|
|
1316
|
+
const paddingRight = toNumber2(style.paddingRight);
|
|
1317
|
+
const paddingBottom = toNumber2(style.paddingBottom);
|
|
1318
|
+
const imgX = x + paddingLeft;
|
|
1319
|
+
const imgY = y + paddingTop;
|
|
1320
|
+
const imgW = width - paddingLeft - paddingRight;
|
|
1321
|
+
const imgH = height - paddingTop - paddingBottom;
|
|
1322
|
+
if (!isClipped) {
|
|
1323
|
+
const borderRadius = getBorderRadiusFromStyle(style);
|
|
1324
|
+
if (borderRadius.topLeft > 0 || borderRadius.topRight > 0 || borderRadius.bottomRight > 0 || borderRadius.bottomLeft > 0) {
|
|
1325
|
+
applyClip(ctx, imgX, imgY, imgW, imgH, borderRadius);
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
await drawImage(
|
|
1329
|
+
ctx,
|
|
1330
|
+
node.props.src,
|
|
1331
|
+
imgX,
|
|
1332
|
+
imgY,
|
|
1333
|
+
imgW,
|
|
1334
|
+
imgH,
|
|
1335
|
+
style
|
|
1336
|
+
);
|
|
1337
|
+
}
|
|
1338
|
+
if (node.type === "svg") {
|
|
1339
|
+
drawSvgContainer(ctx, node, x, y, width, height);
|
|
1340
|
+
} else {
|
|
1341
|
+
for (const child of node.children) {
|
|
1342
|
+
await drawNode(ctx, child, x, y, debug, emojiStyle);
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
ctx.restore();
|
|
1346
|
+
}
|
|
1347
|
+
function applyTransform(ctx, transform, x, y, width, height, transformOrigin) {
|
|
1348
|
+
let ox = x + width / 2;
|
|
1349
|
+
let oy = y + height / 2;
|
|
1350
|
+
if (transformOrigin) {
|
|
1351
|
+
const parts = transformOrigin.split(/\s+/);
|
|
1352
|
+
ox = resolveOrigin(parts[0], x, width);
|
|
1353
|
+
oy = resolveOrigin(parts[1], y, height);
|
|
1354
|
+
}
|
|
1355
|
+
ctx.translate(ox, oy);
|
|
1356
|
+
const funcs = transform.matchAll(/(\w+)\(([^)]+)\)/g);
|
|
1357
|
+
for (const [, name, args] of funcs) {
|
|
1358
|
+
const values = args.split(",").map((s) => s.trim());
|
|
1359
|
+
switch (name) {
|
|
1360
|
+
case "translate":
|
|
1361
|
+
case "translateX":
|
|
1362
|
+
case "translateY": {
|
|
1363
|
+
const tx = name === "translateY" ? 0 : parseFloat(values[0]);
|
|
1364
|
+
const ty = name === "translateX" ? 0 : parseFloat(values[name === "translate" ? 1 : 0] ?? "0");
|
|
1365
|
+
ctx.translate(tx, ty);
|
|
1366
|
+
break;
|
|
1367
|
+
}
|
|
1368
|
+
case "scale":
|
|
1369
|
+
case "scaleX":
|
|
1370
|
+
case "scaleY": {
|
|
1371
|
+
const sx = name === "scaleY" ? 1 : parseFloat(values[0]);
|
|
1372
|
+
const sy = name === "scaleX" ? 1 : parseFloat(values[name === "scale" ? 1 : 0] ?? String(sx));
|
|
1373
|
+
ctx.scale(sx, sy);
|
|
1374
|
+
break;
|
|
1375
|
+
}
|
|
1376
|
+
case "rotate": {
|
|
1377
|
+
const angle = parseAngle(values[0]);
|
|
1378
|
+
ctx.rotate(angle);
|
|
1379
|
+
break;
|
|
1380
|
+
}
|
|
1381
|
+
case "skewX": {
|
|
1382
|
+
const angle = parseAngle(values[0]);
|
|
1383
|
+
ctx.transform(1, 0, Math.tan(angle), 1, 0, 0);
|
|
1384
|
+
break;
|
|
1385
|
+
}
|
|
1386
|
+
case "skewY": {
|
|
1387
|
+
const angle = parseAngle(values[0]);
|
|
1388
|
+
ctx.transform(1, Math.tan(angle), 0, 1, 0, 0);
|
|
1389
|
+
break;
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
ctx.translate(-ox, -oy);
|
|
1394
|
+
}
|
|
1395
|
+
function resolveOrigin(value, base, size) {
|
|
1396
|
+
if (!value) return base + size / 2;
|
|
1397
|
+
if (value === "left" || value === "top") return base;
|
|
1398
|
+
if (value === "right" || value === "bottom") return base + size;
|
|
1399
|
+
if (value === "center") return base + size / 2;
|
|
1400
|
+
if (value.endsWith("%")) return base + parseFloat(value) / 100 * size;
|
|
1401
|
+
return base + parseFloat(value);
|
|
1402
|
+
}
|
|
1403
|
+
function parseAngle(value) {
|
|
1404
|
+
if (value.endsWith("deg")) return parseFloat(value) * Math.PI / 180;
|
|
1405
|
+
if (value.endsWith("rad")) return parseFloat(value);
|
|
1406
|
+
if (value.endsWith("turn")) return parseFloat(value) * 2 * Math.PI;
|
|
1407
|
+
return parseFloat(value);
|
|
1408
|
+
}
|
|
1409
|
+
function toNumber2(v) {
|
|
1410
|
+
if (typeof v === "number") return v;
|
|
1411
|
+
if (v === void 0 || v === null) return 0;
|
|
1412
|
+
const n = parseFloat(String(v));
|
|
1413
|
+
return isNaN(n) ? 0 : n;
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
// src/jsx/font.ts
|
|
1417
|
+
import { GlobalFonts } from "@napi-rs/canvas";
|
|
1418
|
+
var registeredFonts = /* @__PURE__ */ new Set();
|
|
1419
|
+
function registerFont(font) {
|
|
1420
|
+
const key = `${font.name}:${font.weight}:${font.style}`;
|
|
1421
|
+
if (registeredFonts.has(key)) return;
|
|
1422
|
+
const buffer = Buffer.isBuffer(font.data) ? font.data : Buffer.from(font.data);
|
|
1423
|
+
GlobalFonts.register(buffer, font.name);
|
|
1424
|
+
registeredFonts.add(key);
|
|
1425
|
+
}
|
|
1426
|
+
function registerFontFromPath(path, nameAlias) {
|
|
1427
|
+
GlobalFonts.registerFromPath(path, nameAlias ?? "");
|
|
1428
|
+
}
|
|
1429
|
+
function registeredFamilies() {
|
|
1430
|
+
return GlobalFonts.families.map((f) => f.family);
|
|
1431
|
+
}
|
|
1432
|
+
function ensureFontsRegistered(fonts) {
|
|
1433
|
+
for (const font of fonts) {
|
|
1434
|
+
registerFont(font);
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
// src/jsx/style/expand.ts
|
|
1439
|
+
var SIDES = ["Top", "Right", "Bottom", "Left"];
|
|
1440
|
+
function parseSides(value) {
|
|
1441
|
+
const parts = value.toString().split(/\s+/).filter(Boolean);
|
|
1442
|
+
switch (parts.length) {
|
|
1443
|
+
case 1:
|
|
1444
|
+
return [parts[0], parts[0], parts[0], parts[0]];
|
|
1445
|
+
case 2:
|
|
1446
|
+
return [parts[0], parts[1], parts[0], parts[1]];
|
|
1447
|
+
case 3:
|
|
1448
|
+
return [parts[0], parts[1], parts[2], parts[1]];
|
|
1449
|
+
default:
|
|
1450
|
+
return [parts[0], parts[1], parts[2], parts[3]];
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
function parseValue(v) {
|
|
1454
|
+
if (v === void 0 || v === null) return void 0;
|
|
1455
|
+
const s = String(v);
|
|
1456
|
+
if (s === "auto") return "auto";
|
|
1457
|
+
const n = parseFloat(s);
|
|
1458
|
+
if (!isNaN(n)) return n;
|
|
1459
|
+
return s;
|
|
1460
|
+
}
|
|
1461
|
+
function expandStyle(raw) {
|
|
1462
|
+
const style = { ...raw };
|
|
1463
|
+
if (style.margin !== void 0) {
|
|
1464
|
+
const sides = parseSides(String(style.margin));
|
|
1465
|
+
for (let i = 0; i < 4; i++) {
|
|
1466
|
+
const key = `margin${SIDES[i]}`;
|
|
1467
|
+
if (style[key] === void 0) style[key] = parseValue(sides[i]);
|
|
1468
|
+
}
|
|
1469
|
+
delete style.margin;
|
|
1470
|
+
}
|
|
1471
|
+
if (style.padding !== void 0) {
|
|
1472
|
+
const sides = parseSides(String(style.padding));
|
|
1473
|
+
for (let i = 0; i < 4; i++) {
|
|
1474
|
+
const key = `padding${SIDES[i]}`;
|
|
1475
|
+
if (style[key] === void 0) style[key] = parseValue(sides[i]);
|
|
1476
|
+
}
|
|
1477
|
+
delete style.padding;
|
|
1478
|
+
}
|
|
1479
|
+
if (style.borderRadius !== void 0) {
|
|
1480
|
+
const sides = parseSides(String(style.borderRadius));
|
|
1481
|
+
const corners = [
|
|
1482
|
+
"borderTopLeftRadius",
|
|
1483
|
+
"borderTopRightRadius",
|
|
1484
|
+
"borderBottomRightRadius",
|
|
1485
|
+
"borderBottomLeftRadius"
|
|
1486
|
+
];
|
|
1487
|
+
for (let i = 0; i < 4; i++) {
|
|
1488
|
+
if (style[corners[i]] === void 0)
|
|
1489
|
+
style[corners[i]] = parseValue(sides[i]);
|
|
1490
|
+
}
|
|
1491
|
+
delete style.borderRadius;
|
|
1492
|
+
}
|
|
1493
|
+
if (style.borderWidth !== void 0) {
|
|
1494
|
+
const sides = parseSides(String(style.borderWidth));
|
|
1495
|
+
for (let i = 0; i < 4; i++) {
|
|
1496
|
+
const key = `border${SIDES[i]}Width`;
|
|
1497
|
+
if (style[key] === void 0) style[key] = parseValue(sides[i]);
|
|
1498
|
+
}
|
|
1499
|
+
delete style.borderWidth;
|
|
1500
|
+
}
|
|
1501
|
+
if (style.borderColor !== void 0) {
|
|
1502
|
+
const val = style.borderColor;
|
|
1503
|
+
for (const side of SIDES) {
|
|
1504
|
+
const key = `border${side}Color`;
|
|
1505
|
+
if (style[key] === void 0) style[key] = val;
|
|
1506
|
+
}
|
|
1507
|
+
delete style.borderColor;
|
|
1508
|
+
}
|
|
1509
|
+
if (style.borderStyle !== void 0) {
|
|
1510
|
+
const val = style.borderStyle;
|
|
1511
|
+
for (const side of SIDES) {
|
|
1512
|
+
const key = `border${side}Style`;
|
|
1513
|
+
if (style[key] === void 0) style[key] = val;
|
|
1514
|
+
}
|
|
1515
|
+
delete style.borderStyle;
|
|
1516
|
+
}
|
|
1517
|
+
if (style.border !== void 0) {
|
|
1518
|
+
const parts = String(style.border).split(/\s+/);
|
|
1519
|
+
const width = parseValue(parts[0]);
|
|
1520
|
+
const borderStyle = parts[1] ?? "solid";
|
|
1521
|
+
const color = parts[2] ?? "black";
|
|
1522
|
+
for (const side of SIDES) {
|
|
1523
|
+
if (style[`border${side}Width`] === void 0)
|
|
1524
|
+
style[`border${side}Width`] = width;
|
|
1525
|
+
if (style[`border${side}Style`] === void 0)
|
|
1526
|
+
style[`border${side}Style`] = borderStyle;
|
|
1527
|
+
if (style[`border${side}Color`] === void 0)
|
|
1528
|
+
style[`border${side}Color`] = color;
|
|
1529
|
+
}
|
|
1530
|
+
delete style.border;
|
|
1531
|
+
}
|
|
1532
|
+
if (style.flex !== void 0) {
|
|
1533
|
+
const val = String(style.flex);
|
|
1534
|
+
const parts = val.split(/\s+/);
|
|
1535
|
+
if (parts.length === 1) {
|
|
1536
|
+
const n = parseFloat(parts[0]);
|
|
1537
|
+
if (!isNaN(n)) {
|
|
1538
|
+
if (style.flexGrow === void 0) style.flexGrow = n;
|
|
1539
|
+
if (style.flexShrink === void 0) style.flexShrink = 1;
|
|
1540
|
+
if (style.flexBasis === void 0) style.flexBasis = 0;
|
|
1541
|
+
}
|
|
1542
|
+
} else if (parts.length === 2) {
|
|
1543
|
+
if (style.flexGrow === void 0) style.flexGrow = parseFloat(parts[0]);
|
|
1544
|
+
if (style.flexShrink === void 0)
|
|
1545
|
+
style.flexShrink = parseFloat(parts[1]);
|
|
1546
|
+
} else if (parts.length >= 3) {
|
|
1547
|
+
if (style.flexGrow === void 0) style.flexGrow = parseFloat(parts[0]);
|
|
1548
|
+
if (style.flexShrink === void 0)
|
|
1549
|
+
style.flexShrink = parseFloat(parts[1]);
|
|
1550
|
+
if (style.flexBasis === void 0) style.flexBasis = parseValue(parts[2]);
|
|
1551
|
+
}
|
|
1552
|
+
delete style.flex;
|
|
1553
|
+
}
|
|
1554
|
+
if (style.gap !== void 0) {
|
|
1555
|
+
const sides = parseSides(String(style.gap));
|
|
1556
|
+
if (style.rowGap === void 0) style.rowGap = parseValue(sides[0]);
|
|
1557
|
+
if (style.columnGap === void 0) style.columnGap = parseValue(sides[1]);
|
|
1558
|
+
delete style.gap;
|
|
1559
|
+
}
|
|
1560
|
+
if (style.background !== void 0) {
|
|
1561
|
+
const bg = String(style.background);
|
|
1562
|
+
if (bg.includes("gradient(") || bg.includes("url(")) {
|
|
1563
|
+
if (style.backgroundImage === void 0) style.backgroundImage = bg;
|
|
1564
|
+
} else {
|
|
1565
|
+
if (style.backgroundColor === void 0) style.backgroundColor = bg;
|
|
1566
|
+
}
|
|
1567
|
+
delete style.background;
|
|
1568
|
+
}
|
|
1569
|
+
if (style.overflow !== void 0) {
|
|
1570
|
+
if (style.overflowX === void 0) style.overflowX = style.overflow;
|
|
1571
|
+
if (style.overflowY === void 0) style.overflowY = style.overflow;
|
|
1572
|
+
}
|
|
1573
|
+
if (typeof style.fontFamily === "string") {
|
|
1574
|
+
style.fontFamily = style.fontFamily.split(",").map((s) => s.trim().replace(/^['"]|['"]$/g, "")).filter(Boolean).join(", ");
|
|
1575
|
+
}
|
|
1576
|
+
return style;
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
// src/jsx/style/compute.ts
|
|
1580
|
+
var DEFAULT_STYLE = {
|
|
1581
|
+
display: "flex",
|
|
1582
|
+
flexDirection: "row",
|
|
1583
|
+
flexWrap: "nowrap",
|
|
1584
|
+
flexGrow: 0,
|
|
1585
|
+
flexShrink: 0,
|
|
1586
|
+
alignItems: "stretch",
|
|
1587
|
+
justifyContent: "flex-start",
|
|
1588
|
+
position: "relative",
|
|
1589
|
+
fontSize: 16,
|
|
1590
|
+
fontWeight: 400,
|
|
1591
|
+
fontStyle: "normal",
|
|
1592
|
+
color: "black",
|
|
1593
|
+
lineHeight: "normal",
|
|
1594
|
+
textAlign: "left",
|
|
1595
|
+
whiteSpace: "normal",
|
|
1596
|
+
wordBreak: "normal",
|
|
1597
|
+
textOverflow: "clip",
|
|
1598
|
+
opacity: 1,
|
|
1599
|
+
overflow: "visible"
|
|
1600
|
+
};
|
|
1601
|
+
var INHERITABLE_PROPS = [
|
|
1602
|
+
"color",
|
|
1603
|
+
"fontSize",
|
|
1604
|
+
"fontFamily",
|
|
1605
|
+
"fontWeight",
|
|
1606
|
+
"fontStyle",
|
|
1607
|
+
"textAlign",
|
|
1608
|
+
"textTransform",
|
|
1609
|
+
"textDecoration",
|
|
1610
|
+
"lineHeight",
|
|
1611
|
+
"letterSpacing",
|
|
1612
|
+
"whiteSpace",
|
|
1613
|
+
"wordBreak",
|
|
1614
|
+
"textOverflow"
|
|
1615
|
+
];
|
|
1616
|
+
function resolveStyle(rawStyle, parentStyle) {
|
|
1617
|
+
const style = { ...rawStyle };
|
|
1618
|
+
for (const prop of INHERITABLE_PROPS) {
|
|
1619
|
+
if (style[prop] === void 0 && parentStyle[prop] !== void 0) {
|
|
1620
|
+
style[prop] = parentStyle[prop];
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
if (typeof style.fontSize === "string") {
|
|
1624
|
+
const parsed = parseFloat(style.fontSize);
|
|
1625
|
+
style.fontSize = isNaN(parsed) ? parentStyle.fontSize : parsed;
|
|
1626
|
+
}
|
|
1627
|
+
if (typeof style.lineHeight === "string") {
|
|
1628
|
+
const str = style.lineHeight;
|
|
1629
|
+
if (str.endsWith("%")) {
|
|
1630
|
+
style.lineHeight = parseFloat(str) / 100;
|
|
1631
|
+
} else {
|
|
1632
|
+
const parsed = parseFloat(str);
|
|
1633
|
+
if (!isNaN(parsed)) {
|
|
1634
|
+
style.lineHeight = parsed;
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
if (typeof style.letterSpacing === "string") {
|
|
1639
|
+
const parsed = parseFloat(style.letterSpacing);
|
|
1640
|
+
if (!isNaN(parsed)) {
|
|
1641
|
+
style.letterSpacing = parsed;
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
return style;
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
// src/jsx/yoga.ts
|
|
1648
|
+
import Yoga, {
|
|
1649
|
+
Align,
|
|
1650
|
+
Display,
|
|
1651
|
+
Edge,
|
|
1652
|
+
FlexDirection,
|
|
1653
|
+
Gutter,
|
|
1654
|
+
Justify,
|
|
1655
|
+
Overflow,
|
|
1656
|
+
PositionType,
|
|
1657
|
+
Wrap
|
|
1658
|
+
} from "yoga-layout";
|
|
1659
|
+
function createYogaNode() {
|
|
1660
|
+
return Yoga.Node.create();
|
|
1661
|
+
}
|
|
1662
|
+
function freeYogaNode(node) {
|
|
1663
|
+
node.freeRecursive();
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
// src/jsx/style/properties.ts
|
|
1667
|
+
function applyStylesToYoga(node, style) {
|
|
1668
|
+
if (style.display === "none") {
|
|
1669
|
+
node.setDisplay(Display.None);
|
|
1670
|
+
} else {
|
|
1671
|
+
node.setDisplay(Display.Flex);
|
|
1672
|
+
}
|
|
1673
|
+
{
|
|
1674
|
+
const map = {
|
|
1675
|
+
row: FlexDirection.Row,
|
|
1676
|
+
"row-reverse": FlexDirection.RowReverse,
|
|
1677
|
+
column: FlexDirection.Column,
|
|
1678
|
+
"column-reverse": FlexDirection.ColumnReverse
|
|
1679
|
+
};
|
|
1680
|
+
node.setFlexDirection(
|
|
1681
|
+
map[style.flexDirection ?? "row"] ?? FlexDirection.Row
|
|
1682
|
+
);
|
|
1683
|
+
}
|
|
1684
|
+
if (style.justifyContent) {
|
|
1685
|
+
const map = {
|
|
1686
|
+
"flex-start": Justify.FlexStart,
|
|
1687
|
+
"flex-end": Justify.FlexEnd,
|
|
1688
|
+
center: Justify.Center,
|
|
1689
|
+
"space-between": Justify.SpaceBetween,
|
|
1690
|
+
"space-around": Justify.SpaceAround,
|
|
1691
|
+
"space-evenly": Justify.SpaceEvenly
|
|
1692
|
+
};
|
|
1693
|
+
const jc = map[style.justifyContent];
|
|
1694
|
+
if (jc !== void 0) node.setJustifyContent(jc);
|
|
1695
|
+
}
|
|
1696
|
+
if (style.alignItems) {
|
|
1697
|
+
const map = {
|
|
1698
|
+
"flex-start": Align.FlexStart,
|
|
1699
|
+
"flex-end": Align.FlexEnd,
|
|
1700
|
+
center: Align.Center,
|
|
1701
|
+
stretch: Align.Stretch,
|
|
1702
|
+
baseline: Align.Baseline
|
|
1703
|
+
};
|
|
1704
|
+
const ai = map[style.alignItems];
|
|
1705
|
+
if (ai !== void 0) node.setAlignItems(ai);
|
|
1706
|
+
}
|
|
1707
|
+
if (style.alignSelf) {
|
|
1708
|
+
const map = {
|
|
1709
|
+
auto: Align.Auto,
|
|
1710
|
+
"flex-start": Align.FlexStart,
|
|
1711
|
+
"flex-end": Align.FlexEnd,
|
|
1712
|
+
center: Align.Center,
|
|
1713
|
+
stretch: Align.Stretch,
|
|
1714
|
+
baseline: Align.Baseline
|
|
1715
|
+
};
|
|
1716
|
+
const as_ = map[style.alignSelf];
|
|
1717
|
+
if (as_ !== void 0) node.setAlignSelf(as_);
|
|
1718
|
+
}
|
|
1719
|
+
if (style.alignContent) {
|
|
1720
|
+
const map = {
|
|
1721
|
+
"flex-start": Align.FlexStart,
|
|
1722
|
+
"flex-end": Align.FlexEnd,
|
|
1723
|
+
center: Align.Center,
|
|
1724
|
+
stretch: Align.Stretch,
|
|
1725
|
+
"space-between": Align.SpaceBetween,
|
|
1726
|
+
"space-around": Align.SpaceAround
|
|
1727
|
+
};
|
|
1728
|
+
const ac = map[style.alignContent];
|
|
1729
|
+
if (ac !== void 0) node.setAlignContent(ac);
|
|
1730
|
+
}
|
|
1731
|
+
if (style.flexWrap) {
|
|
1732
|
+
const map = {
|
|
1733
|
+
nowrap: Wrap.NoWrap,
|
|
1734
|
+
wrap: Wrap.Wrap,
|
|
1735
|
+
"wrap-reverse": Wrap.WrapReverse
|
|
1736
|
+
};
|
|
1737
|
+
const fw = map[style.flexWrap];
|
|
1738
|
+
if (fw !== void 0) node.setFlexWrap(fw);
|
|
1739
|
+
}
|
|
1740
|
+
if (style.flexGrow !== void 0) node.setFlexGrow(style.flexGrow);
|
|
1741
|
+
node.setFlexShrink(style.flexShrink ?? 0);
|
|
1742
|
+
if (style.flexBasis !== void 0) {
|
|
1743
|
+
if (typeof style.flexBasis === "number") {
|
|
1744
|
+
node.setFlexBasis(style.flexBasis);
|
|
1745
|
+
} else if (String(style.flexBasis).endsWith("%")) {
|
|
1746
|
+
node.setFlexBasis(String(style.flexBasis));
|
|
1747
|
+
} else if (style.flexBasis === "auto") {
|
|
1748
|
+
node.setFlexBasis("auto");
|
|
1749
|
+
} else {
|
|
1750
|
+
const n = parseFloat(String(style.flexBasis));
|
|
1751
|
+
if (!isNaN(n)) node.setFlexBasis(n);
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
applyDimension(node, "setWidth", style.width);
|
|
1755
|
+
applyDimension(node, "setHeight", style.height);
|
|
1756
|
+
applyDimension(node, "setMinWidth", style.minWidth);
|
|
1757
|
+
applyDimension(node, "setMinHeight", style.minHeight);
|
|
1758
|
+
applyDimension(node, "setMaxWidth", style.maxWidth);
|
|
1759
|
+
applyDimension(node, "setMaxHeight", style.maxHeight);
|
|
1760
|
+
if (style.position === "absolute") {
|
|
1761
|
+
node.setPositionType(PositionType.Absolute);
|
|
1762
|
+
} else {
|
|
1763
|
+
node.setPositionType(PositionType.Relative);
|
|
1764
|
+
}
|
|
1765
|
+
applyEdgeValue(node, "setPosition", Edge.Top, style.top);
|
|
1766
|
+
applyEdgeValue(node, "setPosition", Edge.Right, style.right);
|
|
1767
|
+
applyEdgeValue(node, "setPosition", Edge.Bottom, style.bottom);
|
|
1768
|
+
applyEdgeValue(node, "setPosition", Edge.Left, style.left);
|
|
1769
|
+
applyEdgeValue(node, "setMargin", Edge.Top, style.marginTop);
|
|
1770
|
+
applyEdgeValue(node, "setMargin", Edge.Right, style.marginRight);
|
|
1771
|
+
applyEdgeValue(node, "setMargin", Edge.Bottom, style.marginBottom);
|
|
1772
|
+
applyEdgeValue(node, "setMargin", Edge.Left, style.marginLeft);
|
|
1773
|
+
applyEdgeValue(node, "setPadding", Edge.Top, style.paddingTop);
|
|
1774
|
+
applyEdgeValue(node, "setPadding", Edge.Right, style.paddingRight);
|
|
1775
|
+
applyEdgeValue(node, "setPadding", Edge.Bottom, style.paddingBottom);
|
|
1776
|
+
applyEdgeValue(node, "setPadding", Edge.Left, style.paddingLeft);
|
|
1777
|
+
if (style.borderTopWidth !== void 0)
|
|
1778
|
+
node.setBorder(Edge.Top, style.borderTopWidth);
|
|
1779
|
+
if (style.borderRightWidth !== void 0)
|
|
1780
|
+
node.setBorder(Edge.Right, style.borderRightWidth);
|
|
1781
|
+
if (style.borderBottomWidth !== void 0)
|
|
1782
|
+
node.setBorder(Edge.Bottom, style.borderBottomWidth);
|
|
1783
|
+
if (style.borderLeftWidth !== void 0)
|
|
1784
|
+
node.setBorder(Edge.Left, style.borderLeftWidth);
|
|
1785
|
+
if (style.rowGap !== void 0) node.setGap(Gutter.Row, style.rowGap);
|
|
1786
|
+
if (style.columnGap !== void 0)
|
|
1787
|
+
node.setGap(Gutter.Column, style.columnGap);
|
|
1788
|
+
if (style.overflow === "hidden" || style.overflowX === "hidden" || style.overflowY === "hidden") {
|
|
1789
|
+
node.setOverflow(Overflow.Hidden);
|
|
1790
|
+
} else {
|
|
1791
|
+
node.setOverflow(Overflow.Visible);
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
function applyDimension(node, setter, value) {
|
|
1795
|
+
if (value === void 0) return;
|
|
1796
|
+
if (value === "auto") {
|
|
1797
|
+
if (setter === "setWidth" || setter === "setHeight") {
|
|
1798
|
+
node[setter]("auto");
|
|
1799
|
+
}
|
|
1800
|
+
return;
|
|
1801
|
+
}
|
|
1802
|
+
if (typeof value === "number") {
|
|
1803
|
+
node[setter](value);
|
|
1804
|
+
return;
|
|
1805
|
+
}
|
|
1806
|
+
const s = String(value);
|
|
1807
|
+
if (s.endsWith("%")) {
|
|
1808
|
+
node[setter](s);
|
|
1809
|
+
} else {
|
|
1810
|
+
const n = parseFloat(s);
|
|
1811
|
+
if (!isNaN(n)) node[setter](n);
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
function applyEdgeValue(node, setter, edge, value) {
|
|
1815
|
+
if (value === void 0) return;
|
|
1816
|
+
if (value === "auto" && setter === "setMargin") {
|
|
1817
|
+
node.setMargin(edge, "auto");
|
|
1818
|
+
return;
|
|
1819
|
+
}
|
|
1820
|
+
if (typeof value === "number") {
|
|
1821
|
+
node[setter](edge, value);
|
|
1822
|
+
return;
|
|
1823
|
+
}
|
|
1824
|
+
const s = String(value);
|
|
1825
|
+
if (s.endsWith("%")) {
|
|
1826
|
+
node[setter](edge, s);
|
|
1827
|
+
} else {
|
|
1828
|
+
const n = parseFloat(s);
|
|
1829
|
+
if (!isNaN(n)) node[setter](edge, n);
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
// src/jsx/layout.ts
|
|
1834
|
+
function buildLayoutTree(element, containerWidth, containerHeight, ctx, emojiEnabled) {
|
|
1835
|
+
const rootYogaNode = createYogaNode();
|
|
1836
|
+
const rootNode = buildNode(
|
|
1837
|
+
element,
|
|
1838
|
+
DEFAULT_STYLE,
|
|
1839
|
+
rootYogaNode,
|
|
1840
|
+
ctx,
|
|
1841
|
+
emojiEnabled
|
|
1842
|
+
);
|
|
1843
|
+
rootYogaNode.setWidth(containerWidth);
|
|
1844
|
+
rootYogaNode.setHeight(containerHeight);
|
|
1845
|
+
rootYogaNode.calculateLayout(containerWidth, containerHeight);
|
|
1846
|
+
const layoutTree = extractLayout(rootNode, rootYogaNode);
|
|
1847
|
+
freeYogaNode(rootYogaNode);
|
|
1848
|
+
return layoutTree;
|
|
1849
|
+
}
|
|
1850
|
+
function buildNode(element, parentStyle, yogaNode, ctx, emojiEnabled) {
|
|
1851
|
+
if (element === null || element === void 0 || typeof element === "boolean") {
|
|
1852
|
+
return {
|
|
1853
|
+
type: "empty",
|
|
1854
|
+
style: parentStyle,
|
|
1855
|
+
children: [],
|
|
1856
|
+
props: {},
|
|
1857
|
+
yogaNode
|
|
1858
|
+
};
|
|
1859
|
+
}
|
|
1860
|
+
if (typeof element === "string" || typeof element === "number") {
|
|
1861
|
+
const text = String(element);
|
|
1862
|
+
const style2 = resolveStyle(void 0, parentStyle);
|
|
1863
|
+
const measureFunc = createTextMeasureFunc(text, style2, ctx, emojiEnabled);
|
|
1864
|
+
yogaNode.setMeasureFunc(measureFunc);
|
|
1865
|
+
return {
|
|
1866
|
+
type: "text",
|
|
1867
|
+
style: style2,
|
|
1868
|
+
children: [],
|
|
1869
|
+
textContent: text,
|
|
1870
|
+
props: {},
|
|
1871
|
+
yogaNode
|
|
1872
|
+
};
|
|
1873
|
+
}
|
|
1874
|
+
if (Array.isArray(element)) {
|
|
1875
|
+
const style2 = resolveStyle(void 0, parentStyle);
|
|
1876
|
+
const children2 = [];
|
|
1877
|
+
for (let i = 0; i < element.length; i++) {
|
|
1878
|
+
const child = element[i];
|
|
1879
|
+
if (child === null || child === void 0 || typeof child === "boolean")
|
|
1880
|
+
continue;
|
|
1881
|
+
const childYogaNode = createYogaNode();
|
|
1882
|
+
yogaNode.insertChild(childYogaNode, children2.length);
|
|
1883
|
+
children2.push(buildNode(child, style2, childYogaNode, ctx, emojiEnabled));
|
|
1884
|
+
}
|
|
1885
|
+
return {
|
|
1886
|
+
type: "div",
|
|
1887
|
+
style: style2,
|
|
1888
|
+
children: children2,
|
|
1889
|
+
props: {},
|
|
1890
|
+
yogaNode
|
|
1891
|
+
};
|
|
1892
|
+
}
|
|
1893
|
+
const el = element;
|
|
1894
|
+
const type = el.type;
|
|
1895
|
+
if (typeof type === "function") {
|
|
1896
|
+
const rendered = type(
|
|
1897
|
+
el.props ?? {}
|
|
1898
|
+
);
|
|
1899
|
+
return buildNode(rendered, parentStyle, yogaNode, ctx, emojiEnabled);
|
|
1900
|
+
}
|
|
1901
|
+
const props = el.props ?? {};
|
|
1902
|
+
const rawStyle = props.style ?? {};
|
|
1903
|
+
const expanded = expandStyle(rawStyle);
|
|
1904
|
+
const style = resolveStyle(expanded, parentStyle);
|
|
1905
|
+
const tagName = String(type);
|
|
1906
|
+
if (tagName === "svg") {
|
|
1907
|
+
if (props.width != null && style.width === void 0)
|
|
1908
|
+
style.width = Number(props.width);
|
|
1909
|
+
if (props.height != null && style.height === void 0)
|
|
1910
|
+
style.height = Number(props.height);
|
|
1911
|
+
const viewBox = props.viewBox;
|
|
1912
|
+
if (viewBox) {
|
|
1913
|
+
const parts = viewBox.split(/[\s,]+/).map(Number);
|
|
1914
|
+
if (parts.length === 4) {
|
|
1915
|
+
const [, , vbW, vbH] = parts;
|
|
1916
|
+
if (vbW > 0 && vbH > 0) {
|
|
1917
|
+
const w = typeof style.width === "number" ? style.width : void 0;
|
|
1918
|
+
const h = typeof style.height === "number" ? style.height : void 0;
|
|
1919
|
+
if (w !== void 0 && h === void 0) {
|
|
1920
|
+
style.height = w * (vbH / vbW);
|
|
1921
|
+
} else if (h !== void 0 && w === void 0) {
|
|
1922
|
+
style.width = h * (vbW / vbH);
|
|
1923
|
+
} else if (w === void 0 && h === void 0) {
|
|
1924
|
+
style.width = vbW;
|
|
1925
|
+
style.height = vbH;
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
applyStylesToYoga(yogaNode, style);
|
|
1932
|
+
if (tagName === "svg") {
|
|
1933
|
+
return {
|
|
1934
|
+
type: tagName,
|
|
1935
|
+
style,
|
|
1936
|
+
children: [],
|
|
1937
|
+
props,
|
|
1938
|
+
yogaNode
|
|
1939
|
+
};
|
|
1940
|
+
}
|
|
1941
|
+
const textContent = extractTextContent(props.children);
|
|
1942
|
+
if (textContent !== void 0 && !hasElementChildren(props.children)) {
|
|
1943
|
+
const childStyle = resolveStyle(void 0, style);
|
|
1944
|
+
const childYogaNode = createYogaNode();
|
|
1945
|
+
const measureFunc = createTextMeasureFunc(
|
|
1946
|
+
textContent,
|
|
1947
|
+
childStyle,
|
|
1948
|
+
ctx,
|
|
1949
|
+
emojiEnabled
|
|
1950
|
+
);
|
|
1951
|
+
childYogaNode.setMeasureFunc(measureFunc);
|
|
1952
|
+
yogaNode.insertChild(childYogaNode, 0);
|
|
1953
|
+
return {
|
|
1954
|
+
type: tagName,
|
|
1955
|
+
style,
|
|
1956
|
+
children: [
|
|
1957
|
+
{
|
|
1958
|
+
type: "text",
|
|
1959
|
+
style: childStyle,
|
|
1960
|
+
children: [],
|
|
1961
|
+
textContent,
|
|
1962
|
+
props: {},
|
|
1963
|
+
yogaNode: childYogaNode
|
|
1964
|
+
}
|
|
1965
|
+
],
|
|
1966
|
+
props,
|
|
1967
|
+
yogaNode
|
|
1968
|
+
};
|
|
1969
|
+
}
|
|
1970
|
+
const children = [];
|
|
1971
|
+
const rawChildren = props.children;
|
|
1972
|
+
if (rawChildren !== void 0 && rawChildren !== null) {
|
|
1973
|
+
const childArray = Array.isArray(rawChildren) ? rawChildren : [rawChildren];
|
|
1974
|
+
for (const child of childArray) {
|
|
1975
|
+
if (child === null || child === void 0 || typeof child === "boolean")
|
|
1976
|
+
continue;
|
|
1977
|
+
const childYogaNode = createYogaNode();
|
|
1978
|
+
yogaNode.insertChild(childYogaNode, children.length);
|
|
1979
|
+
children.push(buildNode(child, style, childYogaNode, ctx, emojiEnabled));
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
return {
|
|
1983
|
+
type: tagName,
|
|
1984
|
+
style,
|
|
1985
|
+
children,
|
|
1986
|
+
props,
|
|
1987
|
+
yogaNode
|
|
1988
|
+
};
|
|
1989
|
+
}
|
|
1990
|
+
function extractLayout(node, yogaNode) {
|
|
1991
|
+
const layout = yogaNode.getComputedLayout();
|
|
1992
|
+
return {
|
|
1993
|
+
type: node.type,
|
|
1994
|
+
style: node.style,
|
|
1995
|
+
children: node.children.map((child, i) => {
|
|
1996
|
+
const childYoga = yogaNode.getChild(i);
|
|
1997
|
+
return extractLayout(child, childYoga);
|
|
1998
|
+
}),
|
|
1999
|
+
textContent: node.textContent,
|
|
2000
|
+
props: node.props,
|
|
2001
|
+
x: layout.left,
|
|
2002
|
+
y: layout.top,
|
|
2003
|
+
width: layout.width,
|
|
2004
|
+
height: layout.height
|
|
2005
|
+
};
|
|
2006
|
+
}
|
|
2007
|
+
function extractTextContent(children) {
|
|
2008
|
+
if (children === void 0 || children === null) return void 0;
|
|
2009
|
+
if (typeof children === "string") return children;
|
|
2010
|
+
if (typeof children === "number") return String(children);
|
|
2011
|
+
if (Array.isArray(children)) {
|
|
2012
|
+
const parts = [];
|
|
2013
|
+
for (const child of children) {
|
|
2014
|
+
if (typeof child === "string") {
|
|
2015
|
+
parts.push(child);
|
|
2016
|
+
} else if (typeof child === "number") {
|
|
2017
|
+
parts.push(String(child));
|
|
2018
|
+
} else if (child !== null && child !== void 0 && typeof child !== "boolean") {
|
|
2019
|
+
return void 0;
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
return parts.join("");
|
|
2023
|
+
}
|
|
2024
|
+
return void 0;
|
|
2025
|
+
}
|
|
2026
|
+
function hasElementChildren(children) {
|
|
2027
|
+
if (children === void 0 || children === null || typeof children === "boolean")
|
|
2028
|
+
return false;
|
|
2029
|
+
if (typeof children === "string" || typeof children === "number")
|
|
2030
|
+
return false;
|
|
2031
|
+
if (Array.isArray(children)) {
|
|
2032
|
+
return children.some(
|
|
2033
|
+
(child) => child !== null && child !== void 0 && typeof child !== "boolean" && typeof child !== "string" && typeof child !== "number"
|
|
2034
|
+
);
|
|
2035
|
+
}
|
|
2036
|
+
return typeof children === "object";
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
// src/jsx/index.ts
|
|
2040
|
+
async function renderReactElement(ctx, element, options) {
|
|
2041
|
+
ensureFontsRegistered(options.fonts);
|
|
2042
|
+
const width = ctx.canvas.width;
|
|
2043
|
+
const height = ctx.canvas.height;
|
|
2044
|
+
const emojiStyle = options.emoji === "none" ? void 0 : options.emoji ?? "twemoji";
|
|
2045
|
+
const layoutTree = buildLayoutTree(element, width, height, ctx, !!emojiStyle);
|
|
2046
|
+
await drawNode(ctx, layoutTree, 0, 0, options.debug ?? false, emojiStyle);
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
// src/index.ts
|
|
2050
|
+
function createCanvas2(width, height) {
|
|
2051
|
+
const canvas = _createCanvas(width, height);
|
|
2052
|
+
const origEncode = canvas.encode.bind(canvas);
|
|
2053
|
+
canvas.encode = (async (...args) => Buffer.from(await origEncode(...args)));
|
|
2054
|
+
return canvas;
|
|
2055
|
+
}
|
|
2056
|
+
export {
|
|
2057
|
+
Canvas,
|
|
2058
|
+
GlobalFonts2 as GlobalFonts,
|
|
2059
|
+
Image,
|
|
2060
|
+
LottieAnimation,
|
|
2061
|
+
createCanvas2 as createCanvas,
|
|
2062
|
+
loadImage4 as loadImage,
|
|
2063
|
+
loadLottie,
|
|
2064
|
+
registerFont,
|
|
2065
|
+
registerFontFromPath,
|
|
2066
|
+
registeredFamilies,
|
|
2067
|
+
renderLottieFrame,
|
|
2068
|
+
renderReactElement
|
|
2069
|
+
};
|
|
2070
|
+
//# sourceMappingURL=index.js.map
|