@codehz/draw-call 0.2.2 → 0.4.1
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/browser/index.cjs +2058 -0
- package/{canvas.d.cts → browser/index.d.cts} +52 -1
- package/{canvas.d.mts → browser/index.d.ts} +52 -1
- package/{render.cjs → browser/index.js} +240 -65
- package/examples/card.ts +2 -3
- package/examples/customdraw-basic.ts +2 -3
- package/examples/customdraw.ts +2 -3
- package/examples/richtext.ts +2 -3
- package/examples/transform.ts +2 -3
- package/node/index.cjs +2054 -0
- package/node/index.d.cts +452 -0
- package/node/index.d.mts +452 -0
- package/{render.mjs → node/index.mjs} +235 -39
- package/package.json +19 -24
- package/index.cjs +0 -241
- package/index.d.cts +0 -54
- package/index.d.mts +0 -54
- package/index.mjs +0 -226
- package/node.cjs +0 -50
- package/node.d.cts +0 -12
- package/node.d.mts +0 -12
- package/node.mjs +0 -50
|
@@ -0,0 +1,2058 @@
|
|
|
1
|
+
|
|
2
|
+
//#region src/compat/index.ts
|
|
3
|
+
const DOMMatrix = window.DOMMatrix;
|
|
4
|
+
const Path2D = window.Path2D;
|
|
5
|
+
function createRawCanvas(width, height) {
|
|
6
|
+
const canvas = document.createElement("canvas");
|
|
7
|
+
canvas.width = width;
|
|
8
|
+
canvas.height = height;
|
|
9
|
+
return canvas;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
//#endregion
|
|
13
|
+
//#region src/types/base.ts
|
|
14
|
+
function linearGradient(angle, ...stops) {
|
|
15
|
+
return {
|
|
16
|
+
type: "linear-gradient",
|
|
17
|
+
angle,
|
|
18
|
+
stops: stops.map((stop, index) => {
|
|
19
|
+
if (typeof stop === "string") return {
|
|
20
|
+
offset: stops.length > 1 ? index / (stops.length - 1) : 0,
|
|
21
|
+
color: stop
|
|
22
|
+
};
|
|
23
|
+
return {
|
|
24
|
+
offset: stop[0],
|
|
25
|
+
color: stop[1]
|
|
26
|
+
};
|
|
27
|
+
})
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function radialGradient(options, ...stops) {
|
|
31
|
+
const colorStops = stops.map((stop, index) => {
|
|
32
|
+
if (typeof stop === "string") return {
|
|
33
|
+
offset: stops.length > 1 ? index / (stops.length - 1) : 0,
|
|
34
|
+
color: stop
|
|
35
|
+
};
|
|
36
|
+
return {
|
|
37
|
+
offset: stop[0],
|
|
38
|
+
color: stop[1]
|
|
39
|
+
};
|
|
40
|
+
});
|
|
41
|
+
return {
|
|
42
|
+
type: "radial-gradient",
|
|
43
|
+
...options,
|
|
44
|
+
stops: colorStops
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function normalizeSpacing(value) {
|
|
48
|
+
if (value === void 0) return {
|
|
49
|
+
top: 0,
|
|
50
|
+
right: 0,
|
|
51
|
+
bottom: 0,
|
|
52
|
+
left: 0
|
|
53
|
+
};
|
|
54
|
+
if (typeof value === "number") return {
|
|
55
|
+
top: value,
|
|
56
|
+
right: value,
|
|
57
|
+
bottom: value,
|
|
58
|
+
left: value
|
|
59
|
+
};
|
|
60
|
+
return {
|
|
61
|
+
top: value.top ?? 0,
|
|
62
|
+
right: value.right ?? 0,
|
|
63
|
+
bottom: value.bottom ?? 0,
|
|
64
|
+
left: value.left ?? 0
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function normalizeBorderRadius(value) {
|
|
68
|
+
if (value === void 0) return [
|
|
69
|
+
0,
|
|
70
|
+
0,
|
|
71
|
+
0,
|
|
72
|
+
0
|
|
73
|
+
];
|
|
74
|
+
if (typeof value === "number") return [
|
|
75
|
+
value,
|
|
76
|
+
value,
|
|
77
|
+
value,
|
|
78
|
+
value
|
|
79
|
+
];
|
|
80
|
+
return value;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
//#endregion
|
|
84
|
+
//#region src/layout/components/box.ts
|
|
85
|
+
function calcEffectiveSize(element, padding, availableWidth) {
|
|
86
|
+
return {
|
|
87
|
+
width: typeof element.width === "number" ? element.width - padding.left - padding.right : availableWidth > 0 ? availableWidth : 0,
|
|
88
|
+
height: typeof element.height === "number" ? element.height - padding.top - padding.bottom : 0
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function collectChildSizes(children, ctx, availableWidth, padding, measureChild) {
|
|
92
|
+
const childSizes = [];
|
|
93
|
+
for (const child of children) {
|
|
94
|
+
const childMargin = normalizeSpacing(child.margin);
|
|
95
|
+
const childSize = measureChild(child, ctx, availableWidth - padding.left - padding.right - childMargin.left - childMargin.right);
|
|
96
|
+
childSizes.push({
|
|
97
|
+
width: childSize.width,
|
|
98
|
+
height: childSize.height,
|
|
99
|
+
margin: childMargin
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
return childSizes;
|
|
103
|
+
}
|
|
104
|
+
function measureWrappedContent(childSizes, gap, availableMain, isRow) {
|
|
105
|
+
let currentMain = 0;
|
|
106
|
+
let currentCross = 0;
|
|
107
|
+
let totalCross = 0;
|
|
108
|
+
let maxMain = 0;
|
|
109
|
+
let lineCount = 0;
|
|
110
|
+
for (let i = 0; i < childSizes.length; i++) {
|
|
111
|
+
const { width, height, margin } = childSizes[i];
|
|
112
|
+
const itemMain = isRow ? width + margin.left + margin.right : height + margin.top + margin.bottom;
|
|
113
|
+
const itemCross = isRow ? height + margin.top + margin.bottom : width + margin.left + margin.right;
|
|
114
|
+
if (lineCount > 0 && currentMain + gap + itemMain > availableMain) {
|
|
115
|
+
totalCross += currentCross;
|
|
116
|
+
maxMain = Math.max(maxMain, currentMain);
|
|
117
|
+
lineCount++;
|
|
118
|
+
currentMain = itemMain;
|
|
119
|
+
currentCross = itemCross;
|
|
120
|
+
} else {
|
|
121
|
+
if (lineCount > 0 || i > 0) currentMain += gap;
|
|
122
|
+
currentMain += itemMain;
|
|
123
|
+
currentCross = Math.max(currentCross, itemCross);
|
|
124
|
+
if (i === 0) lineCount = 1;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (childSizes.length > 0) {
|
|
128
|
+
totalCross += currentCross;
|
|
129
|
+
maxMain = Math.max(maxMain, currentMain);
|
|
130
|
+
}
|
|
131
|
+
if (lineCount > 1) totalCross += gap * (lineCount - 1);
|
|
132
|
+
return isRow ? {
|
|
133
|
+
width: maxMain,
|
|
134
|
+
height: totalCross
|
|
135
|
+
} : {
|
|
136
|
+
width: totalCross,
|
|
137
|
+
height: maxMain
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* 测量 Box 元素的固有尺寸
|
|
142
|
+
*/
|
|
143
|
+
function measureBoxSize(element, ctx, availableWidth, measureChild) {
|
|
144
|
+
const padding = normalizeSpacing(element.padding);
|
|
145
|
+
const gap = element.gap ?? 0;
|
|
146
|
+
const direction = element.direction ?? "row";
|
|
147
|
+
const wrap = element.wrap ?? false;
|
|
148
|
+
const isRow = direction === "row" || direction === "row-reverse";
|
|
149
|
+
let contentWidth = 0;
|
|
150
|
+
let contentHeight = 0;
|
|
151
|
+
const children = element.children ?? [];
|
|
152
|
+
const { width: effectiveWidth, height: effectiveHeight } = calcEffectiveSize(element, padding, availableWidth);
|
|
153
|
+
if (wrap && isRow && effectiveWidth > 0) {
|
|
154
|
+
const wrapped = measureWrappedContent(collectChildSizes(children, ctx, availableWidth, padding, measureChild), gap, effectiveWidth, true);
|
|
155
|
+
contentWidth = wrapped.width;
|
|
156
|
+
contentHeight = wrapped.height;
|
|
157
|
+
} else if (wrap && !isRow && effectiveHeight > 0) {
|
|
158
|
+
const wrapped = measureWrappedContent(collectChildSizes(children, ctx, availableWidth, padding, measureChild), gap, effectiveHeight, false);
|
|
159
|
+
contentWidth = wrapped.width;
|
|
160
|
+
contentHeight = wrapped.height;
|
|
161
|
+
} else for (let i = 0; i < children.length; i++) {
|
|
162
|
+
const child = children[i];
|
|
163
|
+
const childMargin = normalizeSpacing(child.margin);
|
|
164
|
+
const childSize = measureChild(child, ctx, availableWidth - padding.left - padding.right - childMargin.left - childMargin.right);
|
|
165
|
+
if (isRow) {
|
|
166
|
+
contentWidth += childSize.width + childMargin.left + childMargin.right;
|
|
167
|
+
contentHeight = Math.max(contentHeight, childSize.height + childMargin.top + childMargin.bottom);
|
|
168
|
+
if (i > 0) contentWidth += gap;
|
|
169
|
+
} else {
|
|
170
|
+
contentHeight += childSize.height + childMargin.top + childMargin.bottom;
|
|
171
|
+
contentWidth = Math.max(contentWidth, childSize.width + childMargin.left + childMargin.right);
|
|
172
|
+
if (i > 0) contentHeight += gap;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const intrinsicWidth = contentWidth + padding.left + padding.right;
|
|
176
|
+
const intrinsicHeight = contentHeight + padding.top + padding.bottom;
|
|
177
|
+
return {
|
|
178
|
+
width: typeof element.width === "number" ? element.width : intrinsicWidth,
|
|
179
|
+
height: typeof element.height === "number" ? element.height : intrinsicHeight
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
//#endregion
|
|
184
|
+
//#region src/layout/components/customDraw.ts
|
|
185
|
+
/**
|
|
186
|
+
* 测量 CustomDraw 元素的固有尺寸
|
|
187
|
+
*/
|
|
188
|
+
function measureCustomDrawSize(element, ctx, availableWidth, measureChild) {
|
|
189
|
+
if (typeof element.width === "number" && typeof element.height === "number") return {
|
|
190
|
+
width: element.width,
|
|
191
|
+
height: element.height
|
|
192
|
+
};
|
|
193
|
+
if (element.children && measureChild) return measureChild(element.children, ctx, availableWidth);
|
|
194
|
+
return {
|
|
195
|
+
width: 0,
|
|
196
|
+
height: 0
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
//#endregion
|
|
201
|
+
//#region src/layout/components/image.ts
|
|
202
|
+
/**
|
|
203
|
+
* 测量 Image 元素的固有尺寸
|
|
204
|
+
*/
|
|
205
|
+
function measureImageSize(element, _ctx, _availableWidth) {
|
|
206
|
+
const imageElement = element;
|
|
207
|
+
if (imageElement.width !== void 0 && imageElement.height !== void 0) return {
|
|
208
|
+
width: typeof imageElement.width === "number" ? imageElement.width : 0,
|
|
209
|
+
height: typeof imageElement.height === "number" ? imageElement.height : 0
|
|
210
|
+
};
|
|
211
|
+
const src = imageElement.src;
|
|
212
|
+
if (src) {
|
|
213
|
+
const imgWidth = "naturalWidth" in src ? src.naturalWidth : "width" in src ? +src.width : 0;
|
|
214
|
+
const imgHeight = "naturalHeight" in src ? src.naturalHeight : "height" in src ? +src.height : 0;
|
|
215
|
+
if (imgWidth > 0 && imgHeight > 0) return {
|
|
216
|
+
width: imgWidth,
|
|
217
|
+
height: imgHeight
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
return {
|
|
221
|
+
width: 0,
|
|
222
|
+
height: 0
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
//#endregion
|
|
227
|
+
//#region src/layout/components/richtext.ts
|
|
228
|
+
/**
|
|
229
|
+
* 合并 span 样式和元素级别样式
|
|
230
|
+
* 优先级:span 样式 > 元素样式 > 默认值
|
|
231
|
+
* font 属性进行深度合并,允许 span 部分覆盖 element 的 font
|
|
232
|
+
*/
|
|
233
|
+
function mergeSpanStyle(span, elementStyle) {
|
|
234
|
+
return {
|
|
235
|
+
font: {
|
|
236
|
+
...elementStyle.font || {},
|
|
237
|
+
...span.font || {}
|
|
238
|
+
},
|
|
239
|
+
color: span.color ?? elementStyle.color,
|
|
240
|
+
background: span.background ?? elementStyle.background,
|
|
241
|
+
underline: span.underline ?? elementStyle.underline ?? false,
|
|
242
|
+
strikethrough: span.strikethrough ?? elementStyle.strikethrough ?? false
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* 测量富文本元素的固有尺寸
|
|
247
|
+
*/
|
|
248
|
+
function measureRichTextSize(element, ctx, availableWidth) {
|
|
249
|
+
const lineHeight = element.lineHeight ?? 1.2;
|
|
250
|
+
const elementStyle = {
|
|
251
|
+
font: element.font,
|
|
252
|
+
color: element.color,
|
|
253
|
+
background: element.background,
|
|
254
|
+
underline: element.underline,
|
|
255
|
+
strikethrough: element.strikethrough
|
|
256
|
+
};
|
|
257
|
+
const richLines = wrapRichText(ctx, element.spans, availableWidth, lineHeight, elementStyle);
|
|
258
|
+
let maxWidth = 0;
|
|
259
|
+
let totalHeight = 0;
|
|
260
|
+
for (const line of richLines) {
|
|
261
|
+
maxWidth = Math.max(maxWidth, line.width);
|
|
262
|
+
totalHeight += line.height;
|
|
263
|
+
}
|
|
264
|
+
return {
|
|
265
|
+
width: maxWidth,
|
|
266
|
+
height: totalHeight
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* 将富文本内容拆分为行
|
|
271
|
+
*/
|
|
272
|
+
function wrapRichText(ctx, spans, maxWidth, lineHeightScale = 1.2, elementStyle = {}) {
|
|
273
|
+
const lines = [];
|
|
274
|
+
let currentSegments = [];
|
|
275
|
+
let currentLineWidth = 0;
|
|
276
|
+
const pushLine = () => {
|
|
277
|
+
if (currentSegments.length === 0) return;
|
|
278
|
+
let maxTopDist = 0;
|
|
279
|
+
let maxBottomDist = 0;
|
|
280
|
+
let maxLineHeight = 0;
|
|
281
|
+
for (const seg of currentSegments) {
|
|
282
|
+
const topDist = seg.ascent - seg.offset;
|
|
283
|
+
const bottomDist = seg.descent + seg.offset;
|
|
284
|
+
maxTopDist = Math.max(maxTopDist, topDist);
|
|
285
|
+
maxBottomDist = Math.max(maxBottomDist, bottomDist);
|
|
286
|
+
maxLineHeight = Math.max(maxLineHeight, seg.height);
|
|
287
|
+
}
|
|
288
|
+
const contentHeight = maxTopDist + maxBottomDist;
|
|
289
|
+
const finalHeight = Math.max(contentHeight, maxLineHeight);
|
|
290
|
+
const extra = (finalHeight - contentHeight) / 2;
|
|
291
|
+
lines.push({
|
|
292
|
+
segments: [...currentSegments],
|
|
293
|
+
width: currentLineWidth,
|
|
294
|
+
height: finalHeight,
|
|
295
|
+
baseline: maxTopDist + extra
|
|
296
|
+
});
|
|
297
|
+
currentSegments = [];
|
|
298
|
+
currentLineWidth = 0;
|
|
299
|
+
};
|
|
300
|
+
for (const span of spans) {
|
|
301
|
+
const mergedStyle = mergeSpanStyle(span, elementStyle);
|
|
302
|
+
const font = mergedStyle.font;
|
|
303
|
+
const lh = (font.size ?? 16) * lineHeightScale;
|
|
304
|
+
const words = span.text.split(/(\s+)/);
|
|
305
|
+
for (const word of words) {
|
|
306
|
+
if (word === "") continue;
|
|
307
|
+
if (/^\s+$/.test(word)) {
|
|
308
|
+
const metrics = ctx.measureText(word, font);
|
|
309
|
+
const wordWidth = metrics.width;
|
|
310
|
+
if (maxWidth > 0 && currentLineWidth + wordWidth > maxWidth && currentSegments.length > 0) pushLine();
|
|
311
|
+
currentSegments.push({
|
|
312
|
+
text: word,
|
|
313
|
+
font: mergedStyle.font,
|
|
314
|
+
color: mergedStyle.color,
|
|
315
|
+
background: mergedStyle.background,
|
|
316
|
+
underline: mergedStyle.underline,
|
|
317
|
+
strikethrough: mergedStyle.strikethrough,
|
|
318
|
+
width: wordWidth,
|
|
319
|
+
height: lh,
|
|
320
|
+
ascent: metrics.ascent,
|
|
321
|
+
descent: metrics.descent,
|
|
322
|
+
offset: metrics.offset
|
|
323
|
+
});
|
|
324
|
+
currentLineWidth += wordWidth;
|
|
325
|
+
} else {
|
|
326
|
+
const metrics = ctx.measureText(word, font);
|
|
327
|
+
const wordWidth = metrics.width;
|
|
328
|
+
if (maxWidth <= 0 || currentLineWidth + wordWidth <= maxWidth) {
|
|
329
|
+
currentSegments.push({
|
|
330
|
+
text: word,
|
|
331
|
+
font: mergedStyle.font,
|
|
332
|
+
color: mergedStyle.color,
|
|
333
|
+
background: mergedStyle.background,
|
|
334
|
+
underline: mergedStyle.underline,
|
|
335
|
+
strikethrough: mergedStyle.strikethrough,
|
|
336
|
+
width: wordWidth,
|
|
337
|
+
height: lh,
|
|
338
|
+
ascent: metrics.ascent,
|
|
339
|
+
descent: metrics.descent,
|
|
340
|
+
offset: metrics.offset
|
|
341
|
+
});
|
|
342
|
+
currentLineWidth += wordWidth;
|
|
343
|
+
} else {
|
|
344
|
+
if (currentSegments.length > 0) pushLine();
|
|
345
|
+
const remainingWidth = maxWidth;
|
|
346
|
+
let currentPos = 0;
|
|
347
|
+
while (currentPos < word.length) {
|
|
348
|
+
let bestLen = 0;
|
|
349
|
+
for (let len = word.length - currentPos; len > 0; len--) {
|
|
350
|
+
const substr = word.substring(currentPos, currentPos + len);
|
|
351
|
+
const m = ctx.measureText(substr, font);
|
|
352
|
+
if (currentLineWidth + m.width <= remainingWidth) {
|
|
353
|
+
bestLen = len;
|
|
354
|
+
if (len < word.length - currentPos) break;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
if (bestLen === 0) {
|
|
358
|
+
if (currentSegments.length > 0) pushLine();
|
|
359
|
+
bestLen = 1;
|
|
360
|
+
}
|
|
361
|
+
const substr = word.substring(currentPos, currentPos + bestLen);
|
|
362
|
+
const m = ctx.measureText(substr, font);
|
|
363
|
+
currentSegments.push({
|
|
364
|
+
text: substr,
|
|
365
|
+
font: mergedStyle.font,
|
|
366
|
+
color: mergedStyle.color,
|
|
367
|
+
background: mergedStyle.background,
|
|
368
|
+
underline: mergedStyle.underline,
|
|
369
|
+
strikethrough: mergedStyle.strikethrough,
|
|
370
|
+
width: m.width,
|
|
371
|
+
height: lh,
|
|
372
|
+
ascent: m.ascent,
|
|
373
|
+
descent: m.descent,
|
|
374
|
+
offset: m.offset
|
|
375
|
+
});
|
|
376
|
+
currentLineWidth += m.width;
|
|
377
|
+
currentPos += bestLen;
|
|
378
|
+
if (currentPos < word.length && currentLineWidth >= remainingWidth) pushLine();
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
pushLine();
|
|
385
|
+
if (lines.length === 0) return [{
|
|
386
|
+
segments: [],
|
|
387
|
+
width: 0,
|
|
388
|
+
height: 0,
|
|
389
|
+
baseline: 0
|
|
390
|
+
}];
|
|
391
|
+
return lines;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
//#endregion
|
|
395
|
+
//#region src/layout/components/stack.ts
|
|
396
|
+
/**
|
|
397
|
+
* 测量 Stack 元素的固有尺寸
|
|
398
|
+
*/
|
|
399
|
+
function measureStackSize(element, ctx, availableWidth, measureChild) {
|
|
400
|
+
const padding = normalizeSpacing(element.padding);
|
|
401
|
+
let contentWidth = 0;
|
|
402
|
+
let contentHeight = 0;
|
|
403
|
+
const children = element.children ?? [];
|
|
404
|
+
for (const child of children) {
|
|
405
|
+
const childMargin = normalizeSpacing(child.margin);
|
|
406
|
+
const childSize = measureChild(child, ctx, availableWidth - padding.left - padding.right - childMargin.left - childMargin.right);
|
|
407
|
+
contentWidth = Math.max(contentWidth, childSize.width + childMargin.left + childMargin.right);
|
|
408
|
+
contentHeight = Math.max(contentHeight, childSize.height + childMargin.top + childMargin.bottom);
|
|
409
|
+
}
|
|
410
|
+
const intrinsicWidth = contentWidth + padding.left + padding.right;
|
|
411
|
+
const intrinsicHeight = contentHeight + padding.top + padding.bottom;
|
|
412
|
+
return {
|
|
413
|
+
width: typeof element.width === "number" ? element.width : intrinsicWidth,
|
|
414
|
+
height: typeof element.height === "number" ? element.height : intrinsicHeight
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
//#endregion
|
|
419
|
+
//#region src/layout/components/svg.ts
|
|
420
|
+
/**
|
|
421
|
+
* 测量 SVG 元素的固有尺寸
|
|
422
|
+
*/
|
|
423
|
+
function measureSvgSize(element, _ctx, _availableWidth) {
|
|
424
|
+
const svgElement = element;
|
|
425
|
+
if (svgElement.width !== void 0 && svgElement.height !== void 0) return {
|
|
426
|
+
width: typeof svgElement.width === "number" ? svgElement.width : 0,
|
|
427
|
+
height: typeof svgElement.height === "number" ? svgElement.height : 0
|
|
428
|
+
};
|
|
429
|
+
if (svgElement.viewBox) {
|
|
430
|
+
const viewBoxWidth = svgElement.viewBox.width;
|
|
431
|
+
const viewBoxHeight = svgElement.viewBox.height;
|
|
432
|
+
const aspectRatio = viewBoxWidth / viewBoxHeight;
|
|
433
|
+
if (svgElement.width !== void 0 && typeof svgElement.width === "number") return {
|
|
434
|
+
width: svgElement.width,
|
|
435
|
+
height: svgElement.width / aspectRatio
|
|
436
|
+
};
|
|
437
|
+
if (svgElement.height !== void 0 && typeof svgElement.height === "number") return {
|
|
438
|
+
width: svgElement.height * aspectRatio,
|
|
439
|
+
height: svgElement.height
|
|
440
|
+
};
|
|
441
|
+
return {
|
|
442
|
+
width: viewBoxWidth,
|
|
443
|
+
height: viewBoxHeight
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
return {
|
|
447
|
+
width: 0,
|
|
448
|
+
height: 0
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
//#endregion
|
|
453
|
+
//#region src/render/utils/font.ts
|
|
454
|
+
/**
|
|
455
|
+
* 构建 Canvas 字体字符串
|
|
456
|
+
* @param font 字体属性
|
|
457
|
+
* @returns CSS 字体字符串
|
|
458
|
+
*/
|
|
459
|
+
function buildFontString(font) {
|
|
460
|
+
return `${font.style ?? "normal"} ${font.weight ?? "normal"} ${font.size ?? 16}px ${font.family ?? "sans-serif"}`;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
//#endregion
|
|
464
|
+
//#region src/layout/utils/measure.ts
|
|
465
|
+
function createCanvasMeasureContext(ctx) {
|
|
466
|
+
return { measureText(text, font) {
|
|
467
|
+
ctx.font = buildFontString(font);
|
|
468
|
+
ctx.textBaseline = "middle";
|
|
469
|
+
const metrics = ctx.measureText(text);
|
|
470
|
+
const height = metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent;
|
|
471
|
+
const fontSize = font.size || 16;
|
|
472
|
+
return {
|
|
473
|
+
width: metrics.width,
|
|
474
|
+
height: height || fontSize,
|
|
475
|
+
offset: (metrics.actualBoundingBoxAscent - metrics.actualBoundingBoxDescent) / 2,
|
|
476
|
+
ascent: metrics.actualBoundingBoxAscent,
|
|
477
|
+
descent: metrics.actualBoundingBoxDescent
|
|
478
|
+
};
|
|
479
|
+
} };
|
|
480
|
+
}
|
|
481
|
+
function wrapText(ctx, text, maxWidth, font) {
|
|
482
|
+
if (maxWidth <= 0) {
|
|
483
|
+
const { offset } = ctx.measureText(text, font);
|
|
484
|
+
return {
|
|
485
|
+
lines: [text],
|
|
486
|
+
offsets: [offset]
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
const lines = [];
|
|
490
|
+
const offsets = [];
|
|
491
|
+
const paragraphs = text.split("\n");
|
|
492
|
+
for (const paragraph of paragraphs) {
|
|
493
|
+
if (paragraph === "") {
|
|
494
|
+
lines.push("");
|
|
495
|
+
offsets.push(0);
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
const words = paragraph.split(/(\s+)/);
|
|
499
|
+
let currentLine = "";
|
|
500
|
+
for (const word of words) {
|
|
501
|
+
const testLine = currentLine + word;
|
|
502
|
+
const { width } = ctx.measureText(testLine, font);
|
|
503
|
+
if (width > maxWidth && currentLine !== "") {
|
|
504
|
+
const trimmed = currentLine.trim();
|
|
505
|
+
lines.push(trimmed);
|
|
506
|
+
offsets.push(ctx.measureText(trimmed, font).offset);
|
|
507
|
+
currentLine = word.trimStart();
|
|
508
|
+
} else currentLine = testLine;
|
|
509
|
+
}
|
|
510
|
+
if (currentLine) {
|
|
511
|
+
const trimmed = currentLine.trim();
|
|
512
|
+
lines.push(trimmed);
|
|
513
|
+
offsets.push(ctx.measureText(trimmed, font).offset);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
return lines.length > 0 ? {
|
|
517
|
+
lines,
|
|
518
|
+
offsets
|
|
519
|
+
} : {
|
|
520
|
+
lines: [""],
|
|
521
|
+
offsets: [0]
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
function truncateText(ctx, text, maxWidth, font, ellipsis = "...") {
|
|
525
|
+
const measured = ctx.measureText(text, font);
|
|
526
|
+
if (measured.width <= maxWidth) return {
|
|
527
|
+
text,
|
|
528
|
+
offset: measured.offset
|
|
529
|
+
};
|
|
530
|
+
const availableWidth = maxWidth - ctx.measureText(ellipsis, font).width;
|
|
531
|
+
if (availableWidth <= 0) {
|
|
532
|
+
const { offset } = ctx.measureText(ellipsis, font);
|
|
533
|
+
return {
|
|
534
|
+
text: ellipsis,
|
|
535
|
+
offset
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
let left = 0;
|
|
539
|
+
let right = text.length;
|
|
540
|
+
while (left < right) {
|
|
541
|
+
const mid = Math.floor((left + right + 1) / 2);
|
|
542
|
+
const truncated = text.slice(0, mid);
|
|
543
|
+
const { width: truncatedWidth } = ctx.measureText(truncated, font);
|
|
544
|
+
if (truncatedWidth <= availableWidth) left = mid;
|
|
545
|
+
else right = mid - 1;
|
|
546
|
+
}
|
|
547
|
+
const result = text.slice(0, left) + ellipsis;
|
|
548
|
+
const { offset } = ctx.measureText(result, font);
|
|
549
|
+
return {
|
|
550
|
+
text: result,
|
|
551
|
+
offset
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
//#endregion
|
|
556
|
+
//#region src/layout/components/text.ts
|
|
557
|
+
/**
|
|
558
|
+
* 测量文本元素的固有尺寸
|
|
559
|
+
*/
|
|
560
|
+
function measureTextSize(element, ctx, availableWidth) {
|
|
561
|
+
const font = element.font ?? {};
|
|
562
|
+
const lineHeightPx = (font.size ?? 16) * (element.lineHeight ?? 1.2);
|
|
563
|
+
if (element.wrap && availableWidth > 0 && availableWidth < Infinity) {
|
|
564
|
+
const { lines } = wrapText(ctx, element.content, availableWidth, font);
|
|
565
|
+
const { width: maxLineWidth } = lines.reduce((max, line) => {
|
|
566
|
+
const { width } = ctx.measureText(line, font);
|
|
567
|
+
return width > max.width ? { width } : max;
|
|
568
|
+
}, { width: 0 });
|
|
569
|
+
return {
|
|
570
|
+
width: maxLineWidth,
|
|
571
|
+
height: lines.length * lineHeightPx
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
const { width, height } = ctx.measureText(element.content, font);
|
|
575
|
+
return {
|
|
576
|
+
width,
|
|
577
|
+
height: Math.max(height, lineHeightPx)
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
//#endregion
|
|
582
|
+
//#region src/layout/components/transform.ts
|
|
583
|
+
/**
|
|
584
|
+
* 测量 Transform 元素的固有尺寸
|
|
585
|
+
* Transform 不施加任何尺寸约束,直接透传子元素的测量结果
|
|
586
|
+
* 变换(rotate, scale 等)仅在渲染时应用,不影响固有尺寸
|
|
587
|
+
*/
|
|
588
|
+
function measureTransformSize(element, ctx, availableWidth, measureIntrinsicSize) {
|
|
589
|
+
return measureIntrinsicSize(element.children, ctx, availableWidth);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
//#endregion
|
|
593
|
+
//#region src/layout/components/index.ts
|
|
594
|
+
/**
|
|
595
|
+
* 计算元素的固有尺寸(不依赖父容器的尺寸)
|
|
596
|
+
*/
|
|
597
|
+
function measureIntrinsicSize(element, ctx, availableWidth) {
|
|
598
|
+
switch (element.type) {
|
|
599
|
+
case "text": return measureTextSize(element, ctx, availableWidth);
|
|
600
|
+
case "richtext": return measureRichTextSize(element, ctx, availableWidth);
|
|
601
|
+
case "box": return measureBoxSize(element, ctx, availableWidth, measureIntrinsicSize);
|
|
602
|
+
case "stack": return measureStackSize(element, ctx, availableWidth, measureIntrinsicSize);
|
|
603
|
+
case "image": return measureImageSize(element, ctx, availableWidth);
|
|
604
|
+
case "svg": return measureSvgSize(element, ctx, availableWidth);
|
|
605
|
+
case "transform": return measureTransformSize(element, ctx, availableWidth, measureIntrinsicSize);
|
|
606
|
+
case "customdraw": return measureCustomDrawSize(element, ctx, availableWidth, measureIntrinsicSize);
|
|
607
|
+
default: return {
|
|
608
|
+
width: 0,
|
|
609
|
+
height: 0
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
//#endregion
|
|
615
|
+
//#region src/layout/utils/offset.ts
|
|
616
|
+
/**
|
|
617
|
+
* 递归地为布局节点及其子节点应用位置偏移
|
|
618
|
+
*/
|
|
619
|
+
function applyOffset(node, dx, dy) {
|
|
620
|
+
node.layout.x += dx;
|
|
621
|
+
node.layout.y += dy;
|
|
622
|
+
node.layout.contentX += dx;
|
|
623
|
+
node.layout.contentY += dy;
|
|
624
|
+
for (const child of node.children) applyOffset(child, dx, dy);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
//#endregion
|
|
628
|
+
//#region src/types/layout.ts
|
|
629
|
+
function resolveSize(size, available, auto) {
|
|
630
|
+
if (size === void 0 || size === "auto") return auto;
|
|
631
|
+
if (size === "fill") return available;
|
|
632
|
+
if (typeof size === "number") return size;
|
|
633
|
+
return available * parseFloat(size) / 100;
|
|
634
|
+
}
|
|
635
|
+
function sizeNeedsParent(size) {
|
|
636
|
+
if (size === void 0 || size === "auto") return false;
|
|
637
|
+
if (size === "fill") return true;
|
|
638
|
+
if (typeof size === "string" && size.endsWith("%")) return true;
|
|
639
|
+
return false;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
//#endregion
|
|
643
|
+
//#region src/layout/engine.ts
|
|
644
|
+
function computeLayout(element, ctx, constraints, x = 0, y = 0) {
|
|
645
|
+
const margin = normalizeSpacing(element.margin);
|
|
646
|
+
const padding = normalizeSpacing("padding" in element ? element.padding : void 0);
|
|
647
|
+
const availableWidth = constraints.maxWidth - margin.left - margin.right;
|
|
648
|
+
const availableHeight = constraints.maxHeight - margin.top - margin.bottom;
|
|
649
|
+
const intrinsic = measureIntrinsicSize(element, ctx, availableWidth);
|
|
650
|
+
let width = constraints.minWidth === constraints.maxWidth && constraints.minWidth > 0 ? constraints.maxWidth - margin.left - margin.right : resolveSize(element.width, availableWidth, intrinsic.width);
|
|
651
|
+
let height = constraints.minHeight === constraints.maxHeight && constraints.minHeight > 0 ? constraints.maxHeight - margin.top - margin.bottom : resolveSize(element.height, availableHeight, intrinsic.height);
|
|
652
|
+
if (element.minWidth !== void 0) width = Math.max(width, element.minWidth);
|
|
653
|
+
if (element.maxWidth !== void 0) width = Math.min(width, element.maxWidth);
|
|
654
|
+
if (element.minHeight !== void 0) height = Math.max(height, element.minHeight);
|
|
655
|
+
if (element.maxHeight !== void 0) height = Math.min(height, element.maxHeight);
|
|
656
|
+
const actualX = x + margin.left;
|
|
657
|
+
const actualY = y + margin.top;
|
|
658
|
+
const contentX = actualX + padding.left;
|
|
659
|
+
const contentY = actualY + padding.top;
|
|
660
|
+
const contentWidth = width - padding.left - padding.right;
|
|
661
|
+
const contentHeight = height - padding.top - padding.bottom;
|
|
662
|
+
const node = {
|
|
663
|
+
element,
|
|
664
|
+
layout: {
|
|
665
|
+
x: actualX,
|
|
666
|
+
y: actualY,
|
|
667
|
+
width,
|
|
668
|
+
height,
|
|
669
|
+
contentX,
|
|
670
|
+
contentY,
|
|
671
|
+
contentWidth,
|
|
672
|
+
contentHeight
|
|
673
|
+
},
|
|
674
|
+
children: []
|
|
675
|
+
};
|
|
676
|
+
if (element.type === "text") {
|
|
677
|
+
const font = element.font ?? {};
|
|
678
|
+
if (element.wrap && contentWidth > 0) {
|
|
679
|
+
let { lines, offsets } = wrapText(ctx, element.content, contentWidth, font);
|
|
680
|
+
if (element.maxLines && lines.length > element.maxLines) {
|
|
681
|
+
lines = lines.slice(0, element.maxLines);
|
|
682
|
+
offsets = offsets.slice(0, element.maxLines);
|
|
683
|
+
if (element.ellipsis && lines.length > 0) {
|
|
684
|
+
const lastIdx = lines.length - 1;
|
|
685
|
+
const truncated = truncateText(ctx, lines[lastIdx], contentWidth, font);
|
|
686
|
+
lines[lastIdx] = truncated.text;
|
|
687
|
+
offsets[lastIdx] = truncated.offset;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
node.lines = lines;
|
|
691
|
+
node.lineOffsets = offsets;
|
|
692
|
+
} else {
|
|
693
|
+
const { text, offset } = truncateText(ctx, element.content, contentWidth > 0 && element.ellipsis ? contentWidth : Infinity, font);
|
|
694
|
+
node.lines = [text];
|
|
695
|
+
node.lineOffsets = [offset];
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
if (element.type === "richtext") {
|
|
699
|
+
const lineHeight = element.lineHeight ?? 1.2;
|
|
700
|
+
let lines = wrapRichText(ctx, element.spans, contentWidth, lineHeight);
|
|
701
|
+
if (element.maxLines && lines.length > element.maxLines) {
|
|
702
|
+
lines = lines.slice(0, element.maxLines);
|
|
703
|
+
if (element.ellipsis && lines.length > 0) {
|
|
704
|
+
const lastLine = lines[lines.length - 1];
|
|
705
|
+
if (lastLine.segments.length > 0) {
|
|
706
|
+
const lastSeg = lastLine.segments[lastLine.segments.length - 1];
|
|
707
|
+
lastSeg.text += "...";
|
|
708
|
+
lastSeg.width = ctx.measureText(lastSeg.text, lastSeg.font ?? {}).width;
|
|
709
|
+
lastLine.width = lastLine.segments.reduce((sum, s) => sum + s.width, 0);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
node.richLines = lines;
|
|
714
|
+
}
|
|
715
|
+
if (element.type === "box" || element.type === "stack") {
|
|
716
|
+
const children = element.children ?? [];
|
|
717
|
+
if (element.type === "stack") {
|
|
718
|
+
const stackAlign = element.align ?? "start";
|
|
719
|
+
const stackJustify = element.justify ?? "start";
|
|
720
|
+
for (const child of children) {
|
|
721
|
+
const childNode = computeLayout(child, ctx, {
|
|
722
|
+
minWidth: 0,
|
|
723
|
+
maxWidth: contentWidth,
|
|
724
|
+
minHeight: 0,
|
|
725
|
+
maxHeight: contentHeight
|
|
726
|
+
}, contentX, contentY);
|
|
727
|
+
const childMargin = normalizeSpacing(child.margin);
|
|
728
|
+
const childOuterWidth = childNode.layout.width + childMargin.left + childMargin.right;
|
|
729
|
+
const childOuterHeight = childNode.layout.height + childMargin.top + childMargin.bottom;
|
|
730
|
+
let offsetX = 0;
|
|
731
|
+
if (stackAlign === "center") offsetX = (contentWidth - childOuterWidth) / 2;
|
|
732
|
+
else if (stackAlign === "end") offsetX = contentWidth - childOuterWidth;
|
|
733
|
+
let offsetY = 0;
|
|
734
|
+
if (stackJustify === "center") offsetY = (contentHeight - childOuterHeight) / 2;
|
|
735
|
+
else if (stackJustify === "end") offsetY = contentHeight - childOuterHeight;
|
|
736
|
+
if (offsetX !== 0 || offsetY !== 0) applyOffset(childNode, offsetX, offsetY);
|
|
737
|
+
node.children.push(childNode);
|
|
738
|
+
}
|
|
739
|
+
} else {
|
|
740
|
+
const direction = element.direction ?? "row";
|
|
741
|
+
const justify = element.justify ?? "start";
|
|
742
|
+
const align = element.align ?? "stretch";
|
|
743
|
+
const gap = element.gap ?? 0;
|
|
744
|
+
const wrap = element.wrap ?? false;
|
|
745
|
+
const isRow = direction === "row" || direction === "row-reverse";
|
|
746
|
+
const isReverse = direction === "row-reverse" || direction === "column-reverse";
|
|
747
|
+
const getContentMainSize = () => isRow ? contentWidth : contentHeight;
|
|
748
|
+
const getContentCrossSize = () => isRow ? contentHeight : contentWidth;
|
|
749
|
+
const childInfos = [];
|
|
750
|
+
for (const child of children) {
|
|
751
|
+
const childMargin = normalizeSpacing(child.margin);
|
|
752
|
+
const childFlex = child.flex ?? 0;
|
|
753
|
+
if (childFlex > 0) childInfos.push({
|
|
754
|
+
element: child,
|
|
755
|
+
width: 0,
|
|
756
|
+
height: 0,
|
|
757
|
+
flex: childFlex,
|
|
758
|
+
margin: childMargin
|
|
759
|
+
});
|
|
760
|
+
else {
|
|
761
|
+
const size = measureIntrinsicSize(child, ctx, contentWidth - childMargin.left - childMargin.right);
|
|
762
|
+
const shouldStretchWidth = !isRow && child.width === void 0 && align === "stretch";
|
|
763
|
+
const shouldStretchHeight = isRow && child.height === void 0 && align === "stretch";
|
|
764
|
+
let w = sizeNeedsParent(child.width) ? resolveSize(child.width, contentWidth - childMargin.left - childMargin.right, size.width) : resolveSize(child.width, 0, size.width);
|
|
765
|
+
let h = sizeNeedsParent(child.height) ? resolveSize(child.height, contentHeight - childMargin.top - childMargin.bottom, size.height) : resolveSize(child.height, 0, size.height);
|
|
766
|
+
if (shouldStretchWidth && !wrap) w = contentWidth - childMargin.left - childMargin.right;
|
|
767
|
+
if (shouldStretchHeight && !wrap) h = contentHeight - childMargin.top - childMargin.bottom;
|
|
768
|
+
childInfos.push({
|
|
769
|
+
element: child,
|
|
770
|
+
width: w,
|
|
771
|
+
height: h,
|
|
772
|
+
flex: 0,
|
|
773
|
+
margin: childMargin
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
const lines = [];
|
|
778
|
+
if (wrap) {
|
|
779
|
+
let currentLine = [];
|
|
780
|
+
let currentLineSize = 0;
|
|
781
|
+
const mainAxisSize = getContentMainSize();
|
|
782
|
+
for (const info of childInfos) {
|
|
783
|
+
const itemSize = isRow ? info.width + info.margin.left + info.margin.right : info.height + info.margin.top + info.margin.bottom;
|
|
784
|
+
if (currentLine.length > 0 && currentLineSize + gap + itemSize > mainAxisSize) {
|
|
785
|
+
lines.push(currentLine);
|
|
786
|
+
currentLine = [info];
|
|
787
|
+
currentLineSize = itemSize;
|
|
788
|
+
} else {
|
|
789
|
+
currentLine.push(info);
|
|
790
|
+
currentLineSize += (currentLine.length > 1 ? gap : 0) + itemSize;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
if (currentLine.length > 0) lines.push(currentLine);
|
|
794
|
+
} else lines.push(childInfos);
|
|
795
|
+
for (const lineInfos of lines) {
|
|
796
|
+
let totalFixed = 0;
|
|
797
|
+
let totalFlex = 0;
|
|
798
|
+
const totalGap = lineInfos.length > 1 ? gap * (lineInfos.length - 1) : 0;
|
|
799
|
+
for (const info of lineInfos) if (info.flex > 0) totalFlex += info.flex;
|
|
800
|
+
else if (isRow) totalFixed += info.width + info.margin.left + info.margin.right;
|
|
801
|
+
else totalFixed += info.height + info.margin.top + info.margin.bottom;
|
|
802
|
+
const mainAxisSize = getContentMainSize();
|
|
803
|
+
const availableForFlex = Math.max(0, mainAxisSize - totalFixed - totalGap);
|
|
804
|
+
for (const info of lineInfos) if (info.flex > 0) {
|
|
805
|
+
const flexSize = totalFlex > 0 ? availableForFlex * info.flex / totalFlex : 0;
|
|
806
|
+
if (isRow) {
|
|
807
|
+
info.width = flexSize;
|
|
808
|
+
const size = measureIntrinsicSize(info.element, ctx, flexSize);
|
|
809
|
+
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);
|
|
810
|
+
} else {
|
|
811
|
+
info.height = flexSize;
|
|
812
|
+
const size = measureIntrinsicSize(info.element, ctx, contentWidth - info.margin.left - info.margin.right);
|
|
813
|
+
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);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
let crossOffset = 0;
|
|
818
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
|
|
819
|
+
const lineInfos = lines[lineIndex];
|
|
820
|
+
const totalGap = lineInfos.length > 1 ? gap * (lineInfos.length - 1) : 0;
|
|
821
|
+
const totalSize = lineInfos.reduce((sum, info) => {
|
|
822
|
+
return sum + (isRow ? info.width + info.margin.left + info.margin.right : info.height + info.margin.top + info.margin.bottom);
|
|
823
|
+
}, 0) + totalGap;
|
|
824
|
+
const freeSpace = getContentMainSize() - totalSize;
|
|
825
|
+
let mainStart = 0;
|
|
826
|
+
let mainGap = gap;
|
|
827
|
+
switch (justify) {
|
|
828
|
+
case "start":
|
|
829
|
+
mainStart = 0;
|
|
830
|
+
break;
|
|
831
|
+
case "end":
|
|
832
|
+
mainStart = freeSpace;
|
|
833
|
+
break;
|
|
834
|
+
case "center":
|
|
835
|
+
mainStart = freeSpace / 2;
|
|
836
|
+
break;
|
|
837
|
+
case "space-between":
|
|
838
|
+
mainStart = 0;
|
|
839
|
+
if (lineInfos.length > 1) mainGap = gap + freeSpace / (lineInfos.length - 1);
|
|
840
|
+
break;
|
|
841
|
+
case "space-around":
|
|
842
|
+
if (lineInfos.length > 0) {
|
|
843
|
+
const spacing = freeSpace / lineInfos.length;
|
|
844
|
+
mainStart = spacing / 2;
|
|
845
|
+
mainGap = gap + spacing;
|
|
846
|
+
}
|
|
847
|
+
break;
|
|
848
|
+
case "space-evenly":
|
|
849
|
+
if (lineInfos.length > 0) {
|
|
850
|
+
const spacing = freeSpace / (lineInfos.length + 1);
|
|
851
|
+
mainStart = spacing;
|
|
852
|
+
mainGap = gap + spacing;
|
|
853
|
+
}
|
|
854
|
+
break;
|
|
855
|
+
}
|
|
856
|
+
const lineCrossSize = lineInfos.reduce((max, info) => {
|
|
857
|
+
const itemCrossSize = isRow ? info.height + info.margin.top + info.margin.bottom : info.width + info.margin.left + info.margin.right;
|
|
858
|
+
return Math.max(max, itemCrossSize);
|
|
859
|
+
}, 0);
|
|
860
|
+
let mainOffset = mainStart;
|
|
861
|
+
const orderedInfos = isReverse ? [...lineInfos].reverse() : lineInfos;
|
|
862
|
+
for (let i = 0; i < orderedInfos.length; i++) {
|
|
863
|
+
const info = orderedInfos[i];
|
|
864
|
+
const crossAxisSize = wrap ? lineCrossSize : getContentCrossSize();
|
|
865
|
+
const childCrossSize = isRow ? info.height + info.margin.top + info.margin.bottom : info.width + info.margin.left + info.margin.right;
|
|
866
|
+
let itemCrossOffset = 0;
|
|
867
|
+
const effectiveAlign = info.element.alignSelf ?? align;
|
|
868
|
+
if (effectiveAlign === "start") itemCrossOffset = 0;
|
|
869
|
+
else if (effectiveAlign === "end") itemCrossOffset = crossAxisSize - childCrossSize;
|
|
870
|
+
else if (effectiveAlign === "center") itemCrossOffset = (crossAxisSize - childCrossSize) / 2;
|
|
871
|
+
else if (effectiveAlign === "stretch") {
|
|
872
|
+
itemCrossOffset = 0;
|
|
873
|
+
if (isRow && info.element.height === void 0) info.height = crossAxisSize - info.margin.top - info.margin.bottom;
|
|
874
|
+
else if (!isRow && info.element.width === void 0) info.width = crossAxisSize - info.margin.left - info.margin.right;
|
|
875
|
+
}
|
|
876
|
+
const childX = isRow ? contentX + mainOffset + info.margin.left : contentX + crossOffset + itemCrossOffset + info.margin.left;
|
|
877
|
+
const childY = isRow ? contentY + crossOffset + itemCrossOffset + info.margin.top : contentY + mainOffset + info.margin.top;
|
|
878
|
+
let minWidth = 0;
|
|
879
|
+
let maxWidth = info.width;
|
|
880
|
+
let minHeight = 0;
|
|
881
|
+
let maxHeight = info.height;
|
|
882
|
+
let shouldStretchCross = false;
|
|
883
|
+
if (info.flex > 0) if (isRow) {
|
|
884
|
+
minWidth = maxWidth = info.width;
|
|
885
|
+
if (info.element.height === void 0 && align === "stretch") {
|
|
886
|
+
minHeight = info.height;
|
|
887
|
+
maxHeight = element.height !== void 0 ? info.height : Infinity;
|
|
888
|
+
shouldStretchCross = true;
|
|
889
|
+
}
|
|
890
|
+
} else {
|
|
891
|
+
minHeight = maxHeight = info.height;
|
|
892
|
+
if (info.element.width === void 0 && align === "stretch") {
|
|
893
|
+
minWidth = info.width;
|
|
894
|
+
maxWidth = element.width !== void 0 ? info.width : Infinity;
|
|
895
|
+
shouldStretchCross = true;
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
else {
|
|
899
|
+
if (!isRow && info.element.width === void 0 && align === "stretch") minWidth = maxWidth = crossAxisSize - info.margin.left - info.margin.right;
|
|
900
|
+
if (isRow && info.element.height === void 0 && align === "stretch") minHeight = maxHeight = crossAxisSize - info.margin.top - info.margin.bottom;
|
|
901
|
+
}
|
|
902
|
+
const childNode = computeLayout(info.element, ctx, {
|
|
903
|
+
minWidth,
|
|
904
|
+
maxWidth,
|
|
905
|
+
minHeight,
|
|
906
|
+
maxHeight
|
|
907
|
+
}, childX - info.margin.left, childY - info.margin.top);
|
|
908
|
+
if (shouldStretchCross && info.flex > 0) {
|
|
909
|
+
const childPadding = normalizeSpacing("padding" in info.element ? info.element.padding : void 0);
|
|
910
|
+
if (isRow && childNode.layout.height < info.height) {
|
|
911
|
+
childNode.layout.height = info.height;
|
|
912
|
+
childNode.layout.contentHeight = info.height - childPadding.top - childPadding.bottom;
|
|
913
|
+
} else if (!isRow && childNode.layout.width < info.width) {
|
|
914
|
+
childNode.layout.width = info.width;
|
|
915
|
+
childNode.layout.contentWidth = info.width - childPadding.left - childPadding.right;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
node.children.push(childNode);
|
|
919
|
+
mainOffset += isRow ? info.width + info.margin.left + info.margin.right : info.height + info.margin.top + info.margin.bottom;
|
|
920
|
+
if (i < orderedInfos.length - 1) mainOffset += mainGap;
|
|
921
|
+
}
|
|
922
|
+
crossOffset += lineCrossSize;
|
|
923
|
+
if (lineIndex < lines.length - 1) crossOffset += gap;
|
|
924
|
+
}
|
|
925
|
+
if (wrap && element.height === void 0 && isRow) {
|
|
926
|
+
const actualContentHeight = crossOffset;
|
|
927
|
+
const actualHeight = actualContentHeight + padding.top + padding.bottom;
|
|
928
|
+
node.layout.height = actualHeight;
|
|
929
|
+
node.layout.contentHeight = actualContentHeight;
|
|
930
|
+
} else if (wrap && element.width === void 0 && !isRow) {
|
|
931
|
+
const actualContentWidth = crossOffset;
|
|
932
|
+
const actualWidth = actualContentWidth + padding.left + padding.right;
|
|
933
|
+
node.layout.width = actualWidth;
|
|
934
|
+
node.layout.contentWidth = actualContentWidth;
|
|
935
|
+
}
|
|
936
|
+
if (!wrap) {
|
|
937
|
+
let maxChildCrossSize = 0;
|
|
938
|
+
for (const childNode of node.children) {
|
|
939
|
+
const childMargin = normalizeSpacing(childNode.element.margin);
|
|
940
|
+
if (isRow) {
|
|
941
|
+
const childOuterHeight = childNode.layout.height + childMargin.top + childMargin.bottom;
|
|
942
|
+
maxChildCrossSize = Math.max(maxChildCrossSize, childOuterHeight);
|
|
943
|
+
} else {
|
|
944
|
+
const childOuterWidth = childNode.layout.width + childMargin.left + childMargin.right;
|
|
945
|
+
maxChildCrossSize = Math.max(maxChildCrossSize, childOuterWidth);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
if (isRow && element.height === void 0) {
|
|
949
|
+
const actualHeight = maxChildCrossSize + padding.top + padding.bottom;
|
|
950
|
+
if (actualHeight > node.layout.height) {
|
|
951
|
+
node.layout.height = actualHeight;
|
|
952
|
+
node.layout.contentHeight = maxChildCrossSize;
|
|
953
|
+
}
|
|
954
|
+
} else if (!isRow && element.width === void 0) {
|
|
955
|
+
const actualWidth = maxChildCrossSize + padding.left + padding.right;
|
|
956
|
+
if (actualWidth > node.layout.width) {
|
|
957
|
+
node.layout.width = actualWidth;
|
|
958
|
+
node.layout.contentWidth = maxChildCrossSize;
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
if (isReverse) node.children.reverse();
|
|
963
|
+
}
|
|
964
|
+
} else if (element.type === "transform") {
|
|
965
|
+
const child = element.children;
|
|
966
|
+
if (child) {
|
|
967
|
+
const childMargin = normalizeSpacing(child.margin);
|
|
968
|
+
const childNode = computeLayout(child, ctx, {
|
|
969
|
+
minWidth: 0,
|
|
970
|
+
maxWidth: contentWidth,
|
|
971
|
+
minHeight: 0,
|
|
972
|
+
maxHeight: contentHeight
|
|
973
|
+
}, contentX, contentY);
|
|
974
|
+
node.children.push(childNode);
|
|
975
|
+
if (element.width === void 0) {
|
|
976
|
+
const childOuterWidth = childNode.layout.width + childMargin.left + childMargin.right;
|
|
977
|
+
const actualWidth = childOuterWidth + padding.left + padding.right;
|
|
978
|
+
node.layout.width = actualWidth;
|
|
979
|
+
node.layout.contentWidth = childOuterWidth;
|
|
980
|
+
}
|
|
981
|
+
if (element.height === void 0) {
|
|
982
|
+
const childOuterHeight = childNode.layout.height + childMargin.top + childMargin.bottom;
|
|
983
|
+
const actualHeight = childOuterHeight + padding.top + padding.bottom;
|
|
984
|
+
node.layout.height = actualHeight;
|
|
985
|
+
node.layout.contentHeight = childOuterHeight;
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
} else if (element.type === "customdraw") {
|
|
989
|
+
const child = element.children;
|
|
990
|
+
if (child) {
|
|
991
|
+
const childMargin = normalizeSpacing(child.margin);
|
|
992
|
+
const childNode = computeLayout(child, ctx, {
|
|
993
|
+
minWidth: 0,
|
|
994
|
+
maxWidth: contentWidth,
|
|
995
|
+
minHeight: 0,
|
|
996
|
+
maxHeight: contentHeight
|
|
997
|
+
}, contentX, contentY);
|
|
998
|
+
node.children.push(childNode);
|
|
999
|
+
if (element.width === void 0) {
|
|
1000
|
+
const childOuterWidth = childNode.layout.width + childMargin.left + childMargin.right;
|
|
1001
|
+
const actualWidth = childOuterWidth + padding.left + padding.right;
|
|
1002
|
+
node.layout.width = actualWidth;
|
|
1003
|
+
node.layout.contentWidth = childOuterWidth;
|
|
1004
|
+
}
|
|
1005
|
+
if (element.height === void 0) {
|
|
1006
|
+
const childOuterHeight = childNode.layout.height + childMargin.top + childMargin.bottom;
|
|
1007
|
+
const actualHeight = childOuterHeight + padding.top + padding.bottom;
|
|
1008
|
+
node.layout.height = actualHeight;
|
|
1009
|
+
node.layout.contentHeight = childOuterHeight;
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
return node;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
//#endregion
|
|
1017
|
+
//#region src/render/utils/colors.ts
|
|
1018
|
+
function isGradientDescriptor$1(color) {
|
|
1019
|
+
return typeof color === "object" && color !== null && "type" in color && (color.type === "linear-gradient" || color.type === "radial-gradient");
|
|
1020
|
+
}
|
|
1021
|
+
function resolveGradient$1(ctx, descriptor, x, y, width, height) {
|
|
1022
|
+
if (descriptor.type === "linear-gradient") {
|
|
1023
|
+
const angleRad = (descriptor.angle - 90) * Math.PI / 180;
|
|
1024
|
+
const centerX = x + width / 2;
|
|
1025
|
+
const centerY = y + height / 2;
|
|
1026
|
+
const diagLength = Math.sqrt(width * width + height * height) / 2;
|
|
1027
|
+
const x0 = centerX - Math.cos(angleRad) * diagLength;
|
|
1028
|
+
const y0 = centerY - Math.sin(angleRad) * diagLength;
|
|
1029
|
+
const x1 = centerX + Math.cos(angleRad) * diagLength;
|
|
1030
|
+
const y1 = centerY + Math.sin(angleRad) * diagLength;
|
|
1031
|
+
const gradient = ctx.createLinearGradient(x0, y0, x1, y1);
|
|
1032
|
+
for (const stop of descriptor.stops) gradient.addColorStop(stop.offset, stop.color);
|
|
1033
|
+
return gradient;
|
|
1034
|
+
} else {
|
|
1035
|
+
const diagLength = Math.sqrt(width * width + height * height);
|
|
1036
|
+
const startX = x + (descriptor.startX ?? .5) * width;
|
|
1037
|
+
const startY = y + (descriptor.startY ?? .5) * height;
|
|
1038
|
+
const startRadius = (descriptor.startRadius ?? 0) * diagLength;
|
|
1039
|
+
const endX = x + (descriptor.endX ?? .5) * width;
|
|
1040
|
+
const endY = y + (descriptor.endY ?? .5) * height;
|
|
1041
|
+
const endRadius = (descriptor.endRadius ?? .5) * diagLength;
|
|
1042
|
+
const gradient = ctx.createRadialGradient(startX, startY, startRadius, endX, endY, endRadius);
|
|
1043
|
+
for (const stop of descriptor.stops) gradient.addColorStop(stop.offset, stop.color);
|
|
1044
|
+
return gradient;
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
function resolveColor$1(ctx, color, x, y, width, height) {
|
|
1048
|
+
if (isGradientDescriptor$1(color)) return resolveGradient$1(ctx, color, x, y, width, height);
|
|
1049
|
+
return color;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
//#endregion
|
|
1053
|
+
//#region src/render/utils/shadows.ts
|
|
1054
|
+
function applyShadow$1(ctx, shadow) {
|
|
1055
|
+
if (shadow) {
|
|
1056
|
+
ctx.shadowOffsetX = shadow.offsetX ?? 0;
|
|
1057
|
+
ctx.shadowOffsetY = shadow.offsetY ?? 0;
|
|
1058
|
+
ctx.shadowBlur = shadow.blur ?? 0;
|
|
1059
|
+
ctx.shadowColor = shadow.color ?? "rgba(0,0,0,0.5)";
|
|
1060
|
+
} else {
|
|
1061
|
+
ctx.shadowOffsetX = 0;
|
|
1062
|
+
ctx.shadowOffsetY = 0;
|
|
1063
|
+
ctx.shadowBlur = 0;
|
|
1064
|
+
ctx.shadowColor = "transparent";
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
function clearShadow$1(ctx) {
|
|
1068
|
+
ctx.shadowOffsetX = 0;
|
|
1069
|
+
ctx.shadowOffsetY = 0;
|
|
1070
|
+
ctx.shadowBlur = 0;
|
|
1071
|
+
ctx.shadowColor = "transparent";
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
//#endregion
|
|
1075
|
+
//#region src/render/utils/shapes.ts
|
|
1076
|
+
function roundRectPath(ctx, x, y, width, height, radius) {
|
|
1077
|
+
const [tl, tr, br, bl] = radius;
|
|
1078
|
+
ctx.beginPath();
|
|
1079
|
+
ctx.moveTo(x + tl, y);
|
|
1080
|
+
ctx.lineTo(x + width - tr, y);
|
|
1081
|
+
ctx.quadraticCurveTo(x + width, y, x + width, y + tr);
|
|
1082
|
+
ctx.lineTo(x + width, y + height - br);
|
|
1083
|
+
ctx.quadraticCurveTo(x + width, y + height, x + width - br, y + height);
|
|
1084
|
+
ctx.lineTo(x + bl, y + height);
|
|
1085
|
+
ctx.quadraticCurveTo(x, y + height, x, y + height - bl);
|
|
1086
|
+
ctx.lineTo(x, y + tl);
|
|
1087
|
+
ctx.quadraticCurveTo(x, y, x + tl, y);
|
|
1088
|
+
ctx.closePath();
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
//#endregion
|
|
1092
|
+
//#region src/render/components/box.ts
|
|
1093
|
+
function renderBox(ctx, node) {
|
|
1094
|
+
const element = node.element;
|
|
1095
|
+
const { x, y, width, height } = node.layout;
|
|
1096
|
+
const border = element.border;
|
|
1097
|
+
const radius = normalizeBorderRadius(border?.radius);
|
|
1098
|
+
const hasRadius = radius.some((r) => r > 0);
|
|
1099
|
+
if (element.opacity !== void 0 && element.opacity < 1) ctx.globalAlpha = element.opacity;
|
|
1100
|
+
if (element.shadow && element.background) applyShadow$1(ctx, element.shadow);
|
|
1101
|
+
if (element.background) {
|
|
1102
|
+
ctx.fillStyle = resolveColor$1(ctx, element.background, x, y, width, height);
|
|
1103
|
+
if (hasRadius) {
|
|
1104
|
+
roundRectPath(ctx, x, y, width, height, radius);
|
|
1105
|
+
ctx.fill();
|
|
1106
|
+
} else ctx.fillRect(x, y, width, height);
|
|
1107
|
+
clearShadow$1(ctx);
|
|
1108
|
+
}
|
|
1109
|
+
if (border && border.width && border.width > 0) {
|
|
1110
|
+
ctx.strokeStyle = border.color ? resolveColor$1(ctx, border.color, x, y, width, height) : "#000";
|
|
1111
|
+
ctx.lineWidth = border.width;
|
|
1112
|
+
if (hasRadius) {
|
|
1113
|
+
roundRectPath(ctx, x, y, width, height, radius);
|
|
1114
|
+
ctx.stroke();
|
|
1115
|
+
} else ctx.strokeRect(x, y, width, height);
|
|
1116
|
+
}
|
|
1117
|
+
if (element.opacity !== void 0 && element.opacity < 1) ctx.globalAlpha = 1;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
//#endregion
|
|
1121
|
+
//#region src/render/components/ProxiedCanvasContext.ts
|
|
1122
|
+
/**
|
|
1123
|
+
* ProxiedCanvasContext - Canvas 上下文代理类
|
|
1124
|
+
*
|
|
1125
|
+
* 该类提供对真实 CanvasRenderingContext2D 的代理,有以下功能:
|
|
1126
|
+
* 1. 管理 save/restore 的平衡(计数器)
|
|
1127
|
+
* 2. 追踪相对变换而不是绝对变换
|
|
1128
|
+
* 3. 在析构时自动恢复所有未恢复的状态
|
|
1129
|
+
* 4. 转发所有其他 Canvas API 调用
|
|
1130
|
+
*/
|
|
1131
|
+
var ProxiedCanvasContext = class {
|
|
1132
|
+
/**
|
|
1133
|
+
* 真实的 Canvas 上下文
|
|
1134
|
+
*/
|
|
1135
|
+
ctx;
|
|
1136
|
+
/**
|
|
1137
|
+
* 基础变换矩阵(初始化时设置,保持不变)
|
|
1138
|
+
*/
|
|
1139
|
+
baseTransform;
|
|
1140
|
+
/**
|
|
1141
|
+
* 相对变换矩阵(用户通过 setTransform 设置)
|
|
1142
|
+
*/
|
|
1143
|
+
relativeTransform;
|
|
1144
|
+
/**
|
|
1145
|
+
* save/restore 计数器
|
|
1146
|
+
*/
|
|
1147
|
+
saveCount = 0;
|
|
1148
|
+
/**
|
|
1149
|
+
* 构造函数
|
|
1150
|
+
* @param ctx 真实的 CanvasRenderingContext2D
|
|
1151
|
+
* @param baseTransform 初始的基础变换矩阵
|
|
1152
|
+
*/
|
|
1153
|
+
constructor(ctx, baseTransform) {
|
|
1154
|
+
this.ctx = ctx;
|
|
1155
|
+
this.baseTransform = baseTransform;
|
|
1156
|
+
this.relativeTransform = new DOMMatrix();
|
|
1157
|
+
}
|
|
1158
|
+
/**
|
|
1159
|
+
* save() - 保存当前状态并增加计数
|
|
1160
|
+
*/
|
|
1161
|
+
save() {
|
|
1162
|
+
this.saveCount++;
|
|
1163
|
+
this.ctx.save();
|
|
1164
|
+
}
|
|
1165
|
+
/**
|
|
1166
|
+
* restore() - 恢复上一个状态并减少计数
|
|
1167
|
+
*/
|
|
1168
|
+
restore() {
|
|
1169
|
+
if (this.saveCount > 0) {
|
|
1170
|
+
this.saveCount--;
|
|
1171
|
+
this.ctx.restore();
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
/**
|
|
1175
|
+
* setTransform() - 设置相对变换
|
|
1176
|
+
*/
|
|
1177
|
+
setTransform(...args) {
|
|
1178
|
+
let matrix;
|
|
1179
|
+
if (args.length === 1 && args[0] instanceof DOMMatrix) matrix = args[0];
|
|
1180
|
+
else if (args.length === 6) matrix = new DOMMatrix([
|
|
1181
|
+
args[0],
|
|
1182
|
+
args[1],
|
|
1183
|
+
args[2],
|
|
1184
|
+
args[3],
|
|
1185
|
+
args[4],
|
|
1186
|
+
args[5]
|
|
1187
|
+
]);
|
|
1188
|
+
else return;
|
|
1189
|
+
this.relativeTransform = matrix;
|
|
1190
|
+
const actualTransform = this.baseTransform.multiply(matrix);
|
|
1191
|
+
this.ctx.setTransform(actualTransform);
|
|
1192
|
+
}
|
|
1193
|
+
/**
|
|
1194
|
+
* getTransform() - 返回相对变换(而不是绝对变换)
|
|
1195
|
+
*/
|
|
1196
|
+
getTransform() {
|
|
1197
|
+
return this.relativeTransform;
|
|
1198
|
+
}
|
|
1199
|
+
/**
|
|
1200
|
+
* 析构函数级的清理 - 自动恢复所有未恢复的 save
|
|
1201
|
+
*/
|
|
1202
|
+
destroy() {
|
|
1203
|
+
while (this.saveCount > 0) {
|
|
1204
|
+
console.log("destroy restore", this.saveCount);
|
|
1205
|
+
this.saveCount--;
|
|
1206
|
+
this.ctx.restore();
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
};
|
|
1210
|
+
function createProxiedCanvasContext(ctx, baseTransform) {
|
|
1211
|
+
const proxy = new ProxiedCanvasContext(ctx, baseTransform);
|
|
1212
|
+
return new Proxy(proxy, {
|
|
1213
|
+
get(target, prop, receiver) {
|
|
1214
|
+
if (prop === "save" || prop === "restore" || prop === "setTransform" || prop === "getTransform" || prop === "destroy") return Reflect.get(target, prop, receiver).bind(proxy);
|
|
1215
|
+
const ownValue = Reflect.get(target, prop, receiver);
|
|
1216
|
+
if (ownValue !== void 0) return ownValue;
|
|
1217
|
+
const contextValue = target.ctx[prop];
|
|
1218
|
+
if (typeof contextValue === "function") return contextValue.bind(target.ctx);
|
|
1219
|
+
return contextValue;
|
|
1220
|
+
},
|
|
1221
|
+
set(target, prop, value, _receiver) {
|
|
1222
|
+
target.ctx[prop] = value;
|
|
1223
|
+
return true;
|
|
1224
|
+
},
|
|
1225
|
+
has(target, prop) {
|
|
1226
|
+
if (prop === "save" || prop === "restore" || prop === "setTransform" || prop === "getTransform" || prop === "destroy") return true;
|
|
1227
|
+
return prop in target.ctx;
|
|
1228
|
+
},
|
|
1229
|
+
ownKeys(target) {
|
|
1230
|
+
return Reflect.ownKeys(target.ctx);
|
|
1231
|
+
},
|
|
1232
|
+
getOwnPropertyDescriptor(target, prop) {
|
|
1233
|
+
return Reflect.getOwnPropertyDescriptor(target.ctx, prop);
|
|
1234
|
+
}
|
|
1235
|
+
});
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
//#endregion
|
|
1239
|
+
//#region src/render/components/customDraw.ts
|
|
1240
|
+
/**
|
|
1241
|
+
* 渲染 CustomDraw 组件
|
|
1242
|
+
* 提供自定义绘制回调,用户可以直接访问 Canvas 上下文并绘制自定义内容
|
|
1243
|
+
*/
|
|
1244
|
+
function renderCustomDraw(ctx, node) {
|
|
1245
|
+
const element = node.element;
|
|
1246
|
+
ctx.save();
|
|
1247
|
+
ctx.translate(node.layout.x, node.layout.y);
|
|
1248
|
+
const proxyCtx = createProxiedCanvasContext(ctx, ctx.getTransform());
|
|
1249
|
+
const inner = () => {
|
|
1250
|
+
if (node.children && node.children.length > 0) {
|
|
1251
|
+
ctx.save();
|
|
1252
|
+
ctx.translate(-node.layout.x, -node.layout.y);
|
|
1253
|
+
renderNode(ctx, node.children[0]);
|
|
1254
|
+
ctx.restore();
|
|
1255
|
+
}
|
|
1256
|
+
};
|
|
1257
|
+
element.draw(proxyCtx, {
|
|
1258
|
+
inner,
|
|
1259
|
+
width: node.layout.contentWidth,
|
|
1260
|
+
height: node.layout.contentHeight
|
|
1261
|
+
});
|
|
1262
|
+
proxyCtx.destroy();
|
|
1263
|
+
ctx.restore();
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
//#endregion
|
|
1267
|
+
//#region src/render/components/image.ts
|
|
1268
|
+
function renderImage(ctx, node) {
|
|
1269
|
+
const element = node.element;
|
|
1270
|
+
const { x, y, width, height } = node.layout;
|
|
1271
|
+
const src = element.src;
|
|
1272
|
+
if (!src) return;
|
|
1273
|
+
if (element.opacity !== void 0 && element.opacity < 1) ctx.globalAlpha = element.opacity;
|
|
1274
|
+
if (element.shadow) applyShadow$1(ctx, element.shadow);
|
|
1275
|
+
const border = element.border;
|
|
1276
|
+
const radius = normalizeBorderRadius(border?.radius);
|
|
1277
|
+
const hasRadius = radius.some((r) => r > 0);
|
|
1278
|
+
if (hasRadius) {
|
|
1279
|
+
ctx.save();
|
|
1280
|
+
roundRectPath(ctx, x, y, width, height, radius);
|
|
1281
|
+
ctx.clip();
|
|
1282
|
+
}
|
|
1283
|
+
const imgWidth = "naturalWidth" in src ? src.naturalWidth : "width" in src ? +src.width : 0;
|
|
1284
|
+
const imgHeight = "naturalHeight" in src ? src.naturalHeight : "height" in src ? +src.height : 0;
|
|
1285
|
+
const fit = element.fit ?? "fill";
|
|
1286
|
+
let drawX = x;
|
|
1287
|
+
let drawY = y;
|
|
1288
|
+
let drawWidth = width;
|
|
1289
|
+
let drawHeight = height;
|
|
1290
|
+
if (fit !== "fill" && imgWidth > 0 && imgHeight > 0) {
|
|
1291
|
+
const imgAspect = imgWidth / imgHeight;
|
|
1292
|
+
const boxAspect = width / height;
|
|
1293
|
+
let scale = 1;
|
|
1294
|
+
switch (fit) {
|
|
1295
|
+
case "contain":
|
|
1296
|
+
scale = imgAspect > boxAspect ? width / imgWidth : height / imgHeight;
|
|
1297
|
+
break;
|
|
1298
|
+
case "cover":
|
|
1299
|
+
scale = imgAspect > boxAspect ? height / imgHeight : width / imgWidth;
|
|
1300
|
+
break;
|
|
1301
|
+
case "scale-down":
|
|
1302
|
+
scale = Math.min(1, imgAspect > boxAspect ? width / imgWidth : height / imgHeight);
|
|
1303
|
+
break;
|
|
1304
|
+
case "none":
|
|
1305
|
+
scale = 1;
|
|
1306
|
+
break;
|
|
1307
|
+
}
|
|
1308
|
+
drawWidth = imgWidth * scale;
|
|
1309
|
+
drawHeight = imgHeight * scale;
|
|
1310
|
+
const position = element.position ?? {};
|
|
1311
|
+
const posX = position.x ?? "center";
|
|
1312
|
+
const posY = position.y ?? "center";
|
|
1313
|
+
if (typeof posX === "number") drawX = x + posX;
|
|
1314
|
+
else switch (posX) {
|
|
1315
|
+
case "left":
|
|
1316
|
+
drawX = x;
|
|
1317
|
+
break;
|
|
1318
|
+
case "center":
|
|
1319
|
+
drawX = x + (width - drawWidth) / 2;
|
|
1320
|
+
break;
|
|
1321
|
+
case "right":
|
|
1322
|
+
drawX = x + width - drawWidth;
|
|
1323
|
+
break;
|
|
1324
|
+
}
|
|
1325
|
+
if (typeof posY === "number") drawY = y + posY;
|
|
1326
|
+
else switch (posY) {
|
|
1327
|
+
case "top":
|
|
1328
|
+
drawY = y;
|
|
1329
|
+
break;
|
|
1330
|
+
case "center":
|
|
1331
|
+
drawY = y + (height - drawHeight) / 2;
|
|
1332
|
+
break;
|
|
1333
|
+
case "bottom":
|
|
1334
|
+
drawY = y + height - drawHeight;
|
|
1335
|
+
break;
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
ctx.drawImage(src, drawX, drawY, drawWidth, drawHeight);
|
|
1339
|
+
if (element.shadow) clearShadow$1(ctx);
|
|
1340
|
+
if (hasRadius) ctx.restore();
|
|
1341
|
+
if (border && border.width && border.width > 0) {
|
|
1342
|
+
ctx.strokeStyle = border.color ? resolveColor$1(ctx, border.color, x, y, width, height) : "#000";
|
|
1343
|
+
ctx.lineWidth = border.width;
|
|
1344
|
+
if (hasRadius) {
|
|
1345
|
+
roundRectPath(ctx, x, y, width, height, radius);
|
|
1346
|
+
ctx.stroke();
|
|
1347
|
+
} else ctx.strokeRect(x, y, width, height);
|
|
1348
|
+
}
|
|
1349
|
+
if (element.opacity !== void 0 && element.opacity < 1) ctx.globalAlpha = 1;
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
//#endregion
|
|
1353
|
+
//#region src/render/components/richtext.ts
|
|
1354
|
+
function renderRichText(ctx, node) {
|
|
1355
|
+
const element = node.element;
|
|
1356
|
+
const { contentX, contentY, contentWidth, contentHeight } = node.layout;
|
|
1357
|
+
const lines = node.richLines ?? [];
|
|
1358
|
+
if (lines.length === 0) return;
|
|
1359
|
+
const totalTextHeight = lines.reduce((sum, line) => sum + line.height, 0);
|
|
1360
|
+
let verticalOffset = 0;
|
|
1361
|
+
if (element.verticalAlign === "middle") verticalOffset = (contentHeight - totalTextHeight) / 2;
|
|
1362
|
+
else if (element.verticalAlign === "bottom") verticalOffset = contentHeight - totalTextHeight;
|
|
1363
|
+
let currentY = contentY + verticalOffset;
|
|
1364
|
+
for (const line of lines) {
|
|
1365
|
+
let lineX = contentX;
|
|
1366
|
+
if (element.align === "center") lineX = contentX + (contentWidth - line.width) / 2;
|
|
1367
|
+
else if (element.align === "right") lineX = contentX + (contentWidth - line.width);
|
|
1368
|
+
const baselineY = currentY + line.baseline;
|
|
1369
|
+
for (const seg of line.segments) {
|
|
1370
|
+
ctx.save();
|
|
1371
|
+
ctx.font = buildFontString(seg.font ?? {});
|
|
1372
|
+
if (seg.background) {
|
|
1373
|
+
ctx.fillStyle = resolveColor$1(ctx, seg.background, lineX, currentY, seg.width, line.height);
|
|
1374
|
+
ctx.fillRect(lineX, currentY, seg.width, line.height);
|
|
1375
|
+
}
|
|
1376
|
+
ctx.fillStyle = seg.color ? resolveColor$1(ctx, seg.color, lineX, currentY, seg.width, line.height) : "#000";
|
|
1377
|
+
ctx.textBaseline = "middle";
|
|
1378
|
+
ctx.fillText(seg.text, lineX, baselineY - seg.offset);
|
|
1379
|
+
if (seg.underline) {
|
|
1380
|
+
ctx.beginPath();
|
|
1381
|
+
ctx.strokeStyle = ctx.fillStyle;
|
|
1382
|
+
ctx.lineWidth = 1;
|
|
1383
|
+
ctx.moveTo(lineX, currentY + seg.height);
|
|
1384
|
+
ctx.lineTo(lineX + seg.width, currentY + seg.height);
|
|
1385
|
+
ctx.stroke();
|
|
1386
|
+
}
|
|
1387
|
+
if (seg.strikethrough) {
|
|
1388
|
+
ctx.beginPath();
|
|
1389
|
+
ctx.strokeStyle = ctx.fillStyle;
|
|
1390
|
+
ctx.lineWidth = 1;
|
|
1391
|
+
const strikeY = currentY + seg.height / 2 + seg.offset;
|
|
1392
|
+
ctx.moveTo(lineX, strikeY);
|
|
1393
|
+
ctx.lineTo(lineX + seg.width, strikeY);
|
|
1394
|
+
ctx.stroke();
|
|
1395
|
+
}
|
|
1396
|
+
ctx.restore();
|
|
1397
|
+
lineX += seg.width;
|
|
1398
|
+
}
|
|
1399
|
+
currentY += line.height;
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
//#endregion
|
|
1404
|
+
//#region src/render/components/svg.ts
|
|
1405
|
+
function isGradientDescriptor(color) {
|
|
1406
|
+
return typeof color === "object" && color !== null && "type" in color && (color.type === "linear-gradient" || color.type === "radial-gradient");
|
|
1407
|
+
}
|
|
1408
|
+
function resolveGradient(ctx, descriptor, x, y, width, height) {
|
|
1409
|
+
if (descriptor.type === "linear-gradient") {
|
|
1410
|
+
const angleRad = (descriptor.angle - 90) * Math.PI / 180;
|
|
1411
|
+
const centerX = x + width / 2;
|
|
1412
|
+
const centerY = y + height / 2;
|
|
1413
|
+
const diagLength = Math.sqrt(width * width + height * height) / 2;
|
|
1414
|
+
const x0 = centerX - Math.cos(angleRad) * diagLength;
|
|
1415
|
+
const y0 = centerY - Math.sin(angleRad) * diagLength;
|
|
1416
|
+
const x1 = centerX + Math.cos(angleRad) * diagLength;
|
|
1417
|
+
const y1 = centerY + Math.sin(angleRad) * diagLength;
|
|
1418
|
+
const gradient = ctx.createLinearGradient(x0, y0, x1, y1);
|
|
1419
|
+
for (const stop of descriptor.stops) gradient.addColorStop(stop.offset, stop.color);
|
|
1420
|
+
return gradient;
|
|
1421
|
+
} else {
|
|
1422
|
+
const diagLength = Math.sqrt(width * width + height * height);
|
|
1423
|
+
const startX = x + (descriptor.startX ?? .5) * width;
|
|
1424
|
+
const startY = y + (descriptor.startY ?? .5) * height;
|
|
1425
|
+
const startRadius = (descriptor.startRadius ?? 0) * diagLength;
|
|
1426
|
+
const endX = x + (descriptor.endX ?? .5) * width;
|
|
1427
|
+
const endY = y + (descriptor.endY ?? .5) * height;
|
|
1428
|
+
const endRadius = (descriptor.endRadius ?? .5) * diagLength;
|
|
1429
|
+
const gradient = ctx.createRadialGradient(startX, startY, startRadius, endX, endY, endRadius);
|
|
1430
|
+
for (const stop of descriptor.stops) gradient.addColorStop(stop.offset, stop.color);
|
|
1431
|
+
return gradient;
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
function resolveColor(ctx, color, x, y, width, height) {
|
|
1435
|
+
if (isGradientDescriptor(color)) return resolveGradient(ctx, color, x, y, width, height);
|
|
1436
|
+
return color;
|
|
1437
|
+
}
|
|
1438
|
+
function applyTransform(base, transform) {
|
|
1439
|
+
if (!transform) return base;
|
|
1440
|
+
let result = new DOMMatrix([
|
|
1441
|
+
base.a,
|
|
1442
|
+
base.b,
|
|
1443
|
+
base.c,
|
|
1444
|
+
base.d,
|
|
1445
|
+
base.e,
|
|
1446
|
+
base.f
|
|
1447
|
+
]);
|
|
1448
|
+
if (transform.matrix) {
|
|
1449
|
+
const [a, b, c, d, e, f] = transform.matrix;
|
|
1450
|
+
result = result.multiply(new DOMMatrix([
|
|
1451
|
+
a,
|
|
1452
|
+
b,
|
|
1453
|
+
c,
|
|
1454
|
+
d,
|
|
1455
|
+
e,
|
|
1456
|
+
f
|
|
1457
|
+
]));
|
|
1458
|
+
}
|
|
1459
|
+
if (transform.translate) result = result.translate(transform.translate[0], transform.translate[1]);
|
|
1460
|
+
if (transform.rotate !== void 0) if (typeof transform.rotate === "number") result = result.rotate(transform.rotate);
|
|
1461
|
+
else {
|
|
1462
|
+
const [angle, cx, cy] = transform.rotate;
|
|
1463
|
+
result = result.translate(cx, cy).rotate(angle).translate(-cx, -cy);
|
|
1464
|
+
}
|
|
1465
|
+
if (transform.scale !== void 0) if (typeof transform.scale === "number") result = result.scale(transform.scale);
|
|
1466
|
+
else result = result.scale(transform.scale[0], transform.scale[1]);
|
|
1467
|
+
if (transform.skewX !== void 0) {
|
|
1468
|
+
const degrees = transform.skewX * 180 / Math.PI;
|
|
1469
|
+
result = result.skewX(degrees);
|
|
1470
|
+
}
|
|
1471
|
+
if (transform.skewY !== void 0) {
|
|
1472
|
+
const degrees = transform.skewY * 180 / Math.PI;
|
|
1473
|
+
result = result.skewY(degrees);
|
|
1474
|
+
}
|
|
1475
|
+
return result;
|
|
1476
|
+
}
|
|
1477
|
+
function applyStroke(ctx, stroke, bounds) {
|
|
1478
|
+
ctx.strokeStyle = resolveColor(ctx, stroke.color, bounds.x, bounds.y, bounds.width, bounds.height);
|
|
1479
|
+
ctx.lineWidth = stroke.width;
|
|
1480
|
+
ctx.setLineDash(stroke.dash ?? []);
|
|
1481
|
+
if (stroke.cap) ctx.lineCap = stroke.cap;
|
|
1482
|
+
if (stroke.join) ctx.lineJoin = stroke.join;
|
|
1483
|
+
}
|
|
1484
|
+
function applyFill(ctx, fill, bounds) {
|
|
1485
|
+
ctx.fillStyle = resolveColor(ctx, fill, bounds.x, bounds.y, bounds.width, bounds.height);
|
|
1486
|
+
ctx.fill();
|
|
1487
|
+
}
|
|
1488
|
+
function applyFillAndStroke(ctx, shape, bounds) {
|
|
1489
|
+
if (shape.fill && shape.fill !== "none") applyFill(ctx, shape.fill, bounds);
|
|
1490
|
+
if (shape.stroke) {
|
|
1491
|
+
applyStroke(ctx, shape.stroke, bounds);
|
|
1492
|
+
ctx.stroke();
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
function renderSvgRect(ctx, rect, bounds) {
|
|
1496
|
+
const { x = 0, y = 0, width, height, rx = 0, ry = 0 } = rect;
|
|
1497
|
+
ctx.beginPath();
|
|
1498
|
+
if (rx || ry) ctx.roundRect(x, y, width, height, Math.max(rx, ry));
|
|
1499
|
+
else ctx.rect(x, y, width, height);
|
|
1500
|
+
applyFillAndStroke(ctx, rect, bounds);
|
|
1501
|
+
}
|
|
1502
|
+
function renderSvgCircle(ctx, circle, bounds) {
|
|
1503
|
+
ctx.beginPath();
|
|
1504
|
+
ctx.arc(circle.cx, circle.cy, circle.r, 0, Math.PI * 2);
|
|
1505
|
+
applyFillAndStroke(ctx, circle, bounds);
|
|
1506
|
+
}
|
|
1507
|
+
function renderSvgEllipse(ctx, ellipse, bounds) {
|
|
1508
|
+
ctx.beginPath();
|
|
1509
|
+
ctx.ellipse(ellipse.cx, ellipse.cy, ellipse.rx, ellipse.ry, 0, 0, Math.PI * 2);
|
|
1510
|
+
applyFillAndStroke(ctx, ellipse, bounds);
|
|
1511
|
+
}
|
|
1512
|
+
function renderSvgLine(ctx, line, bounds) {
|
|
1513
|
+
ctx.beginPath();
|
|
1514
|
+
ctx.moveTo(line.x1, line.y1);
|
|
1515
|
+
ctx.lineTo(line.x2, line.y2);
|
|
1516
|
+
if (line.stroke) {
|
|
1517
|
+
applyStroke(ctx, line.stroke, bounds);
|
|
1518
|
+
ctx.stroke();
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
function renderSvgPolyline(ctx, polyline, bounds) {
|
|
1522
|
+
const { points } = polyline;
|
|
1523
|
+
if (points.length === 0) return;
|
|
1524
|
+
ctx.beginPath();
|
|
1525
|
+
ctx.moveTo(points[0][0], points[0][1]);
|
|
1526
|
+
for (let i = 1; i < points.length; i++) ctx.lineTo(points[i][0], points[i][1]);
|
|
1527
|
+
applyFillAndStroke(ctx, polyline, bounds);
|
|
1528
|
+
}
|
|
1529
|
+
function renderSvgPolygon(ctx, polygon, bounds) {
|
|
1530
|
+
const { points } = polygon;
|
|
1531
|
+
if (points.length === 0) return;
|
|
1532
|
+
ctx.beginPath();
|
|
1533
|
+
ctx.moveTo(points[0][0], points[0][1]);
|
|
1534
|
+
for (let i = 1; i < points.length; i++) ctx.lineTo(points[i][0], points[i][1]);
|
|
1535
|
+
ctx.closePath();
|
|
1536
|
+
applyFillAndStroke(ctx, polygon, bounds);
|
|
1537
|
+
}
|
|
1538
|
+
function renderSvgPath(ctx, path, bounds) {
|
|
1539
|
+
const path2d = new Path2D(path.d);
|
|
1540
|
+
if (path.fill && path.fill !== "none") {
|
|
1541
|
+
ctx.fillStyle = resolveColor(ctx, path.fill, bounds.x, bounds.y, bounds.width, bounds.height);
|
|
1542
|
+
ctx.fill(path2d);
|
|
1543
|
+
}
|
|
1544
|
+
if (path.stroke) {
|
|
1545
|
+
applyStroke(ctx, path.stroke, bounds);
|
|
1546
|
+
ctx.stroke(path2d);
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
const TEXT_ANCHOR_MAP = {
|
|
1550
|
+
start: "left",
|
|
1551
|
+
middle: "center",
|
|
1552
|
+
end: "right"
|
|
1553
|
+
};
|
|
1554
|
+
const BASELINE_MAP = {
|
|
1555
|
+
auto: "alphabetic",
|
|
1556
|
+
middle: "middle",
|
|
1557
|
+
hanging: "hanging"
|
|
1558
|
+
};
|
|
1559
|
+
function renderSvgText(ctx, text, bounds) {
|
|
1560
|
+
const { x = 0, y = 0, content, font, textAnchor = "start", dominantBaseline = "auto" } = text;
|
|
1561
|
+
ctx.font = buildFontString(font ?? {});
|
|
1562
|
+
ctx.textAlign = TEXT_ANCHOR_MAP[textAnchor] ?? "left";
|
|
1563
|
+
ctx.textBaseline = BASELINE_MAP[dominantBaseline] ?? "alphabetic";
|
|
1564
|
+
if (text.fill && text.fill !== "none") {
|
|
1565
|
+
ctx.fillStyle = resolveColor(ctx, text.fill, bounds.x, bounds.y, bounds.width, bounds.height);
|
|
1566
|
+
ctx.fillText(content, x, y);
|
|
1567
|
+
}
|
|
1568
|
+
if (text.stroke) {
|
|
1569
|
+
applyStroke(ctx, text.stroke, bounds);
|
|
1570
|
+
ctx.strokeText(content, x, y);
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
function renderSvgChild(ctx, child, parentTransform, bounds, baseTransform) {
|
|
1574
|
+
const localTransform = applyTransform(parentTransform, child.transform);
|
|
1575
|
+
ctx.save();
|
|
1576
|
+
ctx.setTransform(baseTransform.multiply(localTransform));
|
|
1577
|
+
if (child.opacity !== void 0) ctx.globalAlpha *= child.opacity;
|
|
1578
|
+
switch (child.type) {
|
|
1579
|
+
case "rect":
|
|
1580
|
+
renderSvgRect(ctx, child, bounds);
|
|
1581
|
+
break;
|
|
1582
|
+
case "circle":
|
|
1583
|
+
renderSvgCircle(ctx, child, bounds);
|
|
1584
|
+
break;
|
|
1585
|
+
case "ellipse":
|
|
1586
|
+
renderSvgEllipse(ctx, child, bounds);
|
|
1587
|
+
break;
|
|
1588
|
+
case "line":
|
|
1589
|
+
renderSvgLine(ctx, child, bounds);
|
|
1590
|
+
break;
|
|
1591
|
+
case "polyline":
|
|
1592
|
+
renderSvgPolyline(ctx, child, bounds);
|
|
1593
|
+
break;
|
|
1594
|
+
case "polygon":
|
|
1595
|
+
renderSvgPolygon(ctx, child, bounds);
|
|
1596
|
+
break;
|
|
1597
|
+
case "path":
|
|
1598
|
+
renderSvgPath(ctx, child, bounds);
|
|
1599
|
+
break;
|
|
1600
|
+
case "text":
|
|
1601
|
+
renderSvgText(ctx, child, bounds);
|
|
1602
|
+
break;
|
|
1603
|
+
case "g":
|
|
1604
|
+
renderSvgGroup(ctx, child, localTransform, bounds, baseTransform);
|
|
1605
|
+
break;
|
|
1606
|
+
}
|
|
1607
|
+
ctx.restore();
|
|
1608
|
+
}
|
|
1609
|
+
function renderSvgGroup(ctx, group, parentTransform, bounds, baseTransform) {
|
|
1610
|
+
for (const child of group.children) renderSvgChild(ctx, child, parentTransform, bounds, baseTransform);
|
|
1611
|
+
}
|
|
1612
|
+
function calculateViewBoxTransform(x, y, width, height, viewBox, preserveAspectRatio) {
|
|
1613
|
+
const vbX = viewBox.x ?? 0;
|
|
1614
|
+
const vbY = viewBox.y ?? 0;
|
|
1615
|
+
const vbWidth = viewBox.width;
|
|
1616
|
+
const vbHeight = viewBox.height;
|
|
1617
|
+
const scaleX = width / vbWidth;
|
|
1618
|
+
const scaleY = height / vbHeight;
|
|
1619
|
+
const align = preserveAspectRatio?.align ?? "xMidYMid";
|
|
1620
|
+
const meetOrSlice = preserveAspectRatio?.meetOrSlice ?? "meet";
|
|
1621
|
+
if (align === "none") return new DOMMatrix().translate(x, y).scale(scaleX, scaleY).translate(-vbX, -vbY);
|
|
1622
|
+
const scale = meetOrSlice === "meet" ? Math.min(scaleX, scaleY) : Math.max(scaleX, scaleY);
|
|
1623
|
+
const scaledWidth = vbWidth * scale;
|
|
1624
|
+
const scaledHeight = vbHeight * scale;
|
|
1625
|
+
let translateX = x;
|
|
1626
|
+
let translateY = y;
|
|
1627
|
+
if (align.includes("xMid")) translateX += (width - scaledWidth) / 2;
|
|
1628
|
+
else if (align.includes("xMax")) translateX += width - scaledWidth;
|
|
1629
|
+
if (align.includes("YMid")) translateY += (height - scaledHeight) / 2;
|
|
1630
|
+
else if (align.includes("YMax")) translateY += height - scaledHeight;
|
|
1631
|
+
return new DOMMatrix().translate(translateX, translateY).scale(scale, scale).translate(-vbX, -vbY);
|
|
1632
|
+
}
|
|
1633
|
+
function applyShadow(ctx, shadow) {
|
|
1634
|
+
ctx.shadowOffsetX = shadow.offsetX ?? 0;
|
|
1635
|
+
ctx.shadowOffsetY = shadow.offsetY ?? 0;
|
|
1636
|
+
ctx.shadowBlur = shadow.blur ?? 0;
|
|
1637
|
+
ctx.shadowColor = shadow.color ?? "rgba(0,0,0,0.5)";
|
|
1638
|
+
}
|
|
1639
|
+
function clearShadow(ctx) {
|
|
1640
|
+
ctx.shadowOffsetX = 0;
|
|
1641
|
+
ctx.shadowOffsetY = 0;
|
|
1642
|
+
ctx.shadowBlur = 0;
|
|
1643
|
+
ctx.shadowColor = "transparent";
|
|
1644
|
+
}
|
|
1645
|
+
function renderSvg(ctx, node) {
|
|
1646
|
+
const element = node.element;
|
|
1647
|
+
const { x, y, width, height } = node.layout;
|
|
1648
|
+
const bounds = {
|
|
1649
|
+
x,
|
|
1650
|
+
y,
|
|
1651
|
+
width,
|
|
1652
|
+
height
|
|
1653
|
+
};
|
|
1654
|
+
if (element.background) {
|
|
1655
|
+
if (element.shadow) applyShadow(ctx, element.shadow);
|
|
1656
|
+
ctx.fillStyle = resolveColor(ctx, element.background, x, y, width, height);
|
|
1657
|
+
ctx.fillRect(x, y, width, height);
|
|
1658
|
+
if (element.shadow) clearShadow(ctx);
|
|
1659
|
+
}
|
|
1660
|
+
ctx.save();
|
|
1661
|
+
ctx.beginPath();
|
|
1662
|
+
ctx.rect(x, y, width, height);
|
|
1663
|
+
ctx.clip();
|
|
1664
|
+
const baseTransform = ctx.getTransform();
|
|
1665
|
+
const transform = calculateViewBoxTransform(x, y, width, height, element.viewBox ?? {
|
|
1666
|
+
x: 0,
|
|
1667
|
+
y: 0,
|
|
1668
|
+
width,
|
|
1669
|
+
height
|
|
1670
|
+
}, element.preserveAspectRatio);
|
|
1671
|
+
for (const child of element.children) renderSvgChild(ctx, child, transform, bounds, baseTransform);
|
|
1672
|
+
ctx.restore();
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
//#endregion
|
|
1676
|
+
//#region src/render/components/text.ts
|
|
1677
|
+
function renderText(ctx, node) {
|
|
1678
|
+
const element = node.element;
|
|
1679
|
+
const { contentX, contentY, contentWidth, contentHeight } = node.layout;
|
|
1680
|
+
const lines = node.lines ?? [element.content];
|
|
1681
|
+
const font = element.font ?? {};
|
|
1682
|
+
const lineHeightPx = (font.size ?? 16) * (element.lineHeight ?? 1.2);
|
|
1683
|
+
ctx.font = buildFontString(font);
|
|
1684
|
+
ctx.fillStyle = element.color ? resolveColor$1(ctx, element.color, contentX, contentY, contentWidth, contentHeight) : "#000";
|
|
1685
|
+
let textAlign = "left";
|
|
1686
|
+
if (element.align === "center") textAlign = "center";
|
|
1687
|
+
else if (element.align === "right") textAlign = "right";
|
|
1688
|
+
ctx.textAlign = textAlign;
|
|
1689
|
+
ctx.textBaseline = "middle";
|
|
1690
|
+
const totalTextHeight = lines.length * lineHeightPx;
|
|
1691
|
+
let verticalOffset = 0;
|
|
1692
|
+
if (element.verticalAlign === "middle") verticalOffset = (contentHeight - totalTextHeight) / 2;
|
|
1693
|
+
else if (element.verticalAlign === "bottom") verticalOffset = contentHeight - totalTextHeight;
|
|
1694
|
+
let textX = contentX;
|
|
1695
|
+
if (element.align === "center") textX = contentX + contentWidth / 2;
|
|
1696
|
+
else if (element.align === "right") textX = contentX + contentWidth;
|
|
1697
|
+
if (element.shadow) applyShadow$1(ctx, element.shadow);
|
|
1698
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1699
|
+
const correctedLineY = contentY + verticalOffset + i * lineHeightPx + lineHeightPx / 2 + (node.lineOffsets?.[i] ?? 0);
|
|
1700
|
+
if (element.stroke) {
|
|
1701
|
+
ctx.strokeStyle = resolveColor$1(ctx, element.stroke.color, contentX, contentY, contentWidth, contentHeight);
|
|
1702
|
+
ctx.lineWidth = element.stroke.width;
|
|
1703
|
+
ctx.strokeText(lines[i], textX, correctedLineY);
|
|
1704
|
+
}
|
|
1705
|
+
ctx.fillText(lines[i], textX, correctedLineY);
|
|
1706
|
+
}
|
|
1707
|
+
if (element.shadow) clearShadow$1(ctx);
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
//#endregion
|
|
1711
|
+
//#region src/render/components/transform.ts
|
|
1712
|
+
/**
|
|
1713
|
+
* 解析 Transform 值为 DOMMatrix
|
|
1714
|
+
* 支持三种格式:
|
|
1715
|
+
* - 数组: [a, b, c, d, e, f]
|
|
1716
|
+
* - DOMMatrix2DInit 对象: { a, b, c, d, e, f, ... }
|
|
1717
|
+
* - 简易对象: { translate, rotate, scale, skewX, skewY }
|
|
1718
|
+
*/
|
|
1719
|
+
function parseTransformValue(transform) {
|
|
1720
|
+
if (transform === void 0) return new DOMMatrix();
|
|
1721
|
+
if (Array.isArray(transform)) return new DOMMatrix(transform);
|
|
1722
|
+
const hasDOMMatrixInit = "a" in transform || "b" in transform || "c" in transform || "d" in transform || "e" in transform || "f" in transform;
|
|
1723
|
+
const hasSimpleTransform = "translate" in transform || "rotate" in transform || "scale" in transform || "skewX" in transform || "skewY" in transform;
|
|
1724
|
+
if (hasDOMMatrixInit && !hasSimpleTransform) {
|
|
1725
|
+
const init = transform;
|
|
1726
|
+
return new DOMMatrix([
|
|
1727
|
+
init.a ?? 1,
|
|
1728
|
+
init.b ?? 0,
|
|
1729
|
+
init.c ?? 0,
|
|
1730
|
+
init.d ?? 1,
|
|
1731
|
+
init.e ?? 0,
|
|
1732
|
+
init.f ?? 0
|
|
1733
|
+
]);
|
|
1734
|
+
}
|
|
1735
|
+
const simpleObj = transform;
|
|
1736
|
+
let result = new DOMMatrix();
|
|
1737
|
+
if (simpleObj.translate) result = result.translate(simpleObj.translate[0], simpleObj.translate[1]);
|
|
1738
|
+
if (simpleObj.rotate !== void 0) if (typeof simpleObj.rotate === "number") result = result.rotate(simpleObj.rotate);
|
|
1739
|
+
else {
|
|
1740
|
+
const [angle, cx, cy] = simpleObj.rotate;
|
|
1741
|
+
result = result.translate(cx, cy).rotate(angle).translate(-cx, -cy);
|
|
1742
|
+
}
|
|
1743
|
+
if (simpleObj.scale !== void 0) if (typeof simpleObj.scale === "number") result = result.scale(simpleObj.scale);
|
|
1744
|
+
else result = result.scale(simpleObj.scale[0], simpleObj.scale[1]);
|
|
1745
|
+
if (simpleObj.skewX !== void 0) result = result.skewX(simpleObj.skewX);
|
|
1746
|
+
if (simpleObj.skewY !== void 0) result = result.skewY(simpleObj.skewY);
|
|
1747
|
+
return result;
|
|
1748
|
+
}
|
|
1749
|
+
/**
|
|
1750
|
+
* 根据 transformOrigin 属性和子元素尺寸计算实际变换原点坐标
|
|
1751
|
+
*/
|
|
1752
|
+
function resolveTransformOrigin(origin, childLayout) {
|
|
1753
|
+
if (origin === void 0) return [0, 0];
|
|
1754
|
+
const [xVal, yVal] = origin;
|
|
1755
|
+
const sizes = [childLayout.width, childLayout.height];
|
|
1756
|
+
const values = [xVal, yVal];
|
|
1757
|
+
const result = [0, 0];
|
|
1758
|
+
for (let i = 0; i < 2; i++) {
|
|
1759
|
+
const val = values[i];
|
|
1760
|
+
if (typeof val === "string") if (val.endsWith("%")) result[i] = parseFloat(val) / 100 * sizes[i];
|
|
1761
|
+
else result[i] = parseFloat(val);
|
|
1762
|
+
else result[i] = val;
|
|
1763
|
+
}
|
|
1764
|
+
return result;
|
|
1765
|
+
}
|
|
1766
|
+
/**
|
|
1767
|
+
* 渲染 Transform 组件及其子元素
|
|
1768
|
+
*/
|
|
1769
|
+
function renderTransform(ctx, node) {
|
|
1770
|
+
const element = node.element;
|
|
1771
|
+
const { children } = node;
|
|
1772
|
+
if (!children || children.length === 0) return;
|
|
1773
|
+
const childNode = children[0];
|
|
1774
|
+
const [relativeOx, relativeOy] = resolveTransformOrigin(element.transformOrigin, childNode.layout);
|
|
1775
|
+
const ox = childNode.layout.x + relativeOx;
|
|
1776
|
+
const oy = childNode.layout.y + relativeOy;
|
|
1777
|
+
const targetMatrix = parseTransformValue(element.transform);
|
|
1778
|
+
const finalMatrix = new DOMMatrix().translate(ox, oy).multiply(targetMatrix).translate(-ox, -oy);
|
|
1779
|
+
ctx.save();
|
|
1780
|
+
const composedTransform = ctx.getTransform().multiply(finalMatrix);
|
|
1781
|
+
ctx.setTransform(composedTransform);
|
|
1782
|
+
renderNode(ctx, childNode);
|
|
1783
|
+
ctx.restore();
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
//#endregion
|
|
1787
|
+
//#region src/render/index.ts
|
|
1788
|
+
function renderNode(ctx, node) {
|
|
1789
|
+
const element = node.element;
|
|
1790
|
+
switch (element.type) {
|
|
1791
|
+
case "box":
|
|
1792
|
+
case "stack": {
|
|
1793
|
+
renderBox(ctx, node);
|
|
1794
|
+
const shouldClip = element.clip === true;
|
|
1795
|
+
if (shouldClip) {
|
|
1796
|
+
ctx.save();
|
|
1797
|
+
const { x, y, width, height } = node.layout;
|
|
1798
|
+
roundRectPath(ctx, x, y, width, height, normalizeBorderRadius(element.border?.radius));
|
|
1799
|
+
ctx.clip();
|
|
1800
|
+
}
|
|
1801
|
+
for (const child of node.children) renderNode(ctx, child);
|
|
1802
|
+
if (shouldClip) ctx.restore();
|
|
1803
|
+
break;
|
|
1804
|
+
}
|
|
1805
|
+
case "text":
|
|
1806
|
+
renderText(ctx, node);
|
|
1807
|
+
break;
|
|
1808
|
+
case "richtext":
|
|
1809
|
+
renderRichText(ctx, node);
|
|
1810
|
+
break;
|
|
1811
|
+
case "image":
|
|
1812
|
+
renderImage(ctx, node);
|
|
1813
|
+
break;
|
|
1814
|
+
case "svg":
|
|
1815
|
+
renderSvg(ctx, node);
|
|
1816
|
+
break;
|
|
1817
|
+
case "transform":
|
|
1818
|
+
renderTransform(ctx, node);
|
|
1819
|
+
break;
|
|
1820
|
+
case "customdraw":
|
|
1821
|
+
renderCustomDraw(ctx, node);
|
|
1822
|
+
break;
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
//#endregion
|
|
1827
|
+
//#region src/canvas.ts
|
|
1828
|
+
/**
|
|
1829
|
+
* 创建适用于浏览器环境的 Canvas
|
|
1830
|
+
*
|
|
1831
|
+
* 在浏览器环境中使用,支持传入已有的 canvas 实例
|
|
1832
|
+
*/
|
|
1833
|
+
function createCanvas(options) {
|
|
1834
|
+
const { width, height, pixelRatio = 1 } = options;
|
|
1835
|
+
const canvas = options.canvas ?? createRawCanvas(width * pixelRatio, height * pixelRatio);
|
|
1836
|
+
const ctx = canvas.getContext("2d");
|
|
1837
|
+
if (!ctx) throw new Error("Failed to get 2d context");
|
|
1838
|
+
if (options.imageSmoothingEnabled !== void 0) ctx.imageSmoothingEnabled = options.imageSmoothingEnabled;
|
|
1839
|
+
if (options.imageSmoothingQuality !== void 0) ctx.imageSmoothingQuality = options.imageSmoothingQuality;
|
|
1840
|
+
if (pixelRatio !== 1) ctx.scale(pixelRatio, pixelRatio);
|
|
1841
|
+
const measureCtx = createCanvasMeasureContext(ctx);
|
|
1842
|
+
return {
|
|
1843
|
+
width,
|
|
1844
|
+
height,
|
|
1845
|
+
pixelRatio,
|
|
1846
|
+
canvas,
|
|
1847
|
+
render(element) {
|
|
1848
|
+
const layoutTree = computeLayout(element, measureCtx, {
|
|
1849
|
+
minWidth: 0,
|
|
1850
|
+
maxWidth: width,
|
|
1851
|
+
minHeight: 0,
|
|
1852
|
+
maxHeight: height
|
|
1853
|
+
});
|
|
1854
|
+
renderNode(ctx, layoutTree);
|
|
1855
|
+
return layoutTree;
|
|
1856
|
+
},
|
|
1857
|
+
clear() {
|
|
1858
|
+
ctx.clearRect(0, 0, width, height);
|
|
1859
|
+
},
|
|
1860
|
+
getContext() {
|
|
1861
|
+
return ctx;
|
|
1862
|
+
},
|
|
1863
|
+
toDataURL(type, quality) {
|
|
1864
|
+
if ("toDataURL" in canvas && typeof canvas.toDataURL === "function") return canvas.toDataURL(type, quality);
|
|
1865
|
+
throw new Error("toDataURL not supported");
|
|
1866
|
+
},
|
|
1867
|
+
toBuffer(type = "image/png") {
|
|
1868
|
+
if ("toBuffer" in canvas && typeof canvas.toBuffer === "function") return canvas.toBuffer(type);
|
|
1869
|
+
throw new Error("toBuffer not supported in this environment");
|
|
1870
|
+
}
|
|
1871
|
+
};
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
//#endregion
|
|
1875
|
+
//#region src/components/Box.ts
|
|
1876
|
+
function Box(props) {
|
|
1877
|
+
return {
|
|
1878
|
+
type: "box",
|
|
1879
|
+
...props
|
|
1880
|
+
};
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
//#endregion
|
|
1884
|
+
//#region src/components/CustomDraw.ts
|
|
1885
|
+
function CustomDraw(props) {
|
|
1886
|
+
return {
|
|
1887
|
+
type: "customdraw",
|
|
1888
|
+
...props
|
|
1889
|
+
};
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
//#endregion
|
|
1893
|
+
//#region src/components/Image.ts
|
|
1894
|
+
function Image(props) {
|
|
1895
|
+
return {
|
|
1896
|
+
type: "image",
|
|
1897
|
+
...props
|
|
1898
|
+
};
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
//#endregion
|
|
1902
|
+
//#region src/components/RichText.ts
|
|
1903
|
+
function RichText(props) {
|
|
1904
|
+
return {
|
|
1905
|
+
type: "richtext",
|
|
1906
|
+
...props
|
|
1907
|
+
};
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
//#endregion
|
|
1911
|
+
//#region src/components/Stack.ts
|
|
1912
|
+
function Stack(props) {
|
|
1913
|
+
return {
|
|
1914
|
+
type: "stack",
|
|
1915
|
+
...props
|
|
1916
|
+
};
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
//#endregion
|
|
1920
|
+
//#region src/components/Svg.ts
|
|
1921
|
+
function Svg(props) {
|
|
1922
|
+
return {
|
|
1923
|
+
type: "svg",
|
|
1924
|
+
...props
|
|
1925
|
+
};
|
|
1926
|
+
}
|
|
1927
|
+
const svg = {
|
|
1928
|
+
rect: (props) => ({
|
|
1929
|
+
type: "rect",
|
|
1930
|
+
...props
|
|
1931
|
+
}),
|
|
1932
|
+
circle: (props) => ({
|
|
1933
|
+
type: "circle",
|
|
1934
|
+
...props
|
|
1935
|
+
}),
|
|
1936
|
+
ellipse: (props) => ({
|
|
1937
|
+
type: "ellipse",
|
|
1938
|
+
...props
|
|
1939
|
+
}),
|
|
1940
|
+
line: (props) => ({
|
|
1941
|
+
type: "line",
|
|
1942
|
+
...props
|
|
1943
|
+
}),
|
|
1944
|
+
polyline: (props) => ({
|
|
1945
|
+
type: "polyline",
|
|
1946
|
+
...props
|
|
1947
|
+
}),
|
|
1948
|
+
polygon: (props) => ({
|
|
1949
|
+
type: "polygon",
|
|
1950
|
+
...props
|
|
1951
|
+
}),
|
|
1952
|
+
path: (props) => ({
|
|
1953
|
+
type: "path",
|
|
1954
|
+
...props
|
|
1955
|
+
}),
|
|
1956
|
+
text: (props) => ({
|
|
1957
|
+
type: "text",
|
|
1958
|
+
...props
|
|
1959
|
+
}),
|
|
1960
|
+
g: (props) => ({
|
|
1961
|
+
type: "g",
|
|
1962
|
+
...props
|
|
1963
|
+
})
|
|
1964
|
+
};
|
|
1965
|
+
|
|
1966
|
+
//#endregion
|
|
1967
|
+
//#region src/components/Text.ts
|
|
1968
|
+
function Text(props) {
|
|
1969
|
+
return {
|
|
1970
|
+
type: "text",
|
|
1971
|
+
...props
|
|
1972
|
+
};
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
//#endregion
|
|
1976
|
+
//#region src/components/Transform.ts
|
|
1977
|
+
function Transform(props) {
|
|
1978
|
+
return {
|
|
1979
|
+
type: "transform",
|
|
1980
|
+
...props
|
|
1981
|
+
};
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
//#endregion
|
|
1985
|
+
//#region src/layout/utils/print.ts
|
|
1986
|
+
/**
|
|
1987
|
+
* 获取元素类型的显示名称
|
|
1988
|
+
*/
|
|
1989
|
+
function getElementType(element) {
|
|
1990
|
+
switch (element.type) {
|
|
1991
|
+
case "box": return "Box";
|
|
1992
|
+
case "text": return `Text "${element.content.slice(0, 20)}${element.content.length > 20 ? "..." : ""}"`;
|
|
1993
|
+
case "stack": return "Stack";
|
|
1994
|
+
case "image": return "Image";
|
|
1995
|
+
case "svg": return "Svg";
|
|
1996
|
+
default: return element.type;
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
/**
|
|
2000
|
+
* 递归打印布局树
|
|
2001
|
+
*/
|
|
2002
|
+
function printLayoutToString(node, prefix = "", isLast = true, depth = 0) {
|
|
2003
|
+
const lines = [];
|
|
2004
|
+
const connector = isLast ? "└─ " : "├─ ";
|
|
2005
|
+
const type = getElementType(node.element);
|
|
2006
|
+
const { x, y, width, height } = node.layout;
|
|
2007
|
+
const childCount = node.children.length;
|
|
2008
|
+
lines.push(`${prefix}${connector}${type} @(${Math.round(x)},${Math.round(y)}) size:${Math.round(width)}x${Math.round(height)}`);
|
|
2009
|
+
if (node.element.type === "text" && node.lines) {
|
|
2010
|
+
const contentPrefix = prefix + (isLast ? " " : "│ ");
|
|
2011
|
+
for (let i = 0; i < node.lines.length; i++) {
|
|
2012
|
+
const lineText = node.lines[i];
|
|
2013
|
+
const isLastLine = i === node.lines.length - 1 && childCount === 0;
|
|
2014
|
+
lines.push(`${contentPrefix}${isLastLine ? "└─ " : "├─ "}${JSON.stringify(lineText)}`);
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
2018
|
+
const child = node.children[i];
|
|
2019
|
+
const isChildLast = i === node.children.length - 1;
|
|
2020
|
+
const childPrefix = prefix + (isLast ? " " : "│ ");
|
|
2021
|
+
lines.push(...printLayoutToString(child, childPrefix, isChildLast, depth + 1));
|
|
2022
|
+
}
|
|
2023
|
+
return lines;
|
|
2024
|
+
}
|
|
2025
|
+
/**
|
|
2026
|
+
* 打印 LayoutNode 树结构到控制台
|
|
2027
|
+
*/
|
|
2028
|
+
function printLayout(node) {
|
|
2029
|
+
const lines = printLayoutToString(node, "", true);
|
|
2030
|
+
console.log(lines.join("\n"));
|
|
2031
|
+
}
|
|
2032
|
+
/**
|
|
2033
|
+
* 将 LayoutNode 转换为美观的字符串
|
|
2034
|
+
* @param node LayoutNode 根节点
|
|
2035
|
+
* @param indent 缩进字符串,默认为两个空格
|
|
2036
|
+
* @returns 格式化的字符串
|
|
2037
|
+
*/
|
|
2038
|
+
function layoutToString(node, _indent = " ") {
|
|
2039
|
+
return printLayoutToString(node, "", true).join("\n");
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
//#endregion
|
|
2043
|
+
exports.Box = Box;
|
|
2044
|
+
exports.CustomDraw = CustomDraw;
|
|
2045
|
+
exports.Image = Image;
|
|
2046
|
+
exports.RichText = RichText;
|
|
2047
|
+
exports.Stack = Stack;
|
|
2048
|
+
exports.Svg = Svg;
|
|
2049
|
+
exports.Text = Text;
|
|
2050
|
+
exports.Transform = Transform;
|
|
2051
|
+
exports.computeLayout = computeLayout;
|
|
2052
|
+
exports.createCanvas = createCanvas;
|
|
2053
|
+
exports.createCanvasMeasureContext = createCanvasMeasureContext;
|
|
2054
|
+
exports.layoutToString = layoutToString;
|
|
2055
|
+
exports.linearGradient = linearGradient;
|
|
2056
|
+
exports.printLayout = printLayout;
|
|
2057
|
+
exports.radialGradient = radialGradient;
|
|
2058
|
+
exports.svg = svg;
|