@codehz/draw-call 0.1.2 → 0.2.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/README.md +417 -6
- package/canvas.d.cts +101 -6
- package/canvas.d.mts +101 -6
- package/examples/card.ts +155 -0
- package/examples/customdraw-basic.ts +269 -0
- package/examples/customdraw.ts +339 -0
- package/examples/demo.ts +478 -0
- package/examples/richtext.ts +284 -0
- package/examples/transform.ts +437 -0
- package/index.cjs +90 -0
- package/index.d.cts +23 -11
- package/index.d.mts +23 -11
- package/index.mjs +86 -1
- package/node.cjs +2 -2
- package/node.d.cts +2 -2
- package/node.d.mts +2 -2
- package/node.mjs +4 -4
- package/package.json +1 -1
- package/render.cjs +567 -12
- package/render.mjs +567 -12
package/render.mjs
CHANGED
|
@@ -174,6 +174,23 @@ function measureBoxSize(element, ctx, availableWidth, measureChild) {
|
|
|
174
174
|
};
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
+
//#endregion
|
|
178
|
+
//#region src/layout/components/customDraw.ts
|
|
179
|
+
/**
|
|
180
|
+
* 测量 CustomDraw 元素的固有尺寸
|
|
181
|
+
*/
|
|
182
|
+
function measureCustomDrawSize(element, ctx, availableWidth, measureChild) {
|
|
183
|
+
if (typeof element.width === "number" && typeof element.height === "number") return {
|
|
184
|
+
width: element.width,
|
|
185
|
+
height: element.height
|
|
186
|
+
};
|
|
187
|
+
if (element.children && measureChild) return measureChild(element.children, ctx, availableWidth);
|
|
188
|
+
return {
|
|
189
|
+
width: 0,
|
|
190
|
+
height: 0
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
177
194
|
//#endregion
|
|
178
195
|
//#region src/layout/components/image.ts
|
|
179
196
|
/**
|
|
@@ -200,6 +217,174 @@ function measureImageSize(element, _ctx, _availableWidth) {
|
|
|
200
217
|
};
|
|
201
218
|
}
|
|
202
219
|
|
|
220
|
+
//#endregion
|
|
221
|
+
//#region src/layout/components/richtext.ts
|
|
222
|
+
/**
|
|
223
|
+
* 合并 span 样式和元素级别样式
|
|
224
|
+
* 优先级:span 样式 > 元素样式 > 默认值
|
|
225
|
+
* font 属性进行深度合并,允许 span 部分覆盖 element 的 font
|
|
226
|
+
*/
|
|
227
|
+
function mergeSpanStyle(span, elementStyle) {
|
|
228
|
+
return {
|
|
229
|
+
font: {
|
|
230
|
+
...elementStyle.font || {},
|
|
231
|
+
...span.font || {}
|
|
232
|
+
},
|
|
233
|
+
color: span.color ?? elementStyle.color,
|
|
234
|
+
background: span.background ?? elementStyle.background,
|
|
235
|
+
underline: span.underline ?? elementStyle.underline ?? false,
|
|
236
|
+
strikethrough: span.strikethrough ?? elementStyle.strikethrough ?? false
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* 测量富文本元素的固有尺寸
|
|
241
|
+
*/
|
|
242
|
+
function measureRichTextSize(element, ctx, availableWidth) {
|
|
243
|
+
const lineHeight = element.lineHeight ?? 1.2;
|
|
244
|
+
const elementStyle = {
|
|
245
|
+
font: element.font,
|
|
246
|
+
color: element.color,
|
|
247
|
+
background: element.background,
|
|
248
|
+
underline: element.underline,
|
|
249
|
+
strikethrough: element.strikethrough
|
|
250
|
+
};
|
|
251
|
+
const richLines = wrapRichText(ctx, element.spans, availableWidth, lineHeight, elementStyle);
|
|
252
|
+
let maxWidth = 0;
|
|
253
|
+
let totalHeight = 0;
|
|
254
|
+
for (const line of richLines) {
|
|
255
|
+
maxWidth = Math.max(maxWidth, line.width);
|
|
256
|
+
totalHeight += line.height;
|
|
257
|
+
}
|
|
258
|
+
return {
|
|
259
|
+
width: maxWidth,
|
|
260
|
+
height: totalHeight
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* 将富文本内容拆分为行
|
|
265
|
+
*/
|
|
266
|
+
function wrapRichText(ctx, spans, maxWidth, lineHeightScale = 1.2, elementStyle = {}) {
|
|
267
|
+
const lines = [];
|
|
268
|
+
let currentSegments = [];
|
|
269
|
+
let currentLineWidth = 0;
|
|
270
|
+
const pushLine = () => {
|
|
271
|
+
if (currentSegments.length === 0) return;
|
|
272
|
+
let maxTopDist = 0;
|
|
273
|
+
let maxBottomDist = 0;
|
|
274
|
+
let maxLineHeight = 0;
|
|
275
|
+
for (const seg of currentSegments) {
|
|
276
|
+
const topDist = seg.ascent - seg.offset;
|
|
277
|
+
const bottomDist = seg.descent + seg.offset;
|
|
278
|
+
maxTopDist = Math.max(maxTopDist, topDist);
|
|
279
|
+
maxBottomDist = Math.max(maxBottomDist, bottomDist);
|
|
280
|
+
maxLineHeight = Math.max(maxLineHeight, seg.height);
|
|
281
|
+
}
|
|
282
|
+
const contentHeight = maxTopDist + maxBottomDist;
|
|
283
|
+
const finalHeight = Math.max(contentHeight, maxLineHeight);
|
|
284
|
+
const extra = (finalHeight - contentHeight) / 2;
|
|
285
|
+
lines.push({
|
|
286
|
+
segments: [...currentSegments],
|
|
287
|
+
width: currentLineWidth,
|
|
288
|
+
height: finalHeight,
|
|
289
|
+
baseline: maxTopDist + extra
|
|
290
|
+
});
|
|
291
|
+
currentSegments = [];
|
|
292
|
+
currentLineWidth = 0;
|
|
293
|
+
};
|
|
294
|
+
for (const span of spans) {
|
|
295
|
+
const mergedStyle = mergeSpanStyle(span, elementStyle);
|
|
296
|
+
const font = mergedStyle.font;
|
|
297
|
+
const lh = (font.size ?? 16) * lineHeightScale;
|
|
298
|
+
const words = span.text.split(/(\s+)/);
|
|
299
|
+
for (const word of words) {
|
|
300
|
+
if (word === "") continue;
|
|
301
|
+
if (/^\s+$/.test(word)) {
|
|
302
|
+
const metrics = ctx.measureText(word, font);
|
|
303
|
+
const wordWidth = metrics.width;
|
|
304
|
+
if (maxWidth > 0 && currentLineWidth + wordWidth > maxWidth && currentSegments.length > 0) pushLine();
|
|
305
|
+
currentSegments.push({
|
|
306
|
+
text: word,
|
|
307
|
+
font: mergedStyle.font,
|
|
308
|
+
color: mergedStyle.color,
|
|
309
|
+
background: mergedStyle.background,
|
|
310
|
+
underline: mergedStyle.underline,
|
|
311
|
+
strikethrough: mergedStyle.strikethrough,
|
|
312
|
+
width: wordWidth,
|
|
313
|
+
height: lh,
|
|
314
|
+
ascent: metrics.ascent,
|
|
315
|
+
descent: metrics.descent,
|
|
316
|
+
offset: metrics.offset
|
|
317
|
+
});
|
|
318
|
+
currentLineWidth += wordWidth;
|
|
319
|
+
} else {
|
|
320
|
+
const metrics = ctx.measureText(word, font);
|
|
321
|
+
const wordWidth = metrics.width;
|
|
322
|
+
if (maxWidth <= 0 || currentLineWidth + wordWidth <= maxWidth) {
|
|
323
|
+
currentSegments.push({
|
|
324
|
+
text: word,
|
|
325
|
+
font: mergedStyle.font,
|
|
326
|
+
color: mergedStyle.color,
|
|
327
|
+
background: mergedStyle.background,
|
|
328
|
+
underline: mergedStyle.underline,
|
|
329
|
+
strikethrough: mergedStyle.strikethrough,
|
|
330
|
+
width: wordWidth,
|
|
331
|
+
height: lh,
|
|
332
|
+
ascent: metrics.ascent,
|
|
333
|
+
descent: metrics.descent,
|
|
334
|
+
offset: metrics.offset
|
|
335
|
+
});
|
|
336
|
+
currentLineWidth += wordWidth;
|
|
337
|
+
} else {
|
|
338
|
+
if (currentSegments.length > 0) pushLine();
|
|
339
|
+
const remainingWidth = maxWidth;
|
|
340
|
+
let currentPos = 0;
|
|
341
|
+
while (currentPos < word.length) {
|
|
342
|
+
let bestLen = 0;
|
|
343
|
+
for (let len = word.length - currentPos; len > 0; len--) {
|
|
344
|
+
const substr = word.substring(currentPos, currentPos + len);
|
|
345
|
+
const m = ctx.measureText(substr, font);
|
|
346
|
+
if (currentLineWidth + m.width <= remainingWidth) {
|
|
347
|
+
bestLen = len;
|
|
348
|
+
if (len < word.length - currentPos) break;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
if (bestLen === 0) {
|
|
352
|
+
if (currentSegments.length > 0) pushLine();
|
|
353
|
+
bestLen = 1;
|
|
354
|
+
}
|
|
355
|
+
const substr = word.substring(currentPos, currentPos + bestLen);
|
|
356
|
+
const m = ctx.measureText(substr, font);
|
|
357
|
+
currentSegments.push({
|
|
358
|
+
text: substr,
|
|
359
|
+
font: mergedStyle.font,
|
|
360
|
+
color: mergedStyle.color,
|
|
361
|
+
background: mergedStyle.background,
|
|
362
|
+
underline: mergedStyle.underline,
|
|
363
|
+
strikethrough: mergedStyle.strikethrough,
|
|
364
|
+
width: m.width,
|
|
365
|
+
height: lh,
|
|
366
|
+
ascent: m.ascent,
|
|
367
|
+
descent: m.descent,
|
|
368
|
+
offset: m.offset
|
|
369
|
+
});
|
|
370
|
+
currentLineWidth += m.width;
|
|
371
|
+
currentPos += bestLen;
|
|
372
|
+
if (currentPos < word.length && currentLineWidth >= remainingWidth) pushLine();
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
pushLine();
|
|
379
|
+
if (lines.length === 0) return [{
|
|
380
|
+
segments: [],
|
|
381
|
+
width: 0,
|
|
382
|
+
height: 0,
|
|
383
|
+
baseline: 0
|
|
384
|
+
}];
|
|
385
|
+
return lines;
|
|
386
|
+
}
|
|
387
|
+
|
|
203
388
|
//#endregion
|
|
204
389
|
//#region src/layout/components/stack.ts
|
|
205
390
|
/**
|
|
@@ -277,10 +462,13 @@ function createCanvasMeasureContext(ctx) {
|
|
|
277
462
|
ctx.textBaseline = "middle";
|
|
278
463
|
const metrics = ctx.measureText(text);
|
|
279
464
|
const height = metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent;
|
|
465
|
+
const fontSize = font.size || 16;
|
|
280
466
|
return {
|
|
281
467
|
width: metrics.width,
|
|
282
|
-
height: height ||
|
|
283
|
-
offset: (metrics.actualBoundingBoxAscent - metrics.actualBoundingBoxDescent) / 2
|
|
468
|
+
height: height || fontSize,
|
|
469
|
+
offset: (metrics.actualBoundingBoxAscent - metrics.actualBoundingBoxDescent) / 2,
|
|
470
|
+
ascent: metrics.actualBoundingBoxAscent,
|
|
471
|
+
descent: metrics.actualBoundingBoxDescent
|
|
284
472
|
};
|
|
285
473
|
} };
|
|
286
474
|
}
|
|
@@ -384,6 +572,17 @@ function measureTextSize(element, ctx, availableWidth) {
|
|
|
384
572
|
};
|
|
385
573
|
}
|
|
386
574
|
|
|
575
|
+
//#endregion
|
|
576
|
+
//#region src/layout/components/transform.ts
|
|
577
|
+
/**
|
|
578
|
+
* 测量 Transform 元素的固有尺寸
|
|
579
|
+
* Transform 不施加任何尺寸约束,直接透传子元素的测量结果
|
|
580
|
+
* 变换(rotate, scale 等)仅在渲染时应用,不影响固有尺寸
|
|
581
|
+
*/
|
|
582
|
+
function measureTransformSize(element, ctx, availableWidth, measureIntrinsicSize) {
|
|
583
|
+
return measureIntrinsicSize(element.children, ctx, availableWidth);
|
|
584
|
+
}
|
|
585
|
+
|
|
387
586
|
//#endregion
|
|
388
587
|
//#region src/layout/components/index.ts
|
|
389
588
|
/**
|
|
@@ -392,10 +591,13 @@ function measureTextSize(element, ctx, availableWidth) {
|
|
|
392
591
|
function measureIntrinsicSize(element, ctx, availableWidth) {
|
|
393
592
|
switch (element.type) {
|
|
394
593
|
case "text": return measureTextSize(element, ctx, availableWidth);
|
|
594
|
+
case "richtext": return measureRichTextSize(element, ctx, availableWidth);
|
|
395
595
|
case "box": return measureBoxSize(element, ctx, availableWidth, measureIntrinsicSize);
|
|
396
596
|
case "stack": return measureStackSize(element, ctx, availableWidth, measureIntrinsicSize);
|
|
397
597
|
case "image": return measureImageSize(element, ctx, availableWidth);
|
|
398
598
|
case "svg": return measureSvgSize(element, ctx, availableWidth);
|
|
599
|
+
case "transform": return measureTransformSize(element, ctx, availableWidth, measureIntrinsicSize);
|
|
600
|
+
case "customdraw": return measureCustomDrawSize(element, ctx, availableWidth, measureIntrinsicSize);
|
|
399
601
|
default: return {
|
|
400
602
|
width: 0,
|
|
401
603
|
height: 0
|
|
@@ -487,6 +689,23 @@ function computeLayout(element, ctx, constraints, x = 0, y = 0) {
|
|
|
487
689
|
node.lineOffsets = [offset];
|
|
488
690
|
}
|
|
489
691
|
}
|
|
692
|
+
if (element.type === "richtext") {
|
|
693
|
+
const lineHeight = element.lineHeight ?? 1.2;
|
|
694
|
+
let lines = wrapRichText(ctx, element.spans, contentWidth, lineHeight);
|
|
695
|
+
if (element.maxLines && lines.length > element.maxLines) {
|
|
696
|
+
lines = lines.slice(0, element.maxLines);
|
|
697
|
+
if (element.ellipsis && lines.length > 0) {
|
|
698
|
+
const lastLine = lines[lines.length - 1];
|
|
699
|
+
if (lastLine.segments.length > 0) {
|
|
700
|
+
const lastSeg = lastLine.segments[lastLine.segments.length - 1];
|
|
701
|
+
lastSeg.text += "...";
|
|
702
|
+
lastSeg.width = ctx.measureText(lastSeg.text, lastSeg.font ?? {}).width;
|
|
703
|
+
lastLine.width = lastLine.segments.reduce((sum, s) => sum + s.width, 0);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
node.richLines = lines;
|
|
708
|
+
}
|
|
490
709
|
if (element.type === "box" || element.type === "stack") {
|
|
491
710
|
const children = element.children ?? [];
|
|
492
711
|
if (element.type === "stack") {
|
|
@@ -736,6 +955,54 @@ function computeLayout(element, ctx, constraints, x = 0, y = 0) {
|
|
|
736
955
|
}
|
|
737
956
|
if (isReverse) node.children.reverse();
|
|
738
957
|
}
|
|
958
|
+
} else if (element.type === "transform") {
|
|
959
|
+
const child = element.children;
|
|
960
|
+
if (child) {
|
|
961
|
+
const childMargin = normalizeSpacing(child.margin);
|
|
962
|
+
const childNode = computeLayout(child, ctx, {
|
|
963
|
+
minWidth: 0,
|
|
964
|
+
maxWidth: contentWidth,
|
|
965
|
+
minHeight: 0,
|
|
966
|
+
maxHeight: contentHeight
|
|
967
|
+
}, contentX, contentY);
|
|
968
|
+
node.children.push(childNode);
|
|
969
|
+
if (element.width === void 0) {
|
|
970
|
+
const childOuterWidth = childNode.layout.width + childMargin.left + childMargin.right;
|
|
971
|
+
const actualWidth = childOuterWidth + padding.left + padding.right;
|
|
972
|
+
node.layout.width = actualWidth;
|
|
973
|
+
node.layout.contentWidth = childOuterWidth;
|
|
974
|
+
}
|
|
975
|
+
if (element.height === void 0) {
|
|
976
|
+
const childOuterHeight = childNode.layout.height + childMargin.top + childMargin.bottom;
|
|
977
|
+
const actualHeight = childOuterHeight + padding.top + padding.bottom;
|
|
978
|
+
node.layout.height = actualHeight;
|
|
979
|
+
node.layout.contentHeight = childOuterHeight;
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
} else if (element.type === "customdraw") {
|
|
983
|
+
const child = element.children;
|
|
984
|
+
if (child) {
|
|
985
|
+
const childMargin = normalizeSpacing(child.margin);
|
|
986
|
+
const childNode = computeLayout(child, ctx, {
|
|
987
|
+
minWidth: 0,
|
|
988
|
+
maxWidth: contentWidth,
|
|
989
|
+
minHeight: 0,
|
|
990
|
+
maxHeight: contentHeight
|
|
991
|
+
}, contentX, contentY);
|
|
992
|
+
node.children.push(childNode);
|
|
993
|
+
if (element.width === void 0) {
|
|
994
|
+
const childOuterWidth = childNode.layout.width + childMargin.left + childMargin.right;
|
|
995
|
+
const actualWidth = childOuterWidth + padding.left + padding.right;
|
|
996
|
+
node.layout.width = actualWidth;
|
|
997
|
+
node.layout.contentWidth = childOuterWidth;
|
|
998
|
+
}
|
|
999
|
+
if (element.height === void 0) {
|
|
1000
|
+
const childOuterHeight = childNode.layout.height + childMargin.top + childMargin.bottom;
|
|
1001
|
+
const actualHeight = childOuterHeight + padding.top + padding.bottom;
|
|
1002
|
+
node.layout.height = actualHeight;
|
|
1003
|
+
node.layout.contentHeight = childOuterHeight;
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
739
1006
|
}
|
|
740
1007
|
return node;
|
|
741
1008
|
}
|
|
@@ -844,6 +1111,163 @@ function renderBox(ctx, node) {
|
|
|
844
1111
|
if (element.opacity !== void 0 && element.opacity < 1) ctx.globalAlpha = 1;
|
|
845
1112
|
}
|
|
846
1113
|
|
|
1114
|
+
//#endregion
|
|
1115
|
+
//#region src/compat/DOMMatrix.ts
|
|
1116
|
+
const DOMMatrixCompat = (() => {
|
|
1117
|
+
if (typeof DOMMatrix !== "undefined") return DOMMatrix;
|
|
1118
|
+
try {
|
|
1119
|
+
return __require("@napi-rs/canvas").DOMMatrix;
|
|
1120
|
+
} catch {
|
|
1121
|
+
throw new Error("DOMMatrix is not available. In Node.js, install @napi-rs/canvas.");
|
|
1122
|
+
}
|
|
1123
|
+
})();
|
|
1124
|
+
|
|
1125
|
+
//#endregion
|
|
1126
|
+
//#region src/render/components/ProxiedCanvasContext.ts
|
|
1127
|
+
/**
|
|
1128
|
+
* ProxiedCanvasContext - Canvas 上下文代理类
|
|
1129
|
+
*
|
|
1130
|
+
* 该类提供对真实 CanvasRenderingContext2D 的代理,有以下功能:
|
|
1131
|
+
* 1. 管理 save/restore 的平衡(计数器)
|
|
1132
|
+
* 2. 追踪相对变换而不是绝对变换
|
|
1133
|
+
* 3. 在析构时自动恢复所有未恢复的状态
|
|
1134
|
+
* 4. 转发所有其他 Canvas API 调用
|
|
1135
|
+
*/
|
|
1136
|
+
var ProxiedCanvasContext = class {
|
|
1137
|
+
/**
|
|
1138
|
+
* 真实的 Canvas 上下文
|
|
1139
|
+
*/
|
|
1140
|
+
ctx;
|
|
1141
|
+
/**
|
|
1142
|
+
* 基础变换矩阵(初始化时设置,保持不变)
|
|
1143
|
+
*/
|
|
1144
|
+
baseTransform;
|
|
1145
|
+
/**
|
|
1146
|
+
* 相对变换矩阵(用户通过 setTransform 设置)
|
|
1147
|
+
*/
|
|
1148
|
+
relativeTransform;
|
|
1149
|
+
/**
|
|
1150
|
+
* save/restore 计数器
|
|
1151
|
+
*/
|
|
1152
|
+
saveCount = 0;
|
|
1153
|
+
/**
|
|
1154
|
+
* 构造函数
|
|
1155
|
+
* @param ctx 真实的 CanvasRenderingContext2D
|
|
1156
|
+
* @param baseTransform 初始的基础变换矩阵
|
|
1157
|
+
*/
|
|
1158
|
+
constructor(ctx, baseTransform) {
|
|
1159
|
+
this.ctx = ctx;
|
|
1160
|
+
this.baseTransform = baseTransform;
|
|
1161
|
+
this.relativeTransform = new DOMMatrixCompat();
|
|
1162
|
+
}
|
|
1163
|
+
/**
|
|
1164
|
+
* save() - 保存当前状态并增加计数
|
|
1165
|
+
*/
|
|
1166
|
+
save() {
|
|
1167
|
+
this.saveCount++;
|
|
1168
|
+
this.ctx.save();
|
|
1169
|
+
}
|
|
1170
|
+
/**
|
|
1171
|
+
* restore() - 恢复上一个状态并减少计数
|
|
1172
|
+
*/
|
|
1173
|
+
restore() {
|
|
1174
|
+
if (this.saveCount > 0) {
|
|
1175
|
+
this.saveCount--;
|
|
1176
|
+
this.ctx.restore();
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
/**
|
|
1180
|
+
* setTransform() - 设置相对变换
|
|
1181
|
+
*/
|
|
1182
|
+
setTransform(...args) {
|
|
1183
|
+
let matrix;
|
|
1184
|
+
if (args.length === 1 && args[0] instanceof DOMMatrixCompat) matrix = args[0];
|
|
1185
|
+
else if (args.length === 6) matrix = new DOMMatrixCompat([
|
|
1186
|
+
args[0],
|
|
1187
|
+
args[1],
|
|
1188
|
+
args[2],
|
|
1189
|
+
args[3],
|
|
1190
|
+
args[4],
|
|
1191
|
+
args[5]
|
|
1192
|
+
]);
|
|
1193
|
+
else return;
|
|
1194
|
+
this.relativeTransform = matrix;
|
|
1195
|
+
const actualTransform = this.baseTransform.multiply(matrix);
|
|
1196
|
+
this.ctx.setTransform(actualTransform);
|
|
1197
|
+
}
|
|
1198
|
+
/**
|
|
1199
|
+
* getTransform() - 返回相对变换(而不是绝对变换)
|
|
1200
|
+
*/
|
|
1201
|
+
getTransform() {
|
|
1202
|
+
return this.relativeTransform;
|
|
1203
|
+
}
|
|
1204
|
+
/**
|
|
1205
|
+
* 析构函数级的清理 - 自动恢复所有未恢复的 save
|
|
1206
|
+
*/
|
|
1207
|
+
destroy() {
|
|
1208
|
+
while (this.saveCount > 0) {
|
|
1209
|
+
console.log("destroy restore", this.saveCount);
|
|
1210
|
+
this.saveCount--;
|
|
1211
|
+
this.ctx.restore();
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
};
|
|
1215
|
+
function createProxiedCanvasContext(ctx, baseTransform) {
|
|
1216
|
+
const proxy = new ProxiedCanvasContext(ctx, baseTransform);
|
|
1217
|
+
return new Proxy(proxy, {
|
|
1218
|
+
get(target, prop, receiver) {
|
|
1219
|
+
if (prop === "save" || prop === "restore" || prop === "setTransform" || prop === "getTransform" || prop === "destroy") return Reflect.get(target, prop, receiver).bind(proxy);
|
|
1220
|
+
const ownValue = Reflect.get(target, prop, receiver);
|
|
1221
|
+
if (ownValue !== void 0) return ownValue;
|
|
1222
|
+
const contextValue = target.ctx[prop];
|
|
1223
|
+
if (typeof contextValue === "function") return contextValue.bind(target.ctx);
|
|
1224
|
+
return contextValue;
|
|
1225
|
+
},
|
|
1226
|
+
set(target, prop, value, _receiver) {
|
|
1227
|
+
target.ctx[prop] = value;
|
|
1228
|
+
return true;
|
|
1229
|
+
},
|
|
1230
|
+
has(target, prop) {
|
|
1231
|
+
if (prop === "save" || prop === "restore" || prop === "setTransform" || prop === "getTransform" || prop === "destroy") return true;
|
|
1232
|
+
return prop in target.ctx;
|
|
1233
|
+
},
|
|
1234
|
+
ownKeys(target) {
|
|
1235
|
+
return Reflect.ownKeys(target.ctx);
|
|
1236
|
+
},
|
|
1237
|
+
getOwnPropertyDescriptor(target, prop) {
|
|
1238
|
+
return Reflect.getOwnPropertyDescriptor(target.ctx, prop);
|
|
1239
|
+
}
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
//#endregion
|
|
1244
|
+
//#region src/render/components/customDraw.ts
|
|
1245
|
+
/**
|
|
1246
|
+
* 渲染 CustomDraw 组件
|
|
1247
|
+
* 提供自定义绘制回调,用户可以直接访问 Canvas 上下文并绘制自定义内容
|
|
1248
|
+
*/
|
|
1249
|
+
function renderCustomDraw(ctx, node) {
|
|
1250
|
+
const element = node.element;
|
|
1251
|
+
ctx.save();
|
|
1252
|
+
ctx.translate(node.layout.x, node.layout.y);
|
|
1253
|
+
const proxyCtx = createProxiedCanvasContext(ctx, ctx.getTransform());
|
|
1254
|
+
const inner = () => {
|
|
1255
|
+
if (node.children && node.children.length > 0) {
|
|
1256
|
+
ctx.save();
|
|
1257
|
+
ctx.translate(-node.layout.x, -node.layout.y);
|
|
1258
|
+
renderNode(ctx, node.children[0]);
|
|
1259
|
+
ctx.restore();
|
|
1260
|
+
}
|
|
1261
|
+
};
|
|
1262
|
+
element.draw(proxyCtx, {
|
|
1263
|
+
inner,
|
|
1264
|
+
width: node.layout.contentWidth,
|
|
1265
|
+
height: node.layout.contentHeight
|
|
1266
|
+
});
|
|
1267
|
+
proxyCtx.destroy();
|
|
1268
|
+
ctx.restore();
|
|
1269
|
+
}
|
|
1270
|
+
|
|
847
1271
|
//#endregion
|
|
848
1272
|
//#region src/render/components/image.ts
|
|
849
1273
|
function renderImage(ctx, node) {
|
|
@@ -931,15 +1355,55 @@ function renderImage(ctx, node) {
|
|
|
931
1355
|
}
|
|
932
1356
|
|
|
933
1357
|
//#endregion
|
|
934
|
-
//#region src/
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
1358
|
+
//#region src/render/components/richtext.ts
|
|
1359
|
+
function renderRichText(ctx, node) {
|
|
1360
|
+
const element = node.element;
|
|
1361
|
+
const { contentX, contentY, contentWidth, contentHeight } = node.layout;
|
|
1362
|
+
const lines = node.richLines ?? [];
|
|
1363
|
+
if (lines.length === 0) return;
|
|
1364
|
+
const totalTextHeight = lines.reduce((sum, line) => sum + line.height, 0);
|
|
1365
|
+
let verticalOffset = 0;
|
|
1366
|
+
if (element.verticalAlign === "middle") verticalOffset = (contentHeight - totalTextHeight) / 2;
|
|
1367
|
+
else if (element.verticalAlign === "bottom") verticalOffset = contentHeight - totalTextHeight;
|
|
1368
|
+
let currentY = contentY + verticalOffset;
|
|
1369
|
+
for (const line of lines) {
|
|
1370
|
+
let lineX = contentX;
|
|
1371
|
+
if (element.align === "center") lineX = contentX + (contentWidth - line.width) / 2;
|
|
1372
|
+
else if (element.align === "right") lineX = contentX + (contentWidth - line.width);
|
|
1373
|
+
const baselineY = currentY + line.baseline;
|
|
1374
|
+
for (const seg of line.segments) {
|
|
1375
|
+
ctx.save();
|
|
1376
|
+
ctx.font = buildFontString(seg.font ?? {});
|
|
1377
|
+
if (seg.background) {
|
|
1378
|
+
ctx.fillStyle = resolveColor$1(ctx, seg.background, lineX, currentY, seg.width, line.height);
|
|
1379
|
+
ctx.fillRect(lineX, currentY, seg.width, line.height);
|
|
1380
|
+
}
|
|
1381
|
+
ctx.fillStyle = seg.color ? resolveColor$1(ctx, seg.color, lineX, currentY, seg.width, line.height) : "#000";
|
|
1382
|
+
ctx.textBaseline = "middle";
|
|
1383
|
+
ctx.fillText(seg.text, lineX, baselineY - seg.offset);
|
|
1384
|
+
if (seg.underline) {
|
|
1385
|
+
ctx.beginPath();
|
|
1386
|
+
ctx.strokeStyle = ctx.fillStyle;
|
|
1387
|
+
ctx.lineWidth = 1;
|
|
1388
|
+
ctx.moveTo(lineX, currentY + seg.height);
|
|
1389
|
+
ctx.lineTo(lineX + seg.width, currentY + seg.height);
|
|
1390
|
+
ctx.stroke();
|
|
1391
|
+
}
|
|
1392
|
+
if (seg.strikethrough) {
|
|
1393
|
+
ctx.beginPath();
|
|
1394
|
+
ctx.strokeStyle = ctx.fillStyle;
|
|
1395
|
+
ctx.lineWidth = 1;
|
|
1396
|
+
const strikeY = currentY + seg.height / 2 + seg.offset;
|
|
1397
|
+
ctx.moveTo(lineX, strikeY);
|
|
1398
|
+
ctx.lineTo(lineX + seg.width, strikeY);
|
|
1399
|
+
ctx.stroke();
|
|
1400
|
+
}
|
|
1401
|
+
ctx.restore();
|
|
1402
|
+
lineX += seg.width;
|
|
1403
|
+
}
|
|
1404
|
+
currentY += line.height;
|
|
941
1405
|
}
|
|
942
|
-
}
|
|
1406
|
+
}
|
|
943
1407
|
|
|
944
1408
|
//#endregion
|
|
945
1409
|
//#region src/compat/Path2D.ts
|
|
@@ -1016,8 +1480,14 @@ function applyTransform(base, transform) {
|
|
|
1016
1480
|
}
|
|
1017
1481
|
if (transform.scale !== void 0) if (typeof transform.scale === "number") result = result.scale(transform.scale);
|
|
1018
1482
|
else result = result.scale(transform.scale[0], transform.scale[1]);
|
|
1019
|
-
if (transform.skewX !== void 0)
|
|
1020
|
-
|
|
1483
|
+
if (transform.skewX !== void 0) {
|
|
1484
|
+
const degrees = transform.skewX * 180 / Math.PI;
|
|
1485
|
+
result = result.skewX(degrees);
|
|
1486
|
+
}
|
|
1487
|
+
if (transform.skewY !== void 0) {
|
|
1488
|
+
const degrees = transform.skewY * 180 / Math.PI;
|
|
1489
|
+
result = result.skewY(degrees);
|
|
1490
|
+
}
|
|
1021
1491
|
return result;
|
|
1022
1492
|
}
|
|
1023
1493
|
function applyStroke(ctx, stroke, bounds) {
|
|
@@ -1253,6 +1723,82 @@ function renderText(ctx, node) {
|
|
|
1253
1723
|
if (element.shadow) clearShadow$1(ctx);
|
|
1254
1724
|
}
|
|
1255
1725
|
|
|
1726
|
+
//#endregion
|
|
1727
|
+
//#region src/render/components/transform.ts
|
|
1728
|
+
/**
|
|
1729
|
+
* 解析 Transform 值为 DOMMatrix
|
|
1730
|
+
* 支持三种格式:
|
|
1731
|
+
* - 数组: [a, b, c, d, e, f]
|
|
1732
|
+
* - DOMMatrix2DInit 对象: { a, b, c, d, e, f, ... }
|
|
1733
|
+
* - 简易对象: { translate, rotate, scale, skewX, skewY }
|
|
1734
|
+
*/
|
|
1735
|
+
function parseTransformValue(transform) {
|
|
1736
|
+
if (transform === void 0) return new DOMMatrixCompat();
|
|
1737
|
+
if (Array.isArray(transform)) return new DOMMatrixCompat(transform);
|
|
1738
|
+
const hasDOMMatrixInit = "a" in transform || "b" in transform || "c" in transform || "d" in transform || "e" in transform || "f" in transform;
|
|
1739
|
+
const hasSimpleTransform = "translate" in transform || "rotate" in transform || "scale" in transform || "skewX" in transform || "skewY" in transform;
|
|
1740
|
+
if (hasDOMMatrixInit && !hasSimpleTransform) {
|
|
1741
|
+
const init = transform;
|
|
1742
|
+
return new DOMMatrixCompat([
|
|
1743
|
+
init.a ?? 1,
|
|
1744
|
+
init.b ?? 0,
|
|
1745
|
+
init.c ?? 0,
|
|
1746
|
+
init.d ?? 1,
|
|
1747
|
+
init.e ?? 0,
|
|
1748
|
+
init.f ?? 0
|
|
1749
|
+
]);
|
|
1750
|
+
}
|
|
1751
|
+
const simpleObj = transform;
|
|
1752
|
+
let result = new DOMMatrixCompat();
|
|
1753
|
+
if (simpleObj.translate) result = result.translate(simpleObj.translate[0], simpleObj.translate[1]);
|
|
1754
|
+
if (simpleObj.rotate !== void 0) if (typeof simpleObj.rotate === "number") result = result.rotate(simpleObj.rotate);
|
|
1755
|
+
else {
|
|
1756
|
+
const [angle, cx, cy] = simpleObj.rotate;
|
|
1757
|
+
result = result.translate(cx, cy).rotate(angle).translate(-cx, -cy);
|
|
1758
|
+
}
|
|
1759
|
+
if (simpleObj.scale !== void 0) if (typeof simpleObj.scale === "number") result = result.scale(simpleObj.scale);
|
|
1760
|
+
else result = result.scale(simpleObj.scale[0], simpleObj.scale[1]);
|
|
1761
|
+
if (simpleObj.skewX !== void 0) result = result.skewX(simpleObj.skewX);
|
|
1762
|
+
if (simpleObj.skewY !== void 0) result = result.skewY(simpleObj.skewY);
|
|
1763
|
+
return result;
|
|
1764
|
+
}
|
|
1765
|
+
/**
|
|
1766
|
+
* 根据 transformOrigin 属性和子元素尺寸计算实际变换原点坐标
|
|
1767
|
+
*/
|
|
1768
|
+
function resolveTransformOrigin(origin, childLayout) {
|
|
1769
|
+
if (origin === void 0) return [0, 0];
|
|
1770
|
+
const [xVal, yVal] = origin;
|
|
1771
|
+
const sizes = [childLayout.width, childLayout.height];
|
|
1772
|
+
const values = [xVal, yVal];
|
|
1773
|
+
const result = [0, 0];
|
|
1774
|
+
for (let i = 0; i < 2; i++) {
|
|
1775
|
+
const val = values[i];
|
|
1776
|
+
if (typeof val === "string") if (val.endsWith("%")) result[i] = parseFloat(val) / 100 * sizes[i];
|
|
1777
|
+
else result[i] = parseFloat(val);
|
|
1778
|
+
else result[i] = val;
|
|
1779
|
+
}
|
|
1780
|
+
return result;
|
|
1781
|
+
}
|
|
1782
|
+
/**
|
|
1783
|
+
* 渲染 Transform 组件及其子元素
|
|
1784
|
+
*/
|
|
1785
|
+
function renderTransform(ctx, node) {
|
|
1786
|
+
const element = node.element;
|
|
1787
|
+
const { children } = node;
|
|
1788
|
+
if (!children || children.length === 0) return;
|
|
1789
|
+
const childNode = children[0];
|
|
1790
|
+
const [relativeOx, relativeOy] = resolveTransformOrigin(element.transformOrigin, childNode.layout);
|
|
1791
|
+
const ox = childNode.layout.x + relativeOx;
|
|
1792
|
+
const oy = childNode.layout.y + relativeOy;
|
|
1793
|
+
const targetMatrix = parseTransformValue(element.transform);
|
|
1794
|
+
const finalMatrix = new DOMMatrixCompat().translate(ox, oy).multiply(targetMatrix).translate(-ox, -oy);
|
|
1795
|
+
ctx.save();
|
|
1796
|
+
const composedTransform = ctx.getTransform().multiply(finalMatrix);
|
|
1797
|
+
ctx.setTransform(composedTransform);
|
|
1798
|
+
renderNode(ctx, childNode);
|
|
1799
|
+
ctx.restore();
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1256
1802
|
//#endregion
|
|
1257
1803
|
//#region src/render/index.ts
|
|
1258
1804
|
function renderNode(ctx, node) {
|
|
@@ -1275,12 +1821,21 @@ function renderNode(ctx, node) {
|
|
|
1275
1821
|
case "text":
|
|
1276
1822
|
renderText(ctx, node);
|
|
1277
1823
|
break;
|
|
1824
|
+
case "richtext":
|
|
1825
|
+
renderRichText(ctx, node);
|
|
1826
|
+
break;
|
|
1278
1827
|
case "image":
|
|
1279
1828
|
renderImage(ctx, node);
|
|
1280
1829
|
break;
|
|
1281
1830
|
case "svg":
|
|
1282
1831
|
renderSvg(ctx, node);
|
|
1283
1832
|
break;
|
|
1833
|
+
case "transform":
|
|
1834
|
+
renderTransform(ctx, node);
|
|
1835
|
+
break;
|
|
1836
|
+
case "customdraw":
|
|
1837
|
+
renderCustomDraw(ctx, node);
|
|
1838
|
+
break;
|
|
1284
1839
|
}
|
|
1285
1840
|
}
|
|
1286
1841
|
|