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