@codehz/draw-call 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/render.cjs ADDED
@@ -0,0 +1,1555 @@
1
+
2
+ //#region src/types/base.ts
3
+ function linearGradient(angle, ...stops) {
4
+ return {
5
+ type: "linear-gradient",
6
+ angle,
7
+ stops: stops.map((stop, index) => {
8
+ if (typeof stop === "string") return {
9
+ offset: stops.length > 1 ? index / (stops.length - 1) : 0,
10
+ color: stop
11
+ };
12
+ return {
13
+ offset: stop[0],
14
+ color: stop[1]
15
+ };
16
+ })
17
+ };
18
+ }
19
+ function radialGradient(options, ...stops) {
20
+ const colorStops = stops.map((stop, index) => {
21
+ if (typeof stop === "string") return {
22
+ offset: stops.length > 1 ? index / (stops.length - 1) : 0,
23
+ color: stop
24
+ };
25
+ return {
26
+ offset: stop[0],
27
+ color: stop[1]
28
+ };
29
+ });
30
+ return {
31
+ type: "radial-gradient",
32
+ ...options,
33
+ stops: colorStops
34
+ };
35
+ }
36
+ function normalizeSpacing(value) {
37
+ if (value === void 0) return {
38
+ top: 0,
39
+ right: 0,
40
+ bottom: 0,
41
+ left: 0
42
+ };
43
+ if (typeof value === "number") return {
44
+ top: value,
45
+ right: value,
46
+ bottom: value,
47
+ left: value
48
+ };
49
+ return {
50
+ top: value.top ?? 0,
51
+ right: value.right ?? 0,
52
+ bottom: value.bottom ?? 0,
53
+ left: value.left ?? 0
54
+ };
55
+ }
56
+ function normalizeBorderRadius(value) {
57
+ if (value === void 0) return [
58
+ 0,
59
+ 0,
60
+ 0,
61
+ 0
62
+ ];
63
+ if (typeof value === "number") return [
64
+ value,
65
+ value,
66
+ value,
67
+ value
68
+ ];
69
+ return value;
70
+ }
71
+
72
+ //#endregion
73
+ //#region src/layout/components/box.ts
74
+ function calcEffectiveSize(element, padding, availableWidth) {
75
+ return {
76
+ width: typeof element.width === "number" ? element.width - padding.left - padding.right : availableWidth > 0 ? availableWidth : 0,
77
+ height: typeof element.height === "number" ? element.height - padding.top - padding.bottom : 0
78
+ };
79
+ }
80
+ function collectChildSizes(children, ctx, availableWidth, padding, measureChild) {
81
+ const childSizes = [];
82
+ for (const child of children) {
83
+ const childMargin = normalizeSpacing(child.margin);
84
+ const childSize = measureChild(child, ctx, availableWidth - padding.left - padding.right - childMargin.left - childMargin.right);
85
+ childSizes.push({
86
+ width: childSize.width,
87
+ height: childSize.height,
88
+ margin: childMargin
89
+ });
90
+ }
91
+ return childSizes;
92
+ }
93
+ function measureWrappedContent(childSizes, gap, availableMain, isRow) {
94
+ let currentMain = 0;
95
+ let currentCross = 0;
96
+ let totalCross = 0;
97
+ let maxMain = 0;
98
+ let lineCount = 0;
99
+ for (let i = 0; i < childSizes.length; i++) {
100
+ const { width, height, margin } = childSizes[i];
101
+ const itemMain = isRow ? width + margin.left + margin.right : height + margin.top + margin.bottom;
102
+ const itemCross = isRow ? height + margin.top + margin.bottom : width + margin.left + margin.right;
103
+ if (lineCount > 0 && currentMain + gap + itemMain > availableMain) {
104
+ totalCross += currentCross;
105
+ maxMain = Math.max(maxMain, currentMain);
106
+ lineCount++;
107
+ currentMain = itemMain;
108
+ currentCross = itemCross;
109
+ } else {
110
+ if (lineCount > 0 || i > 0) currentMain += gap;
111
+ currentMain += itemMain;
112
+ currentCross = Math.max(currentCross, itemCross);
113
+ if (i === 0) lineCount = 1;
114
+ }
115
+ }
116
+ if (childSizes.length > 0) {
117
+ totalCross += currentCross;
118
+ maxMain = Math.max(maxMain, currentMain);
119
+ }
120
+ if (lineCount > 1) totalCross += gap * (lineCount - 1);
121
+ return isRow ? {
122
+ width: maxMain,
123
+ height: totalCross
124
+ } : {
125
+ width: totalCross,
126
+ height: maxMain
127
+ };
128
+ }
129
+ /**
130
+ * 测量 Box 元素的固有尺寸
131
+ */
132
+ function measureBoxSize(element, ctx, availableWidth, measureChild) {
133
+ const padding = normalizeSpacing(element.padding);
134
+ const gap = element.gap ?? 0;
135
+ const direction = element.direction ?? "row";
136
+ const wrap = element.wrap ?? false;
137
+ const isRow = direction === "row" || direction === "row-reverse";
138
+ let contentWidth = 0;
139
+ let contentHeight = 0;
140
+ const children = element.children ?? [];
141
+ const { width: effectiveWidth, height: effectiveHeight } = calcEffectiveSize(element, padding, availableWidth);
142
+ if (wrap && isRow && effectiveWidth > 0) {
143
+ const wrapped = measureWrappedContent(collectChildSizes(children, ctx, availableWidth, padding, measureChild), gap, effectiveWidth, true);
144
+ contentWidth = wrapped.width;
145
+ contentHeight = wrapped.height;
146
+ } else if (wrap && !isRow && effectiveHeight > 0) {
147
+ const wrapped = measureWrappedContent(collectChildSizes(children, ctx, availableWidth, padding, measureChild), gap, effectiveHeight, false);
148
+ contentWidth = wrapped.width;
149
+ contentHeight = wrapped.height;
150
+ } else for (let i = 0; i < children.length; i++) {
151
+ const child = children[i];
152
+ const childMargin = normalizeSpacing(child.margin);
153
+ const childSize = measureChild(child, ctx, availableWidth - padding.left - padding.right - childMargin.left - childMargin.right);
154
+ if (isRow) {
155
+ contentWidth += childSize.width + childMargin.left + childMargin.right;
156
+ contentHeight = Math.max(contentHeight, childSize.height + childMargin.top + childMargin.bottom);
157
+ if (i > 0) contentWidth += gap;
158
+ } else {
159
+ contentHeight += childSize.height + childMargin.top + childMargin.bottom;
160
+ contentWidth = Math.max(contentWidth, childSize.width + childMargin.left + childMargin.right);
161
+ if (i > 0) contentHeight += gap;
162
+ }
163
+ }
164
+ const intrinsicWidth = contentWidth + padding.left + padding.right;
165
+ const intrinsicHeight = contentHeight + padding.top + padding.bottom;
166
+ return {
167
+ width: typeof element.width === "number" ? element.width : intrinsicWidth,
168
+ height: typeof element.height === "number" ? element.height : intrinsicHeight
169
+ };
170
+ }
171
+
172
+ //#endregion
173
+ //#region src/layout/components/image.ts
174
+ /**
175
+ * 测量 Image 元素的固有尺寸
176
+ */
177
+ function measureImageSize(element, _ctx, _availableWidth) {
178
+ const imageElement = element;
179
+ if (imageElement.width !== void 0 && imageElement.height !== void 0) return {
180
+ width: typeof imageElement.width === "number" ? imageElement.width : 0,
181
+ height: typeof imageElement.height === "number" ? imageElement.height : 0
182
+ };
183
+ const src = imageElement.src;
184
+ if (src) {
185
+ const imgWidth = "naturalWidth" in src ? src.naturalWidth : "width" in src ? +src.width : 0;
186
+ const imgHeight = "naturalHeight" in src ? src.naturalHeight : "height" in src ? +src.height : 0;
187
+ if (imgWidth > 0 && imgHeight > 0) return {
188
+ width: imgWidth,
189
+ height: imgHeight
190
+ };
191
+ }
192
+ return {
193
+ width: 0,
194
+ height: 0
195
+ };
196
+ }
197
+
198
+ //#endregion
199
+ //#region src/layout/components/richtext.ts
200
+ /**
201
+ * 合并 span 样式和元素级别样式
202
+ * 优先级:span 样式 > 元素样式 > 默认值
203
+ * font 属性进行深度合并,允许 span 部分覆盖 element 的 font
204
+ */
205
+ function mergeSpanStyle(span, elementStyle) {
206
+ return {
207
+ font: {
208
+ ...elementStyle.font || {},
209
+ ...span.font || {}
210
+ },
211
+ color: span.color ?? elementStyle.color,
212
+ background: span.background ?? elementStyle.background,
213
+ underline: span.underline ?? elementStyle.underline ?? false,
214
+ strikethrough: span.strikethrough ?? elementStyle.strikethrough ?? false
215
+ };
216
+ }
217
+ /**
218
+ * 测量富文本元素的固有尺寸
219
+ */
220
+ function measureRichTextSize(element, ctx, availableWidth) {
221
+ const lineHeight = element.lineHeight ?? 1.2;
222
+ const elementStyle = {
223
+ font: element.font,
224
+ color: element.color,
225
+ background: element.background,
226
+ underline: element.underline,
227
+ strikethrough: element.strikethrough
228
+ };
229
+ const richLines = wrapRichText(ctx, element.spans, availableWidth, lineHeight, elementStyle);
230
+ let maxWidth = 0;
231
+ let totalHeight = 0;
232
+ for (const line of richLines) {
233
+ maxWidth = Math.max(maxWidth, line.width);
234
+ totalHeight += line.height;
235
+ }
236
+ return {
237
+ width: maxWidth,
238
+ height: totalHeight
239
+ };
240
+ }
241
+ /**
242
+ * 将富文本内容拆分为行
243
+ */
244
+ function wrapRichText(ctx, spans, maxWidth, lineHeightScale = 1.2, elementStyle = {}) {
245
+ const lines = [];
246
+ let currentSegments = [];
247
+ let currentLineWidth = 0;
248
+ const pushLine = () => {
249
+ if (currentSegments.length === 0) return;
250
+ let maxTopDist = 0;
251
+ let maxBottomDist = 0;
252
+ let maxLineHeight = 0;
253
+ for (const seg of currentSegments) {
254
+ const topDist = seg.ascent - seg.offset;
255
+ const bottomDist = seg.descent + seg.offset;
256
+ maxTopDist = Math.max(maxTopDist, topDist);
257
+ maxBottomDist = Math.max(maxBottomDist, bottomDist);
258
+ maxLineHeight = Math.max(maxLineHeight, seg.height);
259
+ }
260
+ const contentHeight = maxTopDist + maxBottomDist;
261
+ const finalHeight = Math.max(contentHeight, maxLineHeight);
262
+ const extra = (finalHeight - contentHeight) / 2;
263
+ lines.push({
264
+ segments: [...currentSegments],
265
+ width: currentLineWidth,
266
+ height: finalHeight,
267
+ baseline: maxTopDist + extra
268
+ });
269
+ currentSegments = [];
270
+ currentLineWidth = 0;
271
+ };
272
+ for (const span of spans) {
273
+ const mergedStyle = mergeSpanStyle(span, elementStyle);
274
+ const font = mergedStyle.font;
275
+ const lh = (font.size ?? 16) * lineHeightScale;
276
+ const words = span.text.split(/(\s+)/);
277
+ for (const word of words) {
278
+ if (word === "") continue;
279
+ if (/^\s+$/.test(word)) {
280
+ const metrics = ctx.measureText(word, font);
281
+ const wordWidth = metrics.width;
282
+ if (maxWidth > 0 && currentLineWidth + wordWidth > maxWidth && currentSegments.length > 0) pushLine();
283
+ currentSegments.push({
284
+ text: word,
285
+ font: mergedStyle.font,
286
+ color: mergedStyle.color,
287
+ background: mergedStyle.background,
288
+ underline: mergedStyle.underline,
289
+ strikethrough: mergedStyle.strikethrough,
290
+ width: wordWidth,
291
+ height: lh,
292
+ ascent: metrics.ascent,
293
+ descent: metrics.descent,
294
+ offset: metrics.offset
295
+ });
296
+ currentLineWidth += wordWidth;
297
+ } else {
298
+ const metrics = ctx.measureText(word, font);
299
+ const wordWidth = metrics.width;
300
+ if (maxWidth <= 0 || currentLineWidth + wordWidth <= maxWidth) {
301
+ currentSegments.push({
302
+ text: word,
303
+ font: mergedStyle.font,
304
+ color: mergedStyle.color,
305
+ background: mergedStyle.background,
306
+ underline: mergedStyle.underline,
307
+ strikethrough: mergedStyle.strikethrough,
308
+ width: wordWidth,
309
+ height: lh,
310
+ ascent: metrics.ascent,
311
+ descent: metrics.descent,
312
+ offset: metrics.offset
313
+ });
314
+ currentLineWidth += wordWidth;
315
+ } else {
316
+ if (currentSegments.length > 0) pushLine();
317
+ const remainingWidth = maxWidth;
318
+ let currentPos = 0;
319
+ while (currentPos < word.length) {
320
+ let bestLen = 0;
321
+ for (let len = word.length - currentPos; len > 0; len--) {
322
+ const substr = word.substring(currentPos, currentPos + len);
323
+ const m = ctx.measureText(substr, font);
324
+ if (currentLineWidth + m.width <= remainingWidth) {
325
+ bestLen = len;
326
+ if (len < word.length - currentPos) break;
327
+ }
328
+ }
329
+ if (bestLen === 0) {
330
+ if (currentSegments.length > 0) pushLine();
331
+ bestLen = 1;
332
+ }
333
+ const substr = word.substring(currentPos, currentPos + bestLen);
334
+ const m = ctx.measureText(substr, font);
335
+ currentSegments.push({
336
+ text: substr,
337
+ font: mergedStyle.font,
338
+ color: mergedStyle.color,
339
+ background: mergedStyle.background,
340
+ underline: mergedStyle.underline,
341
+ strikethrough: mergedStyle.strikethrough,
342
+ width: m.width,
343
+ height: lh,
344
+ ascent: m.ascent,
345
+ descent: m.descent,
346
+ offset: m.offset
347
+ });
348
+ currentLineWidth += m.width;
349
+ currentPos += bestLen;
350
+ if (currentPos < word.length && currentLineWidth >= remainingWidth) pushLine();
351
+ }
352
+ }
353
+ }
354
+ }
355
+ }
356
+ pushLine();
357
+ if (lines.length === 0) return [{
358
+ segments: [],
359
+ width: 0,
360
+ height: 0,
361
+ baseline: 0
362
+ }];
363
+ return lines;
364
+ }
365
+
366
+ //#endregion
367
+ //#region src/layout/components/stack.ts
368
+ /**
369
+ * 测量 Stack 元素的固有尺寸
370
+ */
371
+ function measureStackSize(element, ctx, availableWidth, measureChild) {
372
+ const padding = normalizeSpacing(element.padding);
373
+ let contentWidth = 0;
374
+ let contentHeight = 0;
375
+ const children = element.children ?? [];
376
+ for (const child of children) {
377
+ const childMargin = normalizeSpacing(child.margin);
378
+ const childSize = measureChild(child, ctx, availableWidth - padding.left - padding.right - childMargin.left - childMargin.right);
379
+ contentWidth = Math.max(contentWidth, childSize.width + childMargin.left + childMargin.right);
380
+ contentHeight = Math.max(contentHeight, childSize.height + childMargin.top + childMargin.bottom);
381
+ }
382
+ const intrinsicWidth = contentWidth + padding.left + padding.right;
383
+ const intrinsicHeight = contentHeight + padding.top + padding.bottom;
384
+ return {
385
+ width: typeof element.width === "number" ? element.width : intrinsicWidth,
386
+ height: typeof element.height === "number" ? element.height : intrinsicHeight
387
+ };
388
+ }
389
+
390
+ //#endregion
391
+ //#region src/layout/components/svg.ts
392
+ /**
393
+ * 测量 SVG 元素的固有尺寸
394
+ */
395
+ function measureSvgSize(element, _ctx, _availableWidth) {
396
+ const svgElement = element;
397
+ if (svgElement.width !== void 0 && svgElement.height !== void 0) return {
398
+ width: typeof svgElement.width === "number" ? svgElement.width : 0,
399
+ height: typeof svgElement.height === "number" ? svgElement.height : 0
400
+ };
401
+ if (svgElement.viewBox) {
402
+ const viewBoxWidth = svgElement.viewBox.width;
403
+ const viewBoxHeight = svgElement.viewBox.height;
404
+ const aspectRatio = viewBoxWidth / viewBoxHeight;
405
+ if (svgElement.width !== void 0 && typeof svgElement.width === "number") return {
406
+ width: svgElement.width,
407
+ height: svgElement.width / aspectRatio
408
+ };
409
+ if (svgElement.height !== void 0 && typeof svgElement.height === "number") return {
410
+ width: svgElement.height * aspectRatio,
411
+ height: svgElement.height
412
+ };
413
+ return {
414
+ width: viewBoxWidth,
415
+ height: viewBoxHeight
416
+ };
417
+ }
418
+ return {
419
+ width: 0,
420
+ height: 0
421
+ };
422
+ }
423
+
424
+ //#endregion
425
+ //#region src/render/utils/font.ts
426
+ /**
427
+ * 构建 Canvas 字体字符串
428
+ * @param font 字体属性
429
+ * @returns CSS 字体字符串
430
+ */
431
+ function buildFontString(font) {
432
+ return `${font.style ?? "normal"} ${font.weight ?? "normal"} ${font.size ?? 16}px ${font.family ?? "sans-serif"}`;
433
+ }
434
+
435
+ //#endregion
436
+ //#region src/layout/utils/measure.ts
437
+ function createCanvasMeasureContext(ctx) {
438
+ return { measureText(text, font) {
439
+ ctx.font = buildFontString(font);
440
+ ctx.textBaseline = "middle";
441
+ const metrics = ctx.measureText(text);
442
+ const height = metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent;
443
+ const fontSize = font.size || 16;
444
+ return {
445
+ width: metrics.width,
446
+ height: height || fontSize,
447
+ offset: (metrics.actualBoundingBoxAscent - metrics.actualBoundingBoxDescent) / 2,
448
+ ascent: metrics.actualBoundingBoxAscent,
449
+ descent: metrics.actualBoundingBoxDescent
450
+ };
451
+ } };
452
+ }
453
+ function wrapText(ctx, text, maxWidth, font) {
454
+ if (maxWidth <= 0) {
455
+ const { offset } = ctx.measureText(text, font);
456
+ return {
457
+ lines: [text],
458
+ offsets: [offset]
459
+ };
460
+ }
461
+ const lines = [];
462
+ const offsets = [];
463
+ const paragraphs = text.split("\n");
464
+ for (const paragraph of paragraphs) {
465
+ if (paragraph === "") {
466
+ lines.push("");
467
+ offsets.push(0);
468
+ continue;
469
+ }
470
+ const words = paragraph.split(/(\s+)/);
471
+ let currentLine = "";
472
+ for (const word of words) {
473
+ const testLine = currentLine + word;
474
+ const { width } = ctx.measureText(testLine, font);
475
+ if (width > maxWidth && currentLine !== "") {
476
+ const trimmed = currentLine.trim();
477
+ lines.push(trimmed);
478
+ offsets.push(ctx.measureText(trimmed, font).offset);
479
+ currentLine = word.trimStart();
480
+ } else currentLine = testLine;
481
+ }
482
+ if (currentLine) {
483
+ const trimmed = currentLine.trim();
484
+ lines.push(trimmed);
485
+ offsets.push(ctx.measureText(trimmed, font).offset);
486
+ }
487
+ }
488
+ return lines.length > 0 ? {
489
+ lines,
490
+ offsets
491
+ } : {
492
+ lines: [""],
493
+ offsets: [0]
494
+ };
495
+ }
496
+ function truncateText(ctx, text, maxWidth, font, ellipsis = "...") {
497
+ const measured = ctx.measureText(text, font);
498
+ if (measured.width <= maxWidth) return {
499
+ text,
500
+ offset: measured.offset
501
+ };
502
+ const availableWidth = maxWidth - ctx.measureText(ellipsis, font).width;
503
+ if (availableWidth <= 0) {
504
+ const { offset } = ctx.measureText(ellipsis, font);
505
+ return {
506
+ text: ellipsis,
507
+ offset
508
+ };
509
+ }
510
+ let left = 0;
511
+ let right = text.length;
512
+ while (left < right) {
513
+ const mid = Math.floor((left + right + 1) / 2);
514
+ const truncated = text.slice(0, mid);
515
+ const { width: truncatedWidth } = ctx.measureText(truncated, font);
516
+ if (truncatedWidth <= availableWidth) left = mid;
517
+ else right = mid - 1;
518
+ }
519
+ const result = text.slice(0, left) + ellipsis;
520
+ const { offset } = ctx.measureText(result, font);
521
+ return {
522
+ text: result,
523
+ offset
524
+ };
525
+ }
526
+
527
+ //#endregion
528
+ //#region src/layout/components/text.ts
529
+ /**
530
+ * 测量文本元素的固有尺寸
531
+ */
532
+ function measureTextSize(element, ctx, availableWidth) {
533
+ const font = element.font ?? {};
534
+ const lineHeightPx = (font.size ?? 16) * (element.lineHeight ?? 1.2);
535
+ if (element.wrap && availableWidth > 0 && availableWidth < Infinity) {
536
+ const { lines } = wrapText(ctx, element.content, availableWidth, font);
537
+ const { width: maxLineWidth } = lines.reduce((max, line) => {
538
+ const { width } = ctx.measureText(line, font);
539
+ return width > max.width ? { width } : max;
540
+ }, { width: 0 });
541
+ return {
542
+ width: maxLineWidth,
543
+ height: lines.length * lineHeightPx
544
+ };
545
+ }
546
+ const { width, height } = ctx.measureText(element.content, font);
547
+ return {
548
+ width,
549
+ height: Math.max(height, lineHeightPx)
550
+ };
551
+ }
552
+
553
+ //#endregion
554
+ //#region src/layout/components/index.ts
555
+ /**
556
+ * 计算元素的固有尺寸(不依赖父容器的尺寸)
557
+ */
558
+ function measureIntrinsicSize(element, ctx, availableWidth) {
559
+ switch (element.type) {
560
+ case "text": return measureTextSize(element, ctx, availableWidth);
561
+ case "richtext": return measureRichTextSize(element, ctx, availableWidth);
562
+ case "box": return measureBoxSize(element, ctx, availableWidth, measureIntrinsicSize);
563
+ case "stack": return measureStackSize(element, ctx, availableWidth, measureIntrinsicSize);
564
+ case "image": return measureImageSize(element, ctx, availableWidth);
565
+ case "svg": return measureSvgSize(element, ctx, availableWidth);
566
+ default: return {
567
+ width: 0,
568
+ height: 0
569
+ };
570
+ }
571
+ }
572
+
573
+ //#endregion
574
+ //#region src/layout/utils/offset.ts
575
+ /**
576
+ * 递归地为布局节点及其子节点应用位置偏移
577
+ */
578
+ function applyOffset(node, dx, dy) {
579
+ node.layout.x += dx;
580
+ node.layout.y += dy;
581
+ node.layout.contentX += dx;
582
+ node.layout.contentY += dy;
583
+ for (const child of node.children) applyOffset(child, dx, dy);
584
+ }
585
+
586
+ //#endregion
587
+ //#region src/types/layout.ts
588
+ function resolveSize(size, available, auto) {
589
+ if (size === void 0 || size === "auto") return auto;
590
+ if (size === "fill") return available;
591
+ if (typeof size === "number") return size;
592
+ return available * parseFloat(size) / 100;
593
+ }
594
+ function sizeNeedsParent(size) {
595
+ if (size === void 0 || size === "auto") return false;
596
+ if (size === "fill") return true;
597
+ if (typeof size === "string" && size.endsWith("%")) return true;
598
+ return false;
599
+ }
600
+
601
+ //#endregion
602
+ //#region src/layout/engine.ts
603
+ function computeLayout(element, ctx, constraints, x = 0, y = 0) {
604
+ const margin = normalizeSpacing(element.margin);
605
+ const padding = normalizeSpacing("padding" in element ? element.padding : void 0);
606
+ const availableWidth = constraints.maxWidth - margin.left - margin.right;
607
+ const availableHeight = constraints.maxHeight - margin.top - margin.bottom;
608
+ const intrinsic = measureIntrinsicSize(element, ctx, availableWidth);
609
+ let width = constraints.minWidth === constraints.maxWidth && constraints.minWidth > 0 ? constraints.maxWidth - margin.left - margin.right : resolveSize(element.width, availableWidth, intrinsic.width);
610
+ let height = constraints.minHeight === constraints.maxHeight && constraints.minHeight > 0 ? constraints.maxHeight - margin.top - margin.bottom : resolveSize(element.height, availableHeight, intrinsic.height);
611
+ if (element.minWidth !== void 0) width = Math.max(width, element.minWidth);
612
+ if (element.maxWidth !== void 0) width = Math.min(width, element.maxWidth);
613
+ if (element.minHeight !== void 0) height = Math.max(height, element.minHeight);
614
+ if (element.maxHeight !== void 0) height = Math.min(height, element.maxHeight);
615
+ const actualX = x + margin.left;
616
+ const actualY = y + margin.top;
617
+ const contentX = actualX + padding.left;
618
+ const contentY = actualY + padding.top;
619
+ const contentWidth = width - padding.left - padding.right;
620
+ const contentHeight = height - padding.top - padding.bottom;
621
+ const node = {
622
+ element,
623
+ layout: {
624
+ x: actualX,
625
+ y: actualY,
626
+ width,
627
+ height,
628
+ contentX,
629
+ contentY,
630
+ contentWidth,
631
+ contentHeight
632
+ },
633
+ children: []
634
+ };
635
+ if (element.type === "text") {
636
+ const font = element.font ?? {};
637
+ if (element.wrap && contentWidth > 0) {
638
+ let { lines, offsets } = wrapText(ctx, element.content, contentWidth, font);
639
+ if (element.maxLines && lines.length > element.maxLines) {
640
+ lines = lines.slice(0, element.maxLines);
641
+ offsets = offsets.slice(0, element.maxLines);
642
+ if (element.ellipsis && lines.length > 0) {
643
+ const lastIdx = lines.length - 1;
644
+ const truncated = truncateText(ctx, lines[lastIdx], contentWidth, font);
645
+ lines[lastIdx] = truncated.text;
646
+ offsets[lastIdx] = truncated.offset;
647
+ }
648
+ }
649
+ node.lines = lines;
650
+ node.lineOffsets = offsets;
651
+ } else {
652
+ const { text, offset } = truncateText(ctx, element.content, contentWidth > 0 && element.ellipsis ? contentWidth : Infinity, font);
653
+ node.lines = [text];
654
+ node.lineOffsets = [offset];
655
+ }
656
+ }
657
+ if (element.type === "richtext") {
658
+ const lineHeight = element.lineHeight ?? 1.2;
659
+ let lines = wrapRichText(ctx, element.spans, contentWidth, lineHeight);
660
+ if (element.maxLines && lines.length > element.maxLines) {
661
+ lines = lines.slice(0, element.maxLines);
662
+ if (element.ellipsis && lines.length > 0) {
663
+ const lastLine = lines[lines.length - 1];
664
+ if (lastLine.segments.length > 0) {
665
+ const lastSeg = lastLine.segments[lastLine.segments.length - 1];
666
+ lastSeg.text += "...";
667
+ lastSeg.width = ctx.measureText(lastSeg.text, lastSeg.font ?? {}).width;
668
+ lastLine.width = lastLine.segments.reduce((sum, s) => sum + s.width, 0);
669
+ }
670
+ }
671
+ }
672
+ node.richLines = lines;
673
+ }
674
+ if (element.type === "box" || element.type === "stack") {
675
+ const children = element.children ?? [];
676
+ if (element.type === "stack") {
677
+ const stackAlign = element.align ?? "start";
678
+ const stackJustify = element.justify ?? "start";
679
+ for (const child of children) {
680
+ const childNode = computeLayout(child, ctx, {
681
+ minWidth: 0,
682
+ maxWidth: contentWidth,
683
+ minHeight: 0,
684
+ maxHeight: contentHeight
685
+ }, contentX, contentY);
686
+ const childMargin = normalizeSpacing(child.margin);
687
+ const childOuterWidth = childNode.layout.width + childMargin.left + childMargin.right;
688
+ const childOuterHeight = childNode.layout.height + childMargin.top + childMargin.bottom;
689
+ let offsetX = 0;
690
+ if (stackAlign === "center") offsetX = (contentWidth - childOuterWidth) / 2;
691
+ else if (stackAlign === "end") offsetX = contentWidth - childOuterWidth;
692
+ let offsetY = 0;
693
+ if (stackJustify === "center") offsetY = (contentHeight - childOuterHeight) / 2;
694
+ else if (stackJustify === "end") offsetY = contentHeight - childOuterHeight;
695
+ if (offsetX !== 0 || offsetY !== 0) applyOffset(childNode, offsetX, offsetY);
696
+ node.children.push(childNode);
697
+ }
698
+ } else {
699
+ const direction = element.direction ?? "row";
700
+ const justify = element.justify ?? "start";
701
+ const align = element.align ?? "stretch";
702
+ const gap = element.gap ?? 0;
703
+ const wrap = element.wrap ?? false;
704
+ const isRow = direction === "row" || direction === "row-reverse";
705
+ const isReverse = direction === "row-reverse" || direction === "column-reverse";
706
+ const getContentMainSize = () => isRow ? contentWidth : contentHeight;
707
+ const getContentCrossSize = () => isRow ? contentHeight : contentWidth;
708
+ const childInfos = [];
709
+ for (const child of children) {
710
+ const childMargin = normalizeSpacing(child.margin);
711
+ const childFlex = child.flex ?? 0;
712
+ if (childFlex > 0) childInfos.push({
713
+ element: child,
714
+ width: 0,
715
+ height: 0,
716
+ flex: childFlex,
717
+ margin: childMargin
718
+ });
719
+ else {
720
+ const size = measureIntrinsicSize(child, ctx, contentWidth - childMargin.left - childMargin.right);
721
+ const shouldStretchWidth = !isRow && child.width === void 0 && align === "stretch";
722
+ const shouldStretchHeight = isRow && child.height === void 0 && align === "stretch";
723
+ let w = sizeNeedsParent(child.width) ? resolveSize(child.width, contentWidth - childMargin.left - childMargin.right, size.width) : resolveSize(child.width, 0, size.width);
724
+ let h = sizeNeedsParent(child.height) ? resolveSize(child.height, contentHeight - childMargin.top - childMargin.bottom, size.height) : resolveSize(child.height, 0, size.height);
725
+ if (shouldStretchWidth && !wrap) w = contentWidth - childMargin.left - childMargin.right;
726
+ if (shouldStretchHeight && !wrap) h = contentHeight - childMargin.top - childMargin.bottom;
727
+ childInfos.push({
728
+ element: child,
729
+ width: w,
730
+ height: h,
731
+ flex: 0,
732
+ margin: childMargin
733
+ });
734
+ }
735
+ }
736
+ const lines = [];
737
+ if (wrap) {
738
+ let currentLine = [];
739
+ let currentLineSize = 0;
740
+ const mainAxisSize = getContentMainSize();
741
+ for (const info of childInfos) {
742
+ const itemSize = isRow ? info.width + info.margin.left + info.margin.right : info.height + info.margin.top + info.margin.bottom;
743
+ if (currentLine.length > 0 && currentLineSize + gap + itemSize > mainAxisSize) {
744
+ lines.push(currentLine);
745
+ currentLine = [info];
746
+ currentLineSize = itemSize;
747
+ } else {
748
+ currentLine.push(info);
749
+ currentLineSize += (currentLine.length > 1 ? gap : 0) + itemSize;
750
+ }
751
+ }
752
+ if (currentLine.length > 0) lines.push(currentLine);
753
+ } else lines.push(childInfos);
754
+ for (const lineInfos of lines) {
755
+ let totalFixed = 0;
756
+ let totalFlex = 0;
757
+ const totalGap = lineInfos.length > 1 ? gap * (lineInfos.length - 1) : 0;
758
+ for (const info of lineInfos) if (info.flex > 0) totalFlex += info.flex;
759
+ else if (isRow) totalFixed += info.width + info.margin.left + info.margin.right;
760
+ else totalFixed += info.height + info.margin.top + info.margin.bottom;
761
+ const mainAxisSize = getContentMainSize();
762
+ const availableForFlex = Math.max(0, mainAxisSize - totalFixed - totalGap);
763
+ for (const info of lineInfos) if (info.flex > 0) {
764
+ const flexSize = totalFlex > 0 ? availableForFlex * info.flex / totalFlex : 0;
765
+ if (isRow) {
766
+ info.width = flexSize;
767
+ const size = measureIntrinsicSize(info.element, ctx, flexSize);
768
+ info.height = sizeNeedsParent(info.element.height) ? resolveSize(info.element.height, contentHeight - info.margin.top - info.margin.bottom, size.height) : resolveSize(info.element.height, 0, size.height);
769
+ } else {
770
+ info.height = flexSize;
771
+ const size = measureIntrinsicSize(info.element, ctx, contentWidth - info.margin.left - info.margin.right);
772
+ info.width = sizeNeedsParent(info.element.width) ? resolveSize(info.element.width, contentWidth - info.margin.left - info.margin.right, size.width) : resolveSize(info.element.width, 0, size.width);
773
+ }
774
+ }
775
+ }
776
+ let crossOffset = 0;
777
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
778
+ const lineInfos = lines[lineIndex];
779
+ const totalGap = lineInfos.length > 1 ? gap * (lineInfos.length - 1) : 0;
780
+ const totalSize = lineInfos.reduce((sum, info) => {
781
+ return sum + (isRow ? info.width + info.margin.left + info.margin.right : info.height + info.margin.top + info.margin.bottom);
782
+ }, 0) + totalGap;
783
+ const freeSpace = getContentMainSize() - totalSize;
784
+ let mainStart = 0;
785
+ let mainGap = gap;
786
+ switch (justify) {
787
+ case "start":
788
+ mainStart = 0;
789
+ break;
790
+ case "end":
791
+ mainStart = freeSpace;
792
+ break;
793
+ case "center":
794
+ mainStart = freeSpace / 2;
795
+ break;
796
+ case "space-between":
797
+ mainStart = 0;
798
+ if (lineInfos.length > 1) mainGap = gap + freeSpace / (lineInfos.length - 1);
799
+ break;
800
+ case "space-around":
801
+ if (lineInfos.length > 0) {
802
+ const spacing = freeSpace / lineInfos.length;
803
+ mainStart = spacing / 2;
804
+ mainGap = gap + spacing;
805
+ }
806
+ break;
807
+ case "space-evenly":
808
+ if (lineInfos.length > 0) {
809
+ const spacing = freeSpace / (lineInfos.length + 1);
810
+ mainStart = spacing;
811
+ mainGap = gap + spacing;
812
+ }
813
+ break;
814
+ }
815
+ const lineCrossSize = lineInfos.reduce((max, info) => {
816
+ const itemCrossSize = isRow ? info.height + info.margin.top + info.margin.bottom : info.width + info.margin.left + info.margin.right;
817
+ return Math.max(max, itemCrossSize);
818
+ }, 0);
819
+ let mainOffset = mainStart;
820
+ const orderedInfos = isReverse ? [...lineInfos].reverse() : lineInfos;
821
+ for (let i = 0; i < orderedInfos.length; i++) {
822
+ const info = orderedInfos[i];
823
+ const crossAxisSize = wrap ? lineCrossSize : getContentCrossSize();
824
+ const childCrossSize = isRow ? info.height + info.margin.top + info.margin.bottom : info.width + info.margin.left + info.margin.right;
825
+ let itemCrossOffset = 0;
826
+ const effectiveAlign = info.element.alignSelf ?? align;
827
+ if (effectiveAlign === "start") itemCrossOffset = 0;
828
+ else if (effectiveAlign === "end") itemCrossOffset = crossAxisSize - childCrossSize;
829
+ else if (effectiveAlign === "center") itemCrossOffset = (crossAxisSize - childCrossSize) / 2;
830
+ else if (effectiveAlign === "stretch") {
831
+ itemCrossOffset = 0;
832
+ if (isRow && info.element.height === void 0) info.height = crossAxisSize - info.margin.top - info.margin.bottom;
833
+ else if (!isRow && info.element.width === void 0) info.width = crossAxisSize - info.margin.left - info.margin.right;
834
+ }
835
+ const childX = isRow ? contentX + mainOffset + info.margin.left : contentX + crossOffset + itemCrossOffset + info.margin.left;
836
+ const childY = isRow ? contentY + crossOffset + itemCrossOffset + info.margin.top : contentY + mainOffset + info.margin.top;
837
+ let minWidth = 0;
838
+ let maxWidth = info.width;
839
+ let minHeight = 0;
840
+ let maxHeight = info.height;
841
+ let shouldStretchCross = false;
842
+ if (info.flex > 0) if (isRow) {
843
+ minWidth = maxWidth = info.width;
844
+ if (info.element.height === void 0 && align === "stretch") {
845
+ minHeight = info.height;
846
+ maxHeight = element.height !== void 0 ? info.height : Infinity;
847
+ shouldStretchCross = true;
848
+ }
849
+ } else {
850
+ minHeight = maxHeight = info.height;
851
+ if (info.element.width === void 0 && align === "stretch") {
852
+ minWidth = info.width;
853
+ maxWidth = element.width !== void 0 ? info.width : Infinity;
854
+ shouldStretchCross = true;
855
+ }
856
+ }
857
+ else {
858
+ if (!isRow && info.element.width === void 0 && align === "stretch") minWidth = maxWidth = crossAxisSize - info.margin.left - info.margin.right;
859
+ if (isRow && info.element.height === void 0 && align === "stretch") minHeight = maxHeight = crossAxisSize - info.margin.top - info.margin.bottom;
860
+ }
861
+ const childNode = computeLayout(info.element, ctx, {
862
+ minWidth,
863
+ maxWidth,
864
+ minHeight,
865
+ maxHeight
866
+ }, childX - info.margin.left, childY - info.margin.top);
867
+ if (shouldStretchCross && info.flex > 0) {
868
+ const childPadding = normalizeSpacing("padding" in info.element ? info.element.padding : void 0);
869
+ if (isRow && childNode.layout.height < info.height) {
870
+ childNode.layout.height = info.height;
871
+ childNode.layout.contentHeight = info.height - childPadding.top - childPadding.bottom;
872
+ } else if (!isRow && childNode.layout.width < info.width) {
873
+ childNode.layout.width = info.width;
874
+ childNode.layout.contentWidth = info.width - childPadding.left - childPadding.right;
875
+ }
876
+ }
877
+ node.children.push(childNode);
878
+ mainOffset += isRow ? info.width + info.margin.left + info.margin.right : info.height + info.margin.top + info.margin.bottom;
879
+ if (i < orderedInfos.length - 1) mainOffset += mainGap;
880
+ }
881
+ crossOffset += lineCrossSize;
882
+ if (lineIndex < lines.length - 1) crossOffset += gap;
883
+ }
884
+ if (wrap && element.height === void 0 && isRow) {
885
+ const actualContentHeight = crossOffset;
886
+ const actualHeight = actualContentHeight + padding.top + padding.bottom;
887
+ node.layout.height = actualHeight;
888
+ node.layout.contentHeight = actualContentHeight;
889
+ } else if (wrap && element.width === void 0 && !isRow) {
890
+ const actualContentWidth = crossOffset;
891
+ const actualWidth = actualContentWidth + padding.left + padding.right;
892
+ node.layout.width = actualWidth;
893
+ node.layout.contentWidth = actualContentWidth;
894
+ }
895
+ if (!wrap) {
896
+ let maxChildCrossSize = 0;
897
+ for (const childNode of node.children) {
898
+ const childMargin = normalizeSpacing(childNode.element.margin);
899
+ if (isRow) {
900
+ const childOuterHeight = childNode.layout.height + childMargin.top + childMargin.bottom;
901
+ maxChildCrossSize = Math.max(maxChildCrossSize, childOuterHeight);
902
+ } else {
903
+ const childOuterWidth = childNode.layout.width + childMargin.left + childMargin.right;
904
+ maxChildCrossSize = Math.max(maxChildCrossSize, childOuterWidth);
905
+ }
906
+ }
907
+ if (isRow && element.height === void 0) {
908
+ const actualHeight = maxChildCrossSize + padding.top + padding.bottom;
909
+ if (actualHeight > node.layout.height) {
910
+ node.layout.height = actualHeight;
911
+ node.layout.contentHeight = maxChildCrossSize;
912
+ }
913
+ } else if (!isRow && element.width === void 0) {
914
+ const actualWidth = maxChildCrossSize + padding.left + padding.right;
915
+ if (actualWidth > node.layout.width) {
916
+ node.layout.width = actualWidth;
917
+ node.layout.contentWidth = maxChildCrossSize;
918
+ }
919
+ }
920
+ }
921
+ if (isReverse) node.children.reverse();
922
+ }
923
+ }
924
+ return node;
925
+ }
926
+
927
+ //#endregion
928
+ //#region src/render/utils/colors.ts
929
+ function isGradientDescriptor$1(color) {
930
+ return typeof color === "object" && color !== null && "type" in color && (color.type === "linear-gradient" || color.type === "radial-gradient");
931
+ }
932
+ function resolveGradient$1(ctx, descriptor, x, y, width, height) {
933
+ if (descriptor.type === "linear-gradient") {
934
+ const angleRad = (descriptor.angle - 90) * Math.PI / 180;
935
+ const centerX = x + width / 2;
936
+ const centerY = y + height / 2;
937
+ const diagLength = Math.sqrt(width * width + height * height) / 2;
938
+ const x0 = centerX - Math.cos(angleRad) * diagLength;
939
+ const y0 = centerY - Math.sin(angleRad) * diagLength;
940
+ const x1 = centerX + Math.cos(angleRad) * diagLength;
941
+ const y1 = centerY + Math.sin(angleRad) * diagLength;
942
+ const gradient = ctx.createLinearGradient(x0, y0, x1, y1);
943
+ for (const stop of descriptor.stops) gradient.addColorStop(stop.offset, stop.color);
944
+ return gradient;
945
+ } else {
946
+ const diagLength = Math.sqrt(width * width + height * height);
947
+ const startX = x + (descriptor.startX ?? .5) * width;
948
+ const startY = y + (descriptor.startY ?? .5) * height;
949
+ const startRadius = (descriptor.startRadius ?? 0) * diagLength;
950
+ const endX = x + (descriptor.endX ?? .5) * width;
951
+ const endY = y + (descriptor.endY ?? .5) * height;
952
+ const endRadius = (descriptor.endRadius ?? .5) * diagLength;
953
+ const gradient = ctx.createRadialGradient(startX, startY, startRadius, endX, endY, endRadius);
954
+ for (const stop of descriptor.stops) gradient.addColorStop(stop.offset, stop.color);
955
+ return gradient;
956
+ }
957
+ }
958
+ function resolveColor$1(ctx, color, x, y, width, height) {
959
+ if (isGradientDescriptor$1(color)) return resolveGradient$1(ctx, color, x, y, width, height);
960
+ return color;
961
+ }
962
+
963
+ //#endregion
964
+ //#region src/render/utils/shadows.ts
965
+ function applyShadow$1(ctx, shadow) {
966
+ if (shadow) {
967
+ ctx.shadowOffsetX = shadow.offsetX ?? 0;
968
+ ctx.shadowOffsetY = shadow.offsetY ?? 0;
969
+ ctx.shadowBlur = shadow.blur ?? 0;
970
+ ctx.shadowColor = shadow.color ?? "rgba(0,0,0,0.5)";
971
+ } else {
972
+ ctx.shadowOffsetX = 0;
973
+ ctx.shadowOffsetY = 0;
974
+ ctx.shadowBlur = 0;
975
+ ctx.shadowColor = "transparent";
976
+ }
977
+ }
978
+ function clearShadow$1(ctx) {
979
+ ctx.shadowOffsetX = 0;
980
+ ctx.shadowOffsetY = 0;
981
+ ctx.shadowBlur = 0;
982
+ ctx.shadowColor = "transparent";
983
+ }
984
+
985
+ //#endregion
986
+ //#region src/render/utils/shapes.ts
987
+ function roundRectPath(ctx, x, y, width, height, radius) {
988
+ const [tl, tr, br, bl] = radius;
989
+ ctx.beginPath();
990
+ ctx.moveTo(x + tl, y);
991
+ ctx.lineTo(x + width - tr, y);
992
+ ctx.quadraticCurveTo(x + width, y, x + width, y + tr);
993
+ ctx.lineTo(x + width, y + height - br);
994
+ ctx.quadraticCurveTo(x + width, y + height, x + width - br, y + height);
995
+ ctx.lineTo(x + bl, y + height);
996
+ ctx.quadraticCurveTo(x, y + height, x, y + height - bl);
997
+ ctx.lineTo(x, y + tl);
998
+ ctx.quadraticCurveTo(x, y, x + tl, y);
999
+ ctx.closePath();
1000
+ }
1001
+
1002
+ //#endregion
1003
+ //#region src/render/components/box.ts
1004
+ function renderBox(ctx, node) {
1005
+ const element = node.element;
1006
+ const { x, y, width, height } = node.layout;
1007
+ const border = element.border;
1008
+ const radius = normalizeBorderRadius(border?.radius);
1009
+ const hasRadius = radius.some((r) => r > 0);
1010
+ if (element.opacity !== void 0 && element.opacity < 1) ctx.globalAlpha = element.opacity;
1011
+ if (element.shadow && element.background) applyShadow$1(ctx, element.shadow);
1012
+ if (element.background) {
1013
+ ctx.fillStyle = resolveColor$1(ctx, element.background, x, y, width, height);
1014
+ if (hasRadius) {
1015
+ roundRectPath(ctx, x, y, width, height, radius);
1016
+ ctx.fill();
1017
+ } else ctx.fillRect(x, y, width, height);
1018
+ clearShadow$1(ctx);
1019
+ }
1020
+ if (border && border.width && border.width > 0) {
1021
+ ctx.strokeStyle = border.color ? resolveColor$1(ctx, border.color, x, y, width, height) : "#000";
1022
+ ctx.lineWidth = border.width;
1023
+ if (hasRadius) {
1024
+ roundRectPath(ctx, x, y, width, height, radius);
1025
+ ctx.stroke();
1026
+ } else ctx.strokeRect(x, y, width, height);
1027
+ }
1028
+ if (element.opacity !== void 0 && element.opacity < 1) ctx.globalAlpha = 1;
1029
+ }
1030
+
1031
+ //#endregion
1032
+ //#region src/render/components/image.ts
1033
+ function renderImage(ctx, node) {
1034
+ const element = node.element;
1035
+ const { x, y, width, height } = node.layout;
1036
+ const src = element.src;
1037
+ if (!src) return;
1038
+ if (element.opacity !== void 0 && element.opacity < 1) ctx.globalAlpha = element.opacity;
1039
+ if (element.shadow) applyShadow$1(ctx, element.shadow);
1040
+ const border = element.border;
1041
+ const radius = normalizeBorderRadius(border?.radius);
1042
+ const hasRadius = radius.some((r) => r > 0);
1043
+ if (hasRadius) {
1044
+ ctx.save();
1045
+ roundRectPath(ctx, x, y, width, height, radius);
1046
+ ctx.clip();
1047
+ }
1048
+ const imgWidth = "naturalWidth" in src ? src.naturalWidth : "width" in src ? +src.width : 0;
1049
+ const imgHeight = "naturalHeight" in src ? src.naturalHeight : "height" in src ? +src.height : 0;
1050
+ const fit = element.fit ?? "fill";
1051
+ let drawX = x;
1052
+ let drawY = y;
1053
+ let drawWidth = width;
1054
+ let drawHeight = height;
1055
+ if (fit !== "fill" && imgWidth > 0 && imgHeight > 0) {
1056
+ const imgAspect = imgWidth / imgHeight;
1057
+ const boxAspect = width / height;
1058
+ let scale = 1;
1059
+ switch (fit) {
1060
+ case "contain":
1061
+ scale = imgAspect > boxAspect ? width / imgWidth : height / imgHeight;
1062
+ break;
1063
+ case "cover":
1064
+ scale = imgAspect > boxAspect ? height / imgHeight : width / imgWidth;
1065
+ break;
1066
+ case "scale-down":
1067
+ scale = Math.min(1, imgAspect > boxAspect ? width / imgWidth : height / imgHeight);
1068
+ break;
1069
+ case "none":
1070
+ scale = 1;
1071
+ break;
1072
+ }
1073
+ drawWidth = imgWidth * scale;
1074
+ drawHeight = imgHeight * scale;
1075
+ const position = element.position ?? {};
1076
+ const posX = position.x ?? "center";
1077
+ const posY = position.y ?? "center";
1078
+ if (typeof posX === "number") drawX = x + posX;
1079
+ else switch (posX) {
1080
+ case "left":
1081
+ drawX = x;
1082
+ break;
1083
+ case "center":
1084
+ drawX = x + (width - drawWidth) / 2;
1085
+ break;
1086
+ case "right":
1087
+ drawX = x + width - drawWidth;
1088
+ break;
1089
+ }
1090
+ if (typeof posY === "number") drawY = y + posY;
1091
+ else switch (posY) {
1092
+ case "top":
1093
+ drawY = y;
1094
+ break;
1095
+ case "center":
1096
+ drawY = y + (height - drawHeight) / 2;
1097
+ break;
1098
+ case "bottom":
1099
+ drawY = y + height - drawHeight;
1100
+ break;
1101
+ }
1102
+ }
1103
+ ctx.drawImage(src, drawX, drawY, drawWidth, drawHeight);
1104
+ if (element.shadow) clearShadow$1(ctx);
1105
+ if (hasRadius) ctx.restore();
1106
+ if (border && border.width && border.width > 0) {
1107
+ ctx.strokeStyle = border.color ? resolveColor$1(ctx, border.color, x, y, width, height) : "#000";
1108
+ ctx.lineWidth = border.width;
1109
+ if (hasRadius) {
1110
+ roundRectPath(ctx, x, y, width, height, radius);
1111
+ ctx.stroke();
1112
+ } else ctx.strokeRect(x, y, width, height);
1113
+ }
1114
+ if (element.opacity !== void 0 && element.opacity < 1) ctx.globalAlpha = 1;
1115
+ }
1116
+
1117
+ //#endregion
1118
+ //#region src/render/components/richtext.ts
1119
+ function renderRichText(ctx, node) {
1120
+ const element = node.element;
1121
+ const { contentX, contentY, contentWidth, contentHeight } = node.layout;
1122
+ const lines = node.richLines ?? [];
1123
+ if (lines.length === 0) return;
1124
+ const totalTextHeight = lines.reduce((sum, line) => sum + line.height, 0);
1125
+ let verticalOffset = 0;
1126
+ if (element.verticalAlign === "middle") verticalOffset = (contentHeight - totalTextHeight) / 2;
1127
+ else if (element.verticalAlign === "bottom") verticalOffset = contentHeight - totalTextHeight;
1128
+ let currentY = contentY + verticalOffset;
1129
+ for (const line of lines) {
1130
+ let lineX = contentX;
1131
+ if (element.align === "center") lineX = contentX + (contentWidth - line.width) / 2;
1132
+ else if (element.align === "right") lineX = contentX + (contentWidth - line.width);
1133
+ const baselineY = currentY + line.baseline;
1134
+ for (const seg of line.segments) {
1135
+ ctx.save();
1136
+ ctx.font = buildFontString(seg.font ?? {});
1137
+ if (seg.background) {
1138
+ ctx.fillStyle = resolveColor$1(ctx, seg.background, lineX, currentY, seg.width, line.height);
1139
+ ctx.fillRect(lineX, currentY, seg.width, line.height);
1140
+ }
1141
+ ctx.fillStyle = seg.color ? resolveColor$1(ctx, seg.color, lineX, currentY, seg.width, line.height) : "#000";
1142
+ ctx.textBaseline = "middle";
1143
+ ctx.fillText(seg.text, lineX, baselineY - seg.offset);
1144
+ if (seg.underline) {
1145
+ ctx.beginPath();
1146
+ ctx.strokeStyle = ctx.fillStyle;
1147
+ ctx.lineWidth = 1;
1148
+ ctx.moveTo(lineX, currentY + seg.height);
1149
+ ctx.lineTo(lineX + seg.width, currentY + seg.height);
1150
+ ctx.stroke();
1151
+ }
1152
+ if (seg.strikethrough) {
1153
+ ctx.beginPath();
1154
+ ctx.strokeStyle = ctx.fillStyle;
1155
+ ctx.lineWidth = 1;
1156
+ const strikeY = currentY + seg.height / 2 + seg.offset;
1157
+ ctx.moveTo(lineX, strikeY);
1158
+ ctx.lineTo(lineX + seg.width, strikeY);
1159
+ ctx.stroke();
1160
+ }
1161
+ ctx.restore();
1162
+ lineX += seg.width;
1163
+ }
1164
+ currentY += line.height;
1165
+ }
1166
+ }
1167
+
1168
+ //#endregion
1169
+ //#region src/compat/DOMMatrix.ts
1170
+ const DOMMatrixCompat = (() => {
1171
+ if (typeof DOMMatrix !== "undefined") return DOMMatrix;
1172
+ try {
1173
+ return require("@napi-rs/canvas").DOMMatrix;
1174
+ } catch {
1175
+ throw new Error("DOMMatrix is not available. In Node.js, install @napi-rs/canvas.");
1176
+ }
1177
+ })();
1178
+
1179
+ //#endregion
1180
+ //#region src/compat/Path2D.ts
1181
+ const Path2DCompat = (() => {
1182
+ if (typeof Path2D !== "undefined") return Path2D;
1183
+ try {
1184
+ return require("@napi-rs/canvas").Path2D;
1185
+ } catch {
1186
+ throw new Error("Path2D is not available. In Node.js, install @napi-rs/canvas.");
1187
+ }
1188
+ })();
1189
+
1190
+ //#endregion
1191
+ //#region src/render/components/svg.ts
1192
+ function isGradientDescriptor(color) {
1193
+ return typeof color === "object" && color !== null && "type" in color && (color.type === "linear-gradient" || color.type === "radial-gradient");
1194
+ }
1195
+ function resolveGradient(ctx, descriptor, x, y, width, height) {
1196
+ if (descriptor.type === "linear-gradient") {
1197
+ const angleRad = (descriptor.angle - 90) * Math.PI / 180;
1198
+ const centerX = x + width / 2;
1199
+ const centerY = y + height / 2;
1200
+ const diagLength = Math.sqrt(width * width + height * height) / 2;
1201
+ const x0 = centerX - Math.cos(angleRad) * diagLength;
1202
+ const y0 = centerY - Math.sin(angleRad) * diagLength;
1203
+ const x1 = centerX + Math.cos(angleRad) * diagLength;
1204
+ const y1 = centerY + Math.sin(angleRad) * diagLength;
1205
+ const gradient = ctx.createLinearGradient(x0, y0, x1, y1);
1206
+ for (const stop of descriptor.stops) gradient.addColorStop(stop.offset, stop.color);
1207
+ return gradient;
1208
+ } else {
1209
+ const diagLength = Math.sqrt(width * width + height * height);
1210
+ const startX = x + (descriptor.startX ?? .5) * width;
1211
+ const startY = y + (descriptor.startY ?? .5) * height;
1212
+ const startRadius = (descriptor.startRadius ?? 0) * diagLength;
1213
+ const endX = x + (descriptor.endX ?? .5) * width;
1214
+ const endY = y + (descriptor.endY ?? .5) * height;
1215
+ const endRadius = (descriptor.endRadius ?? .5) * diagLength;
1216
+ const gradient = ctx.createRadialGradient(startX, startY, startRadius, endX, endY, endRadius);
1217
+ for (const stop of descriptor.stops) gradient.addColorStop(stop.offset, stop.color);
1218
+ return gradient;
1219
+ }
1220
+ }
1221
+ function resolveColor(ctx, color, x, y, width, height) {
1222
+ if (isGradientDescriptor(color)) return resolveGradient(ctx, color, x, y, width, height);
1223
+ return color;
1224
+ }
1225
+ function applyTransform(base, transform) {
1226
+ if (!transform) return base;
1227
+ let result = new DOMMatrixCompat([
1228
+ base.a,
1229
+ base.b,
1230
+ base.c,
1231
+ base.d,
1232
+ base.e,
1233
+ base.f
1234
+ ]);
1235
+ if (transform.matrix) {
1236
+ const [a, b, c, d, e, f] = transform.matrix;
1237
+ result = result.multiply(new DOMMatrixCompat([
1238
+ a,
1239
+ b,
1240
+ c,
1241
+ d,
1242
+ e,
1243
+ f
1244
+ ]));
1245
+ }
1246
+ if (transform.translate) result = result.translate(transform.translate[0], transform.translate[1]);
1247
+ if (transform.rotate !== void 0) if (typeof transform.rotate === "number") result = result.rotate(transform.rotate);
1248
+ else {
1249
+ const [angle, cx, cy] = transform.rotate;
1250
+ result = result.translate(cx, cy).rotate(angle).translate(-cx, -cy);
1251
+ }
1252
+ if (transform.scale !== void 0) if (typeof transform.scale === "number") result = result.scale(transform.scale);
1253
+ else result = result.scale(transform.scale[0], transform.scale[1]);
1254
+ if (transform.skewX !== void 0) result = result.skewX(transform.skewX);
1255
+ if (transform.skewY !== void 0) result = result.skewY(transform.skewY);
1256
+ return result;
1257
+ }
1258
+ function applyStroke(ctx, stroke, bounds) {
1259
+ ctx.strokeStyle = resolveColor(ctx, stroke.color, bounds.x, bounds.y, bounds.width, bounds.height);
1260
+ ctx.lineWidth = stroke.width;
1261
+ ctx.setLineDash(stroke.dash ?? []);
1262
+ if (stroke.cap) ctx.lineCap = stroke.cap;
1263
+ if (stroke.join) ctx.lineJoin = stroke.join;
1264
+ }
1265
+ function applyFill(ctx, fill, bounds) {
1266
+ ctx.fillStyle = resolveColor(ctx, fill, bounds.x, bounds.y, bounds.width, bounds.height);
1267
+ ctx.fill();
1268
+ }
1269
+ function applyFillAndStroke(ctx, shape, bounds) {
1270
+ if (shape.fill && shape.fill !== "none") applyFill(ctx, shape.fill, bounds);
1271
+ if (shape.stroke) {
1272
+ applyStroke(ctx, shape.stroke, bounds);
1273
+ ctx.stroke();
1274
+ }
1275
+ }
1276
+ function renderSvgRect(ctx, rect, bounds) {
1277
+ const { x = 0, y = 0, width, height, rx = 0, ry = 0 } = rect;
1278
+ ctx.beginPath();
1279
+ if (rx || ry) ctx.roundRect(x, y, width, height, Math.max(rx, ry));
1280
+ else ctx.rect(x, y, width, height);
1281
+ applyFillAndStroke(ctx, rect, bounds);
1282
+ }
1283
+ function renderSvgCircle(ctx, circle, bounds) {
1284
+ ctx.beginPath();
1285
+ ctx.arc(circle.cx, circle.cy, circle.r, 0, Math.PI * 2);
1286
+ applyFillAndStroke(ctx, circle, bounds);
1287
+ }
1288
+ function renderSvgEllipse(ctx, ellipse, bounds) {
1289
+ ctx.beginPath();
1290
+ ctx.ellipse(ellipse.cx, ellipse.cy, ellipse.rx, ellipse.ry, 0, 0, Math.PI * 2);
1291
+ applyFillAndStroke(ctx, ellipse, bounds);
1292
+ }
1293
+ function renderSvgLine(ctx, line, bounds) {
1294
+ ctx.beginPath();
1295
+ ctx.moveTo(line.x1, line.y1);
1296
+ ctx.lineTo(line.x2, line.y2);
1297
+ if (line.stroke) {
1298
+ applyStroke(ctx, line.stroke, bounds);
1299
+ ctx.stroke();
1300
+ }
1301
+ }
1302
+ function renderSvgPolyline(ctx, polyline, bounds) {
1303
+ const { points } = polyline;
1304
+ if (points.length === 0) return;
1305
+ ctx.beginPath();
1306
+ ctx.moveTo(points[0][0], points[0][1]);
1307
+ for (let i = 1; i < points.length; i++) ctx.lineTo(points[i][0], points[i][1]);
1308
+ applyFillAndStroke(ctx, polyline, bounds);
1309
+ }
1310
+ function renderSvgPolygon(ctx, polygon, bounds) {
1311
+ const { points } = polygon;
1312
+ if (points.length === 0) return;
1313
+ ctx.beginPath();
1314
+ ctx.moveTo(points[0][0], points[0][1]);
1315
+ for (let i = 1; i < points.length; i++) ctx.lineTo(points[i][0], points[i][1]);
1316
+ ctx.closePath();
1317
+ applyFillAndStroke(ctx, polygon, bounds);
1318
+ }
1319
+ function renderSvgPath(ctx, path, bounds) {
1320
+ const path2d = new Path2DCompat(path.d);
1321
+ if (path.fill && path.fill !== "none") {
1322
+ ctx.fillStyle = resolveColor(ctx, path.fill, bounds.x, bounds.y, bounds.width, bounds.height);
1323
+ ctx.fill(path2d);
1324
+ }
1325
+ if (path.stroke) {
1326
+ applyStroke(ctx, path.stroke, bounds);
1327
+ ctx.stroke(path2d);
1328
+ }
1329
+ }
1330
+ const TEXT_ANCHOR_MAP = {
1331
+ start: "left",
1332
+ middle: "center",
1333
+ end: "right"
1334
+ };
1335
+ const BASELINE_MAP = {
1336
+ auto: "alphabetic",
1337
+ middle: "middle",
1338
+ hanging: "hanging"
1339
+ };
1340
+ function renderSvgText(ctx, text, bounds) {
1341
+ const { x = 0, y = 0, content, font, textAnchor = "start", dominantBaseline = "auto" } = text;
1342
+ ctx.font = buildFontString(font ?? {});
1343
+ ctx.textAlign = TEXT_ANCHOR_MAP[textAnchor] ?? "left";
1344
+ ctx.textBaseline = BASELINE_MAP[dominantBaseline] ?? "alphabetic";
1345
+ if (text.fill && text.fill !== "none") {
1346
+ ctx.fillStyle = resolveColor(ctx, text.fill, bounds.x, bounds.y, bounds.width, bounds.height);
1347
+ ctx.fillText(content, x, y);
1348
+ }
1349
+ if (text.stroke) {
1350
+ applyStroke(ctx, text.stroke, bounds);
1351
+ ctx.strokeText(content, x, y);
1352
+ }
1353
+ }
1354
+ function renderSvgChild(ctx, child, parentTransform, bounds, baseTransform) {
1355
+ const localTransform = applyTransform(parentTransform, child.transform);
1356
+ ctx.save();
1357
+ ctx.setTransform(baseTransform.multiply(localTransform));
1358
+ if (child.opacity !== void 0) ctx.globalAlpha *= child.opacity;
1359
+ switch (child.type) {
1360
+ case "rect":
1361
+ renderSvgRect(ctx, child, bounds);
1362
+ break;
1363
+ case "circle":
1364
+ renderSvgCircle(ctx, child, bounds);
1365
+ break;
1366
+ case "ellipse":
1367
+ renderSvgEllipse(ctx, child, bounds);
1368
+ break;
1369
+ case "line":
1370
+ renderSvgLine(ctx, child, bounds);
1371
+ break;
1372
+ case "polyline":
1373
+ renderSvgPolyline(ctx, child, bounds);
1374
+ break;
1375
+ case "polygon":
1376
+ renderSvgPolygon(ctx, child, bounds);
1377
+ break;
1378
+ case "path":
1379
+ renderSvgPath(ctx, child, bounds);
1380
+ break;
1381
+ case "text":
1382
+ renderSvgText(ctx, child, bounds);
1383
+ break;
1384
+ case "g":
1385
+ renderSvgGroup(ctx, child, localTransform, bounds, baseTransform);
1386
+ break;
1387
+ }
1388
+ ctx.restore();
1389
+ }
1390
+ function renderSvgGroup(ctx, group, parentTransform, bounds, baseTransform) {
1391
+ for (const child of group.children) renderSvgChild(ctx, child, parentTransform, bounds, baseTransform);
1392
+ }
1393
+ function calculateViewBoxTransform(x, y, width, height, viewBox, preserveAspectRatio) {
1394
+ const vbX = viewBox.x ?? 0;
1395
+ const vbY = viewBox.y ?? 0;
1396
+ const vbWidth = viewBox.width;
1397
+ const vbHeight = viewBox.height;
1398
+ const scaleX = width / vbWidth;
1399
+ const scaleY = height / vbHeight;
1400
+ const align = preserveAspectRatio?.align ?? "xMidYMid";
1401
+ const meetOrSlice = preserveAspectRatio?.meetOrSlice ?? "meet";
1402
+ if (align === "none") return new DOMMatrixCompat().translate(x, y).scale(scaleX, scaleY).translate(-vbX, -vbY);
1403
+ const scale = meetOrSlice === "meet" ? Math.min(scaleX, scaleY) : Math.max(scaleX, scaleY);
1404
+ const scaledWidth = vbWidth * scale;
1405
+ const scaledHeight = vbHeight * scale;
1406
+ let translateX = x;
1407
+ let translateY = y;
1408
+ if (align.includes("xMid")) translateX += (width - scaledWidth) / 2;
1409
+ else if (align.includes("xMax")) translateX += width - scaledWidth;
1410
+ if (align.includes("YMid")) translateY += (height - scaledHeight) / 2;
1411
+ else if (align.includes("YMax")) translateY += height - scaledHeight;
1412
+ return new DOMMatrixCompat().translate(translateX, translateY).scale(scale, scale).translate(-vbX, -vbY);
1413
+ }
1414
+ function applyShadow(ctx, shadow) {
1415
+ ctx.shadowOffsetX = shadow.offsetX ?? 0;
1416
+ ctx.shadowOffsetY = shadow.offsetY ?? 0;
1417
+ ctx.shadowBlur = shadow.blur ?? 0;
1418
+ ctx.shadowColor = shadow.color ?? "rgba(0,0,0,0.5)";
1419
+ }
1420
+ function clearShadow(ctx) {
1421
+ ctx.shadowOffsetX = 0;
1422
+ ctx.shadowOffsetY = 0;
1423
+ ctx.shadowBlur = 0;
1424
+ ctx.shadowColor = "transparent";
1425
+ }
1426
+ function renderSvg(ctx, node) {
1427
+ const element = node.element;
1428
+ const { x, y, width, height } = node.layout;
1429
+ const bounds = {
1430
+ x,
1431
+ y,
1432
+ width,
1433
+ height
1434
+ };
1435
+ if (element.background) {
1436
+ if (element.shadow) applyShadow(ctx, element.shadow);
1437
+ ctx.fillStyle = resolveColor(ctx, element.background, x, y, width, height);
1438
+ ctx.fillRect(x, y, width, height);
1439
+ if (element.shadow) clearShadow(ctx);
1440
+ }
1441
+ ctx.save();
1442
+ ctx.beginPath();
1443
+ ctx.rect(x, y, width, height);
1444
+ ctx.clip();
1445
+ const baseTransform = ctx.getTransform();
1446
+ const transform = calculateViewBoxTransform(x, y, width, height, element.viewBox ?? {
1447
+ x: 0,
1448
+ y: 0,
1449
+ width,
1450
+ height
1451
+ }, element.preserveAspectRatio);
1452
+ for (const child of element.children) renderSvgChild(ctx, child, transform, bounds, baseTransform);
1453
+ ctx.restore();
1454
+ }
1455
+
1456
+ //#endregion
1457
+ //#region src/render/components/text.ts
1458
+ function renderText(ctx, node) {
1459
+ const element = node.element;
1460
+ const { contentX, contentY, contentWidth, contentHeight } = node.layout;
1461
+ const lines = node.lines ?? [element.content];
1462
+ const font = element.font ?? {};
1463
+ const lineHeightPx = (font.size ?? 16) * (element.lineHeight ?? 1.2);
1464
+ ctx.font = buildFontString(font);
1465
+ ctx.fillStyle = element.color ? resolveColor$1(ctx, element.color, contentX, contentY, contentWidth, contentHeight) : "#000";
1466
+ let textAlign = "left";
1467
+ if (element.align === "center") textAlign = "center";
1468
+ else if (element.align === "right") textAlign = "right";
1469
+ ctx.textAlign = textAlign;
1470
+ ctx.textBaseline = "middle";
1471
+ const totalTextHeight = lines.length * lineHeightPx;
1472
+ let verticalOffset = 0;
1473
+ if (element.verticalAlign === "middle") verticalOffset = (contentHeight - totalTextHeight) / 2;
1474
+ else if (element.verticalAlign === "bottom") verticalOffset = contentHeight - totalTextHeight;
1475
+ let textX = contentX;
1476
+ if (element.align === "center") textX = contentX + contentWidth / 2;
1477
+ else if (element.align === "right") textX = contentX + contentWidth;
1478
+ if (element.shadow) applyShadow$1(ctx, element.shadow);
1479
+ for (let i = 0; i < lines.length; i++) {
1480
+ const correctedLineY = contentY + verticalOffset + i * lineHeightPx + lineHeightPx / 2 + (node.lineOffsets?.[i] ?? 0);
1481
+ if (element.stroke) {
1482
+ ctx.strokeStyle = resolveColor$1(ctx, element.stroke.color, contentX, contentY, contentWidth, contentHeight);
1483
+ ctx.lineWidth = element.stroke.width;
1484
+ ctx.strokeText(lines[i], textX, correctedLineY);
1485
+ }
1486
+ ctx.fillText(lines[i], textX, correctedLineY);
1487
+ }
1488
+ if (element.shadow) clearShadow$1(ctx);
1489
+ }
1490
+
1491
+ //#endregion
1492
+ //#region src/render/index.ts
1493
+ function renderNode(ctx, node) {
1494
+ const element = node.element;
1495
+ switch (element.type) {
1496
+ case "box":
1497
+ case "stack": {
1498
+ renderBox(ctx, node);
1499
+ const shouldClip = element.clip === true;
1500
+ if (shouldClip) {
1501
+ ctx.save();
1502
+ const { x, y, width, height } = node.layout;
1503
+ roundRectPath(ctx, x, y, width, height, normalizeBorderRadius(element.border?.radius));
1504
+ ctx.clip();
1505
+ }
1506
+ for (const child of node.children) renderNode(ctx, child);
1507
+ if (shouldClip) ctx.restore();
1508
+ break;
1509
+ }
1510
+ case "text":
1511
+ renderText(ctx, node);
1512
+ break;
1513
+ case "richtext":
1514
+ renderRichText(ctx, node);
1515
+ break;
1516
+ case "image":
1517
+ renderImage(ctx, node);
1518
+ break;
1519
+ case "svg":
1520
+ renderSvg(ctx, node);
1521
+ break;
1522
+ }
1523
+ }
1524
+
1525
+ //#endregion
1526
+ Object.defineProperty(exports, 'computeLayout', {
1527
+ enumerable: true,
1528
+ get: function () {
1529
+ return computeLayout;
1530
+ }
1531
+ });
1532
+ Object.defineProperty(exports, 'createCanvasMeasureContext', {
1533
+ enumerable: true,
1534
+ get: function () {
1535
+ return createCanvasMeasureContext;
1536
+ }
1537
+ });
1538
+ Object.defineProperty(exports, 'linearGradient', {
1539
+ enumerable: true,
1540
+ get: function () {
1541
+ return linearGradient;
1542
+ }
1543
+ });
1544
+ Object.defineProperty(exports, 'radialGradient', {
1545
+ enumerable: true,
1546
+ get: function () {
1547
+ return radialGradient;
1548
+ }
1549
+ });
1550
+ Object.defineProperty(exports, 'renderNode', {
1551
+ enumerable: true,
1552
+ get: function () {
1553
+ return renderNode;
1554
+ }
1555
+ });