@codehz/draw-call 0.1.0 → 0.1.2

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/index.cjs CHANGED
@@ -1,603 +1,38 @@
1
+ const require_render = require('./render.cjs');
1
2
 
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/types/layout.ts
74
- function resolveSize(size, available, auto) {
75
- if (size === void 0 || size === "auto") return auto;
76
- if (size === "fill") return available;
77
- if (typeof size === "number") return size;
78
- return available * parseFloat(size) / 100;
79
- }
80
- function sizeNeedsParent(size) {
81
- if (size === void 0 || size === "auto") return false;
82
- if (size === "fill") return true;
83
- if (typeof size === "string" && size.endsWith("%")) return true;
84
- return false;
85
- }
86
-
87
- //#endregion
88
- //#region src/layout/measure.ts
89
- function buildFontString(font) {
90
- return `${font.style ?? "normal"} ${font.weight ?? "normal"} ${font.size ?? 16}px ${font.family ?? "sans-serif"}`;
91
- }
92
- function createCanvasMeasureContext(ctx) {
93
- return { measureText(text, font) {
94
- ctx.font = buildFontString(font);
95
- const metrics = ctx.measureText(text);
96
- const height = metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent;
97
- return {
98
- width: metrics.width,
99
- height: height || font.size || 16
100
- };
101
- } };
102
- }
103
- function wrapText(ctx, text, maxWidth, font) {
104
- if (maxWidth <= 0) return [text];
105
- const lines = [];
106
- const paragraphs = text.split("\n");
107
- for (const paragraph of paragraphs) {
108
- if (paragraph === "") {
109
- lines.push("");
110
- continue;
111
- }
112
- const words = paragraph.split(/(\s+)/);
113
- let currentLine = "";
114
- for (const word of words) {
115
- const testLine = currentLine + word;
116
- const { width } = ctx.measureText(testLine, font);
117
- if (width > maxWidth && currentLine !== "") {
118
- lines.push(currentLine.trim());
119
- currentLine = word.trimStart();
120
- } else currentLine = testLine;
121
- }
122
- if (currentLine) lines.push(currentLine.trim());
123
- }
124
- return lines.length > 0 ? lines : [""];
125
- }
126
- function truncateText(ctx, text, maxWidth, font, ellipsis = "...") {
127
- const { width } = ctx.measureText(text, font);
128
- if (width <= maxWidth) return text;
129
- const availableWidth = maxWidth - ctx.measureText(ellipsis, font).width;
130
- if (availableWidth <= 0) return ellipsis;
131
- let left = 0;
132
- let right = text.length;
133
- while (left < right) {
134
- const mid = Math.floor((left + right + 1) / 2);
135
- const truncated = text.slice(0, mid);
136
- const { width: truncatedWidth } = ctx.measureText(truncated, font);
137
- if (truncatedWidth <= availableWidth) left = mid;
138
- else right = mid - 1;
139
- }
140
- return text.slice(0, left) + ellipsis;
141
- }
142
-
143
- //#endregion
144
- //#region src/layout/engine.ts
145
- function measureIntrinsicSize(element, ctx, availableWidth) {
146
- switch (element.type) {
147
- case "text": {
148
- const font = element.font ?? {};
149
- const lineHeightPx = (font.size ?? 16) * (element.lineHeight ?? 1.2);
150
- if (element.wrap && availableWidth > 0 && availableWidth < Infinity) {
151
- const lines = wrapText(ctx, element.content, availableWidth, font);
152
- const { width: maxLineWidth } = lines.reduce((max, line) => {
153
- const { width } = ctx.measureText(line, font);
154
- return width > max.width ? { width } : max;
155
- }, { width: 0 });
156
- return {
157
- width: maxLineWidth,
158
- height: lines.length * lineHeightPx
159
- };
160
- }
161
- const { width, height } = ctx.measureText(element.content, font);
162
- return {
163
- width,
164
- height: Math.max(height, lineHeightPx)
165
- };
166
- }
167
- case "box":
168
- case "stack": {
169
- const padding = normalizeSpacing(element.padding);
170
- const gap = element.type === "box" ? element.gap ?? 0 : 0;
171
- const direction = element.direction ?? "row";
172
- const isRow = direction === "row" || direction === "row-reverse";
173
- let contentWidth = 0;
174
- let contentHeight = 0;
175
- const children = element.children ?? [];
176
- if (element.type === "stack") for (const child of children) {
177
- const childMargin = normalizeSpacing(child.margin);
178
- const childSize = measureIntrinsicSize(child, ctx, availableWidth - padding.left - padding.right - childMargin.left - childMargin.right);
179
- contentWidth = Math.max(contentWidth, childSize.width + childMargin.left + childMargin.right);
180
- contentHeight = Math.max(contentHeight, childSize.height + childMargin.top + childMargin.bottom);
181
- }
182
- else for (let i = 0; i < children.length; i++) {
183
- const child = children[i];
184
- const childMargin = normalizeSpacing(child.margin);
185
- const childSize = measureIntrinsicSize(child, ctx, availableWidth - padding.left - padding.right - childMargin.left - childMargin.right);
186
- if (isRow) {
187
- contentWidth += childSize.width + childMargin.left + childMargin.right;
188
- contentHeight = Math.max(contentHeight, childSize.height + childMargin.top + childMargin.bottom);
189
- if (i > 0) contentWidth += gap;
190
- } else {
191
- contentHeight += childSize.height + childMargin.top + childMargin.bottom;
192
- contentWidth = Math.max(contentWidth, childSize.width + childMargin.left + childMargin.right);
193
- if (i > 0) contentHeight += gap;
194
- }
195
- }
196
- const intrinsicWidth = contentWidth + padding.left + padding.right;
197
- const intrinsicHeight = contentHeight + padding.top + padding.bottom;
198
- return {
199
- width: typeof element.width === "number" ? element.width : intrinsicWidth,
200
- height: typeof element.height === "number" ? element.height : intrinsicHeight
201
- };
202
- }
203
- case "image": return {
204
- width: 0,
205
- height: 0
206
- };
207
- case "shape": return {
208
- width: 0,
209
- height: 0
210
- };
211
- default: return {
212
- width: 0,
213
- height: 0
214
- };
215
- }
216
- }
217
- function computeLayout(element, ctx, constraints, x = 0, y = 0) {
218
- const margin = normalizeSpacing(element.margin);
219
- const padding = normalizeSpacing("padding" in element ? element.padding : void 0);
220
- const availableWidth = constraints.maxWidth - margin.left - margin.right;
221
- const availableHeight = constraints.maxHeight - margin.top - margin.bottom;
222
- const intrinsic = measureIntrinsicSize(element, ctx, availableWidth);
223
- let width = constraints.minWidth === constraints.maxWidth && constraints.minWidth > 0 ? constraints.maxWidth - margin.left - margin.right : resolveSize(element.width, availableWidth, intrinsic.width);
224
- let height = constraints.minHeight === constraints.maxHeight && constraints.minHeight > 0 ? constraints.maxHeight - margin.top - margin.bottom : resolveSize(element.height, availableHeight, intrinsic.height);
225
- if (element.minWidth !== void 0) width = Math.max(width, element.minWidth);
226
- if (element.maxWidth !== void 0) width = Math.min(width, element.maxWidth);
227
- if (element.minHeight !== void 0) height = Math.max(height, element.minHeight);
228
- if (element.maxHeight !== void 0) height = Math.min(height, element.maxHeight);
229
- const actualX = x + margin.left;
230
- const actualY = y + margin.top;
231
- const contentX = actualX + padding.left;
232
- const contentY = actualY + padding.top;
233
- const contentWidth = width - padding.left - padding.right;
234
- const contentHeight = height - padding.top - padding.bottom;
235
- const node = {
236
- element,
237
- layout: {
238
- x: actualX,
239
- y: actualY,
240
- width,
241
- height,
242
- contentX,
243
- contentY,
244
- contentWidth,
245
- contentHeight
246
- },
247
- children: []
248
- };
249
- if (element.type === "text") {
250
- const font = element.font ?? {};
251
- if (element.wrap && contentWidth > 0) {
252
- let lines = wrapText(ctx, element.content, contentWidth, font);
253
- if (element.maxLines && lines.length > element.maxLines) {
254
- lines = lines.slice(0, element.maxLines);
255
- if (element.ellipsis && lines.length > 0) lines[lines.length - 1] = truncateText(ctx, lines[lines.length - 1], contentWidth, font);
256
- }
257
- node.lines = lines;
258
- } else {
259
- let text = element.content;
260
- if (element.ellipsis && contentWidth > 0) text = truncateText(ctx, text, contentWidth, font);
261
- node.lines = [text];
262
- }
263
- }
264
- if (element.type === "box" || element.type === "stack") {
265
- const children = element.children ?? [];
266
- if (element.type === "stack") for (const child of children) {
267
- const childNode = computeLayout(child, ctx, {
268
- minWidth: 0,
269
- maxWidth: contentWidth,
270
- minHeight: 0,
271
- maxHeight: contentHeight
272
- }, contentX, contentY);
273
- node.children.push(childNode);
274
- }
275
- else {
276
- const direction = element.direction ?? "row";
277
- const justify = element.justify ?? "start";
278
- const align = element.align ?? "stretch";
279
- const gap = element.gap ?? 0;
280
- const isRow = direction === "row" || direction === "row-reverse";
281
- const isReverse = direction === "row-reverse" || direction === "column-reverse";
282
- const childInfos = [];
283
- let totalFixed = 0;
284
- let totalFlex = 0;
285
- let totalGap = children.length > 1 ? gap * (children.length - 1) : 0;
286
- for (const child of children) {
287
- const childMargin = normalizeSpacing(child.margin);
288
- const childFlex = child.flex ?? 0;
289
- if (childFlex > 0) {
290
- totalFlex += childFlex;
291
- childInfos.push({
292
- element: child,
293
- width: 0,
294
- height: 0,
295
- flex: childFlex,
296
- margin: childMargin
297
- });
298
- } else {
299
- const size = measureIntrinsicSize(child, ctx, isRow ? contentWidth - childMargin.left - childMargin.right : contentWidth - childMargin.left - childMargin.right);
300
- const shouldStretchWidth = !isRow && child.width === void 0 && align === "stretch";
301
- const shouldStretchHeight = isRow && child.height === void 0 && align === "stretch";
302
- let w = sizeNeedsParent(child.width) ? resolveSize(child.width, contentWidth - childMargin.left - childMargin.right, size.width) : resolveSize(child.width, 0, size.width);
303
- let h = sizeNeedsParent(child.height) ? resolveSize(child.height, contentHeight - childMargin.top - childMargin.bottom, size.height) : resolveSize(child.height, 0, size.height);
304
- if (shouldStretchWidth) w = contentWidth - childMargin.left - childMargin.right;
305
- if (shouldStretchHeight) h = contentHeight - childMargin.top - childMargin.bottom;
306
- if (isRow) totalFixed += w + childMargin.left + childMargin.right;
307
- else totalFixed += h + childMargin.top + childMargin.bottom;
308
- childInfos.push({
309
- element: child,
310
- width: w,
311
- height: h,
312
- flex: 0,
313
- margin: childMargin
314
- });
315
- }
316
- }
317
- const availableForFlex = isRow ? Math.max(0, contentWidth - totalFixed - totalGap) : Math.max(0, contentHeight - totalFixed - totalGap);
318
- for (const info of childInfos) if (info.flex > 0) {
319
- const flexSize = availableForFlex * info.flex / totalFlex;
320
- if (isRow) {
321
- info.width = flexSize;
322
- const size = measureIntrinsicSize(info.element, ctx, flexSize);
323
- 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);
324
- } else {
325
- info.height = flexSize;
326
- const size = measureIntrinsicSize(info.element, ctx, contentWidth - info.margin.left - info.margin.right);
327
- 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);
328
- }
329
- }
330
- const totalSize = childInfos.reduce((sum, info) => {
331
- if (isRow) return sum + info.width + info.margin.left + info.margin.right;
332
- else return sum + info.height + info.margin.top + info.margin.bottom;
333
- }, 0) + totalGap;
334
- const freeSpace = (isRow ? contentWidth : contentHeight) - totalSize;
335
- let mainStart = 0;
336
- let mainGap = gap;
337
- switch (justify) {
338
- case "start":
339
- mainStart = 0;
340
- break;
341
- case "end":
342
- mainStart = freeSpace;
343
- break;
344
- case "center":
345
- mainStart = freeSpace / 2;
346
- break;
347
- case "space-between":
348
- mainStart = 0;
349
- if (children.length > 1) mainGap = gap + freeSpace / (children.length - 1);
350
- break;
351
- case "space-around":
352
- if (children.length > 0) {
353
- const spacing = freeSpace / children.length;
354
- mainStart = spacing / 2;
355
- mainGap = gap + spacing;
356
- }
357
- break;
358
- case "space-evenly":
359
- if (children.length > 0) {
360
- const spacing = freeSpace / (children.length + 1);
361
- mainStart = spacing;
362
- mainGap = gap + spacing;
363
- }
364
- break;
365
- }
366
- let mainOffset = mainStart;
367
- const orderedInfos = isReverse ? [...childInfos].reverse() : childInfos;
368
- for (let i = 0; i < orderedInfos.length; i++) {
369
- const info = orderedInfos[i];
370
- const crossAxisSize = isRow ? contentHeight : contentWidth;
371
- const childCrossSize = isRow ? info.height + info.margin.top + info.margin.bottom : info.width + info.margin.left + info.margin.right;
372
- let crossOffset = 0;
373
- switch (info.element.alignSelf === "auto" || info.element.alignSelf === void 0 ? align : info.element.alignSelf) {
374
- case "start":
375
- crossOffset = 0;
376
- break;
377
- case "end":
378
- crossOffset = crossAxisSize - childCrossSize;
379
- break;
380
- case "center":
381
- crossOffset = (crossAxisSize - childCrossSize) / 2;
382
- break;
383
- case "stretch":
384
- crossOffset = 0;
385
- if (isRow && info.element.height === void 0) info.height = crossAxisSize - info.margin.top - info.margin.bottom;
386
- else if (!isRow && info.element.width === void 0) info.width = crossAxisSize - info.margin.left - info.margin.right;
387
- break;
388
- case "baseline":
389
- crossOffset = 0;
390
- break;
391
- }
392
- const childX = isRow ? contentX + mainOffset + info.margin.left : contentX + crossOffset + info.margin.left;
393
- const childY = isRow ? contentY + crossOffset + info.margin.top : contentY + mainOffset + info.margin.top;
394
- const stretchWidth = !isRow && info.element.width === void 0 && align === "stretch" ? contentWidth - info.margin.left - info.margin.right : null;
395
- const stretchHeight = isRow && info.element.height === void 0 && align === "stretch" ? contentHeight - info.margin.top - info.margin.bottom : null;
396
- const childNode = computeLayout(info.element, ctx, {
397
- minWidth: stretchWidth ?? 0,
398
- maxWidth: stretchWidth ?? info.width,
399
- minHeight: stretchHeight ?? 0,
400
- maxHeight: stretchHeight ?? info.height
401
- }, childX - info.margin.left, childY - info.margin.top);
402
- node.children.push(childNode);
403
- mainOffset += isRow ? info.width + info.margin.left + info.margin.right : info.height + info.margin.top + info.margin.bottom;
404
- if (i < orderedInfos.length - 1) mainOffset += mainGap;
405
- }
406
- if (isReverse) node.children.reverse();
407
- }
408
- }
409
- return node;
410
- }
411
-
412
- //#endregion
413
- //#region src/render/engine.ts
414
- function isGradientDescriptor(color) {
415
- return typeof color === "object" && color !== null && "type" in color && (color.type === "linear-gradient" || color.type === "radial-gradient");
416
- }
417
- function resolveGradient(ctx, descriptor, x, y, width, height) {
418
- if (descriptor.type === "linear-gradient") {
419
- const angleRad = (descriptor.angle - 90) * Math.PI / 180;
420
- const centerX = x + width / 2;
421
- const centerY = y + height / 2;
422
- const diagLength = Math.sqrt(width * width + height * height) / 2;
423
- const x0 = centerX - Math.cos(angleRad) * diagLength;
424
- const y0 = centerY - Math.sin(angleRad) * diagLength;
425
- const x1 = centerX + Math.cos(angleRad) * diagLength;
426
- const y1 = centerY + Math.sin(angleRad) * diagLength;
427
- const gradient = ctx.createLinearGradient(x0, y0, x1, y1);
428
- for (const stop of descriptor.stops) gradient.addColorStop(stop.offset, stop.color);
429
- return gradient;
430
- } else {
431
- const diagLength = Math.sqrt(width * width + height * height);
432
- const startX = x + (descriptor.startX ?? .5) * width;
433
- const startY = y + (descriptor.startY ?? .5) * height;
434
- const startRadius = (descriptor.startRadius ?? 0) * diagLength;
435
- const endX = x + (descriptor.endX ?? .5) * width;
436
- const endY = y + (descriptor.endY ?? .5) * height;
437
- const endRadius = (descriptor.endRadius ?? .5) * diagLength;
438
- const gradient = ctx.createRadialGradient(startX, startY, startRadius, endX, endY, endRadius);
439
- for (const stop of descriptor.stops) gradient.addColorStop(stop.offset, stop.color);
440
- return gradient;
441
- }
442
- }
443
- function resolveColor(ctx, color, x, y, width, height) {
444
- if (isGradientDescriptor(color)) return resolveGradient(ctx, color, x, y, width, height);
445
- return color;
446
- }
447
- function roundRectPath(ctx, x, y, width, height, radius) {
448
- const [tl, tr, br, bl] = radius;
449
- ctx.beginPath();
450
- ctx.moveTo(x + tl, y);
451
- ctx.lineTo(x + width - tr, y);
452
- ctx.quadraticCurveTo(x + width, y, x + width, y + tr);
453
- ctx.lineTo(x + width, y + height - br);
454
- ctx.quadraticCurveTo(x + width, y + height, x + width - br, y + height);
455
- ctx.lineTo(x + bl, y + height);
456
- ctx.quadraticCurveTo(x, y + height, x, y + height - bl);
457
- ctx.lineTo(x, y + tl);
458
- ctx.quadraticCurveTo(x, y, x + tl, y);
459
- ctx.closePath();
460
- }
461
- function applyShadow(ctx, shadow) {
462
- if (shadow) {
463
- ctx.shadowOffsetX = shadow.offsetX ?? 0;
464
- ctx.shadowOffsetY = shadow.offsetY ?? 0;
465
- ctx.shadowBlur = shadow.blur ?? 0;
466
- ctx.shadowColor = shadow.color ?? "rgba(0,0,0,0.5)";
467
- } else {
468
- ctx.shadowOffsetX = 0;
469
- ctx.shadowOffsetY = 0;
470
- ctx.shadowBlur = 0;
471
- ctx.shadowColor = "transparent";
472
- }
473
- }
474
- function clearShadow(ctx) {
475
- ctx.shadowOffsetX = 0;
476
- ctx.shadowOffsetY = 0;
477
- ctx.shadowBlur = 0;
478
- ctx.shadowColor = "transparent";
479
- }
480
- function renderBox(ctx, node) {
481
- const element = node.element;
482
- const { x, y, width, height } = node.layout;
483
- const border = element.border;
484
- const radius = normalizeBorderRadius(border?.radius);
485
- const hasRadius = radius.some((r) => r > 0);
486
- if (element.opacity !== void 0 && element.opacity < 1) ctx.globalAlpha = element.opacity;
487
- if (element.shadow && element.background) applyShadow(ctx, element.shadow);
488
- if (element.background) {
489
- ctx.fillStyle = resolveColor(ctx, element.background, x, y, width, height);
490
- if (hasRadius) {
491
- roundRectPath(ctx, x, y, width, height, radius);
492
- ctx.fill();
493
- } else ctx.fillRect(x, y, width, height);
494
- clearShadow(ctx);
495
- }
496
- if (border && border.width && border.width > 0) {
497
- ctx.strokeStyle = border.color ? resolveColor(ctx, border.color, x, y, width, height) : "#000";
498
- ctx.lineWidth = border.width;
499
- if (hasRadius) {
500
- roundRectPath(ctx, x, y, width, height, radius);
501
- ctx.stroke();
502
- } else ctx.strokeRect(x, y, width, height);
503
- }
504
- if (element.opacity !== void 0 && element.opacity < 1) ctx.globalAlpha = 1;
505
- }
506
- function renderText(ctx, node) {
507
- const element = node.element;
508
- const { contentX, contentY, contentWidth, contentHeight } = node.layout;
509
- const lines = node.lines ?? [element.content];
510
- const font = element.font ?? {};
511
- const lineHeightPx = (font.size ?? 16) * (element.lineHeight ?? 1.2);
512
- ctx.font = buildFontString(font);
513
- ctx.fillStyle = element.color ? resolveColor(ctx, element.color, contentX, contentY, contentWidth, contentHeight) : "#000";
514
- let textAlign = "left";
515
- if (element.align === "center") textAlign = "center";
516
- else if (element.align === "right") textAlign = "right";
517
- ctx.textAlign = textAlign;
518
- ctx.textBaseline = "top";
519
- const totalTextHeight = lines.length * lineHeightPx;
520
- let verticalOffset = 0;
521
- if (element.verticalAlign === "middle") verticalOffset = (contentHeight - totalTextHeight) / 2;
522
- else if (element.verticalAlign === "bottom") verticalOffset = contentHeight - totalTextHeight;
523
- let textX = contentX;
524
- if (element.align === "center") textX = contentX + contentWidth / 2;
525
- else if (element.align === "right") textX = contentX + contentWidth;
526
- if (element.shadow) applyShadow(ctx, element.shadow);
527
- for (let i = 0; i < lines.length; i++) {
528
- const lineY = contentY + verticalOffset + i * lineHeightPx;
529
- if (element.stroke) {
530
- ctx.strokeStyle = resolveColor(ctx, element.stroke.color, contentX, contentY, contentWidth, contentHeight);
531
- ctx.lineWidth = element.stroke.width;
532
- ctx.strokeText(lines[i], textX, lineY);
533
- }
534
- ctx.fillText(lines[i], textX, lineY);
535
- }
536
- if (element.shadow) clearShadow(ctx);
537
- }
538
- function renderNode(ctx, node) {
539
- const element = node.element;
540
- switch (element.type) {
541
- case "box":
542
- case "stack": {
543
- renderBox(ctx, node);
544
- const shouldClip = element.clip === true;
545
- if (shouldClip) {
546
- ctx.save();
547
- const { x, y, width, height } = node.layout;
548
- roundRectPath(ctx, x, y, width, height, normalizeBorderRadius(element.border?.radius));
549
- ctx.clip();
550
- }
551
- for (const child of node.children) renderNode(ctx, child);
552
- if (shouldClip) ctx.restore();
553
- break;
554
- }
555
- case "text":
556
- renderText(ctx, node);
557
- break;
558
- case "image": break;
559
- case "shape": break;
560
- }
561
- }
562
-
563
- //#endregion
564
3
  //#region src/canvas.ts
565
- async function loadNapiCanvas() {
566
- try {
567
- return await import("@napi-rs/canvas");
568
- } catch {
569
- return null;
570
- }
571
- }
572
- function isBrowser() {
573
- return typeof window !== "undefined" && typeof document !== "undefined";
574
- }
4
+ /**
5
+ * 创建适用于浏览器环境的 Canvas
6
+ *
7
+ * 在浏览器环境中使用,支持传入已有的 canvas 实例
8
+ */
575
9
  function createCanvas(options) {
576
10
  const { width, height, pixelRatio = 1 } = options;
577
11
  let canvas;
578
12
  if (options.canvas) canvas = options.canvas;
579
- else if (isBrowser()) {
13
+ else {
580
14
  const el = document.createElement("canvas");
581
15
  el.width = width * pixelRatio;
582
16
  el.height = height * pixelRatio;
583
17
  canvas = el;
584
- } else throw new Error("No canvas provided. In Node.js/Bun environment, use createCanvasAsync() or provide a canvas instance.");
18
+ }
585
19
  const ctx = canvas.getContext("2d");
586
20
  if (!ctx) throw new Error("Failed to get 2d context");
587
21
  if (pixelRatio !== 1) ctx.scale(pixelRatio, pixelRatio);
588
- const measureCtx = createCanvasMeasureContext(ctx);
22
+ const measureCtx = require_render.createCanvasMeasureContext(ctx);
589
23
  return {
590
24
  width,
591
25
  height,
592
26
  pixelRatio,
27
+ canvas,
593
28
  render(element) {
594
- const layoutTree = computeLayout(element, measureCtx, {
29
+ const layoutTree = require_render.computeLayout(element, measureCtx, {
595
30
  minWidth: 0,
596
31
  maxWidth: width,
597
32
  minHeight: 0,
598
33
  maxHeight: height
599
34
  });
600
- renderNode(ctx, layoutTree);
35
+ require_render.renderNode(ctx, layoutTree);
601
36
  return layoutTree;
602
37
  },
603
38
  clear() {
@@ -610,49 +45,12 @@ function createCanvas(options) {
610
45
  if ("toDataURL" in canvas && typeof canvas.toDataURL === "function") return canvas.toDataURL(type, quality);
611
46
  throw new Error("toDataURL not supported");
612
47
  },
613
- async toBuffer(type = "image/png") {
48
+ toBuffer(type = "image/png") {
614
49
  if ("toBuffer" in canvas && typeof canvas.toBuffer === "function") return canvas.toBuffer(type);
615
50
  throw new Error("toBuffer not supported in this environment");
616
51
  }
617
52
  };
618
53
  }
619
- async function createCanvasAsync(options) {
620
- const { width, height, pixelRatio = 1 } = options;
621
- if (isBrowser()) return createCanvas(options);
622
- const napiCanvas = await loadNapiCanvas();
623
- if (!napiCanvas) throw new Error("@napi-rs/canvas is required in Node.js/Bun environment. Install it with: bun add @napi-rs/canvas");
624
- const canvas = napiCanvas.createCanvas(width * pixelRatio, height * pixelRatio);
625
- const ctx = canvas.getContext("2d");
626
- if (pixelRatio !== 1) ctx.scale(pixelRatio, pixelRatio);
627
- const measureCtx = createCanvasMeasureContext(ctx);
628
- return {
629
- width,
630
- height,
631
- pixelRatio,
632
- render(element) {
633
- const layoutTree = computeLayout(element, measureCtx, {
634
- minWidth: 0,
635
- maxWidth: width,
636
- minHeight: 0,
637
- maxHeight: height
638
- });
639
- renderNode(ctx, layoutTree);
640
- return layoutTree;
641
- },
642
- clear() {
643
- ctx.clearRect(0, 0, width, height);
644
- },
645
- getContext() {
646
- return ctx;
647
- },
648
- toDataURL(type, quality) {
649
- return canvas.toDataURL(type, quality);
650
- },
651
- async toBuffer(type = "image/png") {
652
- return canvas.toBuffer(type);
653
- }
654
- };
655
- }
656
54
 
657
55
  //#endregion
658
56
  //#region src/components/Box.ts
@@ -663,6 +61,15 @@ function Box(props) {
663
61
  };
664
62
  }
665
63
 
64
+ //#endregion
65
+ //#region src/components/Image.ts
66
+ function Image(props) {
67
+ return {
68
+ type: "image",
69
+ ...props
70
+ };
71
+ }
72
+
666
73
  //#endregion
667
74
  //#region src/components/Stack.ts
668
75
  function Stack(props) {
@@ -672,6 +79,53 @@ function Stack(props) {
672
79
  };
673
80
  }
674
81
 
82
+ //#endregion
83
+ //#region src/components/Svg.ts
84
+ function Svg(props) {
85
+ return {
86
+ type: "svg",
87
+ ...props
88
+ };
89
+ }
90
+ const svg = {
91
+ rect: (props) => ({
92
+ type: "rect",
93
+ ...props
94
+ }),
95
+ circle: (props) => ({
96
+ type: "circle",
97
+ ...props
98
+ }),
99
+ ellipse: (props) => ({
100
+ type: "ellipse",
101
+ ...props
102
+ }),
103
+ line: (props) => ({
104
+ type: "line",
105
+ ...props
106
+ }),
107
+ polyline: (props) => ({
108
+ type: "polyline",
109
+ ...props
110
+ }),
111
+ polygon: (props) => ({
112
+ type: "polygon",
113
+ ...props
114
+ }),
115
+ path: (props) => ({
116
+ type: "path",
117
+ ...props
118
+ }),
119
+ text: (props) => ({
120
+ type: "text",
121
+ ...props
122
+ }),
123
+ g: (props) => ({
124
+ type: "g",
125
+ ...props
126
+ })
127
+ };
128
+
675
129
  //#endregion
676
130
  //#region src/components/Text.ts
677
131
  function Text(props) {
@@ -683,11 +137,13 @@ function Text(props) {
683
137
 
684
138
  //#endregion
685
139
  exports.Box = Box;
140
+ exports.Image = Image;
686
141
  exports.Stack = Stack;
142
+ exports.Svg = Svg;
687
143
  exports.Text = Text;
688
- exports.computeLayout = computeLayout;
144
+ exports.computeLayout = require_render.computeLayout;
689
145
  exports.createCanvas = createCanvas;
690
- exports.createCanvasAsync = createCanvasAsync;
691
- exports.createCanvasMeasureContext = createCanvasMeasureContext;
692
- exports.linearGradient = linearGradient;
693
- exports.radialGradient = radialGradient;
146
+ exports.createCanvasMeasureContext = require_render.createCanvasMeasureContext;
147
+ exports.linearGradient = require_render.linearGradient;
148
+ exports.radialGradient = require_render.radialGradient;
149
+ exports.svg = svg;