@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/render.cjs CHANGED
@@ -169,6 +169,23 @@ function measureBoxSize(element, ctx, availableWidth, measureChild) {
169
169
  };
170
170
  }
171
171
 
172
+ //#endregion
173
+ //#region src/layout/components/customDraw.ts
174
+ /**
175
+ * 测量 CustomDraw 元素的固有尺寸
176
+ */
177
+ function measureCustomDrawSize(element, ctx, availableWidth, measureChild) {
178
+ if (typeof element.width === "number" && typeof element.height === "number") return {
179
+ width: element.width,
180
+ height: element.height
181
+ };
182
+ if (element.children && measureChild) return measureChild(element.children, ctx, availableWidth);
183
+ return {
184
+ width: 0,
185
+ height: 0
186
+ };
187
+ }
188
+
172
189
  //#endregion
173
190
  //#region src/layout/components/image.ts
174
191
  /**
@@ -195,6 +212,174 @@ function measureImageSize(element, _ctx, _availableWidth) {
195
212
  };
196
213
  }
197
214
 
215
+ //#endregion
216
+ //#region src/layout/components/richtext.ts
217
+ /**
218
+ * 合并 span 样式和元素级别样式
219
+ * 优先级:span 样式 > 元素样式 > 默认值
220
+ * font 属性进行深度合并,允许 span 部分覆盖 element 的 font
221
+ */
222
+ function mergeSpanStyle(span, elementStyle) {
223
+ return {
224
+ font: {
225
+ ...elementStyle.font || {},
226
+ ...span.font || {}
227
+ },
228
+ color: span.color ?? elementStyle.color,
229
+ background: span.background ?? elementStyle.background,
230
+ underline: span.underline ?? elementStyle.underline ?? false,
231
+ strikethrough: span.strikethrough ?? elementStyle.strikethrough ?? false
232
+ };
233
+ }
234
+ /**
235
+ * 测量富文本元素的固有尺寸
236
+ */
237
+ function measureRichTextSize(element, ctx, availableWidth) {
238
+ const lineHeight = element.lineHeight ?? 1.2;
239
+ const elementStyle = {
240
+ font: element.font,
241
+ color: element.color,
242
+ background: element.background,
243
+ underline: element.underline,
244
+ strikethrough: element.strikethrough
245
+ };
246
+ const richLines = wrapRichText(ctx, element.spans, availableWidth, lineHeight, elementStyle);
247
+ let maxWidth = 0;
248
+ let totalHeight = 0;
249
+ for (const line of richLines) {
250
+ maxWidth = Math.max(maxWidth, line.width);
251
+ totalHeight += line.height;
252
+ }
253
+ return {
254
+ width: maxWidth,
255
+ height: totalHeight
256
+ };
257
+ }
258
+ /**
259
+ * 将富文本内容拆分为行
260
+ */
261
+ function wrapRichText(ctx, spans, maxWidth, lineHeightScale = 1.2, elementStyle = {}) {
262
+ const lines = [];
263
+ let currentSegments = [];
264
+ let currentLineWidth = 0;
265
+ const pushLine = () => {
266
+ if (currentSegments.length === 0) return;
267
+ let maxTopDist = 0;
268
+ let maxBottomDist = 0;
269
+ let maxLineHeight = 0;
270
+ for (const seg of currentSegments) {
271
+ const topDist = seg.ascent - seg.offset;
272
+ const bottomDist = seg.descent + seg.offset;
273
+ maxTopDist = Math.max(maxTopDist, topDist);
274
+ maxBottomDist = Math.max(maxBottomDist, bottomDist);
275
+ maxLineHeight = Math.max(maxLineHeight, seg.height);
276
+ }
277
+ const contentHeight = maxTopDist + maxBottomDist;
278
+ const finalHeight = Math.max(contentHeight, maxLineHeight);
279
+ const extra = (finalHeight - contentHeight) / 2;
280
+ lines.push({
281
+ segments: [...currentSegments],
282
+ width: currentLineWidth,
283
+ height: finalHeight,
284
+ baseline: maxTopDist + extra
285
+ });
286
+ currentSegments = [];
287
+ currentLineWidth = 0;
288
+ };
289
+ for (const span of spans) {
290
+ const mergedStyle = mergeSpanStyle(span, elementStyle);
291
+ const font = mergedStyle.font;
292
+ const lh = (font.size ?? 16) * lineHeightScale;
293
+ const words = span.text.split(/(\s+)/);
294
+ for (const word of words) {
295
+ if (word === "") continue;
296
+ if (/^\s+$/.test(word)) {
297
+ const metrics = ctx.measureText(word, font);
298
+ const wordWidth = metrics.width;
299
+ if (maxWidth > 0 && currentLineWidth + wordWidth > maxWidth && currentSegments.length > 0) pushLine();
300
+ currentSegments.push({
301
+ text: word,
302
+ font: mergedStyle.font,
303
+ color: mergedStyle.color,
304
+ background: mergedStyle.background,
305
+ underline: mergedStyle.underline,
306
+ strikethrough: mergedStyle.strikethrough,
307
+ width: wordWidth,
308
+ height: lh,
309
+ ascent: metrics.ascent,
310
+ descent: metrics.descent,
311
+ offset: metrics.offset
312
+ });
313
+ currentLineWidth += wordWidth;
314
+ } else {
315
+ const metrics = ctx.measureText(word, font);
316
+ const wordWidth = metrics.width;
317
+ if (maxWidth <= 0 || currentLineWidth + wordWidth <= maxWidth) {
318
+ currentSegments.push({
319
+ text: word,
320
+ font: mergedStyle.font,
321
+ color: mergedStyle.color,
322
+ background: mergedStyle.background,
323
+ underline: mergedStyle.underline,
324
+ strikethrough: mergedStyle.strikethrough,
325
+ width: wordWidth,
326
+ height: lh,
327
+ ascent: metrics.ascent,
328
+ descent: metrics.descent,
329
+ offset: metrics.offset
330
+ });
331
+ currentLineWidth += wordWidth;
332
+ } else {
333
+ if (currentSegments.length > 0) pushLine();
334
+ const remainingWidth = maxWidth;
335
+ let currentPos = 0;
336
+ while (currentPos < word.length) {
337
+ let bestLen = 0;
338
+ for (let len = word.length - currentPos; len > 0; len--) {
339
+ const substr = word.substring(currentPos, currentPos + len);
340
+ const m = ctx.measureText(substr, font);
341
+ if (currentLineWidth + m.width <= remainingWidth) {
342
+ bestLen = len;
343
+ if (len < word.length - currentPos) break;
344
+ }
345
+ }
346
+ if (bestLen === 0) {
347
+ if (currentSegments.length > 0) pushLine();
348
+ bestLen = 1;
349
+ }
350
+ const substr = word.substring(currentPos, currentPos + bestLen);
351
+ const m = ctx.measureText(substr, font);
352
+ currentSegments.push({
353
+ text: substr,
354
+ font: mergedStyle.font,
355
+ color: mergedStyle.color,
356
+ background: mergedStyle.background,
357
+ underline: mergedStyle.underline,
358
+ strikethrough: mergedStyle.strikethrough,
359
+ width: m.width,
360
+ height: lh,
361
+ ascent: m.ascent,
362
+ descent: m.descent,
363
+ offset: m.offset
364
+ });
365
+ currentLineWidth += m.width;
366
+ currentPos += bestLen;
367
+ if (currentPos < word.length && currentLineWidth >= remainingWidth) pushLine();
368
+ }
369
+ }
370
+ }
371
+ }
372
+ }
373
+ pushLine();
374
+ if (lines.length === 0) return [{
375
+ segments: [],
376
+ width: 0,
377
+ height: 0,
378
+ baseline: 0
379
+ }];
380
+ return lines;
381
+ }
382
+
198
383
  //#endregion
199
384
  //#region src/layout/components/stack.ts
200
385
  /**
@@ -272,10 +457,13 @@ function createCanvasMeasureContext(ctx) {
272
457
  ctx.textBaseline = "middle";
273
458
  const metrics = ctx.measureText(text);
274
459
  const height = metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent;
460
+ const fontSize = font.size || 16;
275
461
  return {
276
462
  width: metrics.width,
277
- height: height || font.size || 16,
278
- offset: (metrics.actualBoundingBoxAscent - metrics.actualBoundingBoxDescent) / 2
463
+ height: height || fontSize,
464
+ offset: (metrics.actualBoundingBoxAscent - metrics.actualBoundingBoxDescent) / 2,
465
+ ascent: metrics.actualBoundingBoxAscent,
466
+ descent: metrics.actualBoundingBoxDescent
279
467
  };
280
468
  } };
281
469
  }
@@ -379,6 +567,17 @@ function measureTextSize(element, ctx, availableWidth) {
379
567
  };
380
568
  }
381
569
 
570
+ //#endregion
571
+ //#region src/layout/components/transform.ts
572
+ /**
573
+ * 测量 Transform 元素的固有尺寸
574
+ * Transform 不施加任何尺寸约束,直接透传子元素的测量结果
575
+ * 变换(rotate, scale 等)仅在渲染时应用,不影响固有尺寸
576
+ */
577
+ function measureTransformSize(element, ctx, availableWidth, measureIntrinsicSize) {
578
+ return measureIntrinsicSize(element.children, ctx, availableWidth);
579
+ }
580
+
382
581
  //#endregion
383
582
  //#region src/layout/components/index.ts
384
583
  /**
@@ -387,10 +586,13 @@ function measureTextSize(element, ctx, availableWidth) {
387
586
  function measureIntrinsicSize(element, ctx, availableWidth) {
388
587
  switch (element.type) {
389
588
  case "text": return measureTextSize(element, ctx, availableWidth);
589
+ case "richtext": return measureRichTextSize(element, ctx, availableWidth);
390
590
  case "box": return measureBoxSize(element, ctx, availableWidth, measureIntrinsicSize);
391
591
  case "stack": return measureStackSize(element, ctx, availableWidth, measureIntrinsicSize);
392
592
  case "image": return measureImageSize(element, ctx, availableWidth);
393
593
  case "svg": return measureSvgSize(element, ctx, availableWidth);
594
+ case "transform": return measureTransformSize(element, ctx, availableWidth, measureIntrinsicSize);
595
+ case "customdraw": return measureCustomDrawSize(element, ctx, availableWidth, measureIntrinsicSize);
394
596
  default: return {
395
597
  width: 0,
396
598
  height: 0
@@ -482,6 +684,23 @@ function computeLayout(element, ctx, constraints, x = 0, y = 0) {
482
684
  node.lineOffsets = [offset];
483
685
  }
484
686
  }
687
+ if (element.type === "richtext") {
688
+ const lineHeight = element.lineHeight ?? 1.2;
689
+ let lines = wrapRichText(ctx, element.spans, contentWidth, lineHeight);
690
+ if (element.maxLines && lines.length > element.maxLines) {
691
+ lines = lines.slice(0, element.maxLines);
692
+ if (element.ellipsis && lines.length > 0) {
693
+ const lastLine = lines[lines.length - 1];
694
+ if (lastLine.segments.length > 0) {
695
+ const lastSeg = lastLine.segments[lastLine.segments.length - 1];
696
+ lastSeg.text += "...";
697
+ lastSeg.width = ctx.measureText(lastSeg.text, lastSeg.font ?? {}).width;
698
+ lastLine.width = lastLine.segments.reduce((sum, s) => sum + s.width, 0);
699
+ }
700
+ }
701
+ }
702
+ node.richLines = lines;
703
+ }
485
704
  if (element.type === "box" || element.type === "stack") {
486
705
  const children = element.children ?? [];
487
706
  if (element.type === "stack") {
@@ -731,6 +950,54 @@ function computeLayout(element, ctx, constraints, x = 0, y = 0) {
731
950
  }
732
951
  if (isReverse) node.children.reverse();
733
952
  }
953
+ } else if (element.type === "transform") {
954
+ const child = element.children;
955
+ if (child) {
956
+ const childMargin = normalizeSpacing(child.margin);
957
+ const childNode = computeLayout(child, ctx, {
958
+ minWidth: 0,
959
+ maxWidth: contentWidth,
960
+ minHeight: 0,
961
+ maxHeight: contentHeight
962
+ }, contentX, contentY);
963
+ node.children.push(childNode);
964
+ if (element.width === void 0) {
965
+ const childOuterWidth = childNode.layout.width + childMargin.left + childMargin.right;
966
+ const actualWidth = childOuterWidth + padding.left + padding.right;
967
+ node.layout.width = actualWidth;
968
+ node.layout.contentWidth = childOuterWidth;
969
+ }
970
+ if (element.height === void 0) {
971
+ const childOuterHeight = childNode.layout.height + childMargin.top + childMargin.bottom;
972
+ const actualHeight = childOuterHeight + padding.top + padding.bottom;
973
+ node.layout.height = actualHeight;
974
+ node.layout.contentHeight = childOuterHeight;
975
+ }
976
+ }
977
+ } else if (element.type === "customdraw") {
978
+ const child = element.children;
979
+ if (child) {
980
+ const childMargin = normalizeSpacing(child.margin);
981
+ const childNode = computeLayout(child, ctx, {
982
+ minWidth: 0,
983
+ maxWidth: contentWidth,
984
+ minHeight: 0,
985
+ maxHeight: contentHeight
986
+ }, contentX, contentY);
987
+ node.children.push(childNode);
988
+ if (element.width === void 0) {
989
+ const childOuterWidth = childNode.layout.width + childMargin.left + childMargin.right;
990
+ const actualWidth = childOuterWidth + padding.left + padding.right;
991
+ node.layout.width = actualWidth;
992
+ node.layout.contentWidth = childOuterWidth;
993
+ }
994
+ if (element.height === void 0) {
995
+ const childOuterHeight = childNode.layout.height + childMargin.top + childMargin.bottom;
996
+ const actualHeight = childOuterHeight + padding.top + padding.bottom;
997
+ node.layout.height = actualHeight;
998
+ node.layout.contentHeight = childOuterHeight;
999
+ }
1000
+ }
734
1001
  }
735
1002
  return node;
736
1003
  }
@@ -839,6 +1106,163 @@ function renderBox(ctx, node) {
839
1106
  if (element.opacity !== void 0 && element.opacity < 1) ctx.globalAlpha = 1;
840
1107
  }
841
1108
 
1109
+ //#endregion
1110
+ //#region src/compat/DOMMatrix.ts
1111
+ const DOMMatrixCompat = (() => {
1112
+ if (typeof DOMMatrix !== "undefined") return DOMMatrix;
1113
+ try {
1114
+ return require("@napi-rs/canvas").DOMMatrix;
1115
+ } catch {
1116
+ throw new Error("DOMMatrix is not available. In Node.js, install @napi-rs/canvas.");
1117
+ }
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 DOMMatrixCompat();
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 DOMMatrixCompat) matrix = args[0];
1180
+ else if (args.length === 6) matrix = new DOMMatrixCompat([
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
+
842
1266
  //#endregion
843
1267
  //#region src/render/components/image.ts
844
1268
  function renderImage(ctx, node) {
@@ -926,15 +1350,55 @@ function renderImage(ctx, node) {
926
1350
  }
927
1351
 
928
1352
  //#endregion
929
- //#region src/compat/DOMMatrix.ts
930
- const DOMMatrixCompat = (() => {
931
- if (typeof DOMMatrix !== "undefined") return DOMMatrix;
932
- try {
933
- return require("@napi-rs/canvas").DOMMatrix;
934
- } catch {
935
- throw new Error("DOMMatrix is not available. In Node.js, install @napi-rs/canvas.");
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;
936
1400
  }
937
- })();
1401
+ }
938
1402
 
939
1403
  //#endregion
940
1404
  //#region src/compat/Path2D.ts
@@ -1011,8 +1475,14 @@ function applyTransform(base, transform) {
1011
1475
  }
1012
1476
  if (transform.scale !== void 0) if (typeof transform.scale === "number") result = result.scale(transform.scale);
1013
1477
  else result = result.scale(transform.scale[0], transform.scale[1]);
1014
- if (transform.skewX !== void 0) result = result.skewX(transform.skewX);
1015
- if (transform.skewY !== void 0) result = result.skewY(transform.skewY);
1478
+ if (transform.skewX !== void 0) {
1479
+ const degrees = transform.skewX * 180 / Math.PI;
1480
+ result = result.skewX(degrees);
1481
+ }
1482
+ if (transform.skewY !== void 0) {
1483
+ const degrees = transform.skewY * 180 / Math.PI;
1484
+ result = result.skewY(degrees);
1485
+ }
1016
1486
  return result;
1017
1487
  }
1018
1488
  function applyStroke(ctx, stroke, bounds) {
@@ -1248,6 +1718,82 @@ function renderText(ctx, node) {
1248
1718
  if (element.shadow) clearShadow$1(ctx);
1249
1719
  }
1250
1720
 
1721
+ //#endregion
1722
+ //#region src/render/components/transform.ts
1723
+ /**
1724
+ * 解析 Transform 值为 DOMMatrix
1725
+ * 支持三种格式:
1726
+ * - 数组: [a, b, c, d, e, f]
1727
+ * - DOMMatrix2DInit 对象: { a, b, c, d, e, f, ... }
1728
+ * - 简易对象: { translate, rotate, scale, skewX, skewY }
1729
+ */
1730
+ function parseTransformValue(transform) {
1731
+ if (transform === void 0) return new DOMMatrixCompat();
1732
+ if (Array.isArray(transform)) return new DOMMatrixCompat(transform);
1733
+ const hasDOMMatrixInit = "a" in transform || "b" in transform || "c" in transform || "d" in transform || "e" in transform || "f" in transform;
1734
+ const hasSimpleTransform = "translate" in transform || "rotate" in transform || "scale" in transform || "skewX" in transform || "skewY" in transform;
1735
+ if (hasDOMMatrixInit && !hasSimpleTransform) {
1736
+ const init = transform;
1737
+ return new DOMMatrixCompat([
1738
+ init.a ?? 1,
1739
+ init.b ?? 0,
1740
+ init.c ?? 0,
1741
+ init.d ?? 1,
1742
+ init.e ?? 0,
1743
+ init.f ?? 0
1744
+ ]);
1745
+ }
1746
+ const simpleObj = transform;
1747
+ let result = new DOMMatrixCompat();
1748
+ if (simpleObj.translate) result = result.translate(simpleObj.translate[0], simpleObj.translate[1]);
1749
+ if (simpleObj.rotate !== void 0) if (typeof simpleObj.rotate === "number") result = result.rotate(simpleObj.rotate);
1750
+ else {
1751
+ const [angle, cx, cy] = simpleObj.rotate;
1752
+ result = result.translate(cx, cy).rotate(angle).translate(-cx, -cy);
1753
+ }
1754
+ if (simpleObj.scale !== void 0) if (typeof simpleObj.scale === "number") result = result.scale(simpleObj.scale);
1755
+ else result = result.scale(simpleObj.scale[0], simpleObj.scale[1]);
1756
+ if (simpleObj.skewX !== void 0) result = result.skewX(simpleObj.skewX);
1757
+ if (simpleObj.skewY !== void 0) result = result.skewY(simpleObj.skewY);
1758
+ return result;
1759
+ }
1760
+ /**
1761
+ * 根据 transformOrigin 属性和子元素尺寸计算实际变换原点坐标
1762
+ */
1763
+ function resolveTransformOrigin(origin, childLayout) {
1764
+ if (origin === void 0) return [0, 0];
1765
+ const [xVal, yVal] = origin;
1766
+ const sizes = [childLayout.width, childLayout.height];
1767
+ const values = [xVal, yVal];
1768
+ const result = [0, 0];
1769
+ for (let i = 0; i < 2; i++) {
1770
+ const val = values[i];
1771
+ if (typeof val === "string") if (val.endsWith("%")) result[i] = parseFloat(val) / 100 * sizes[i];
1772
+ else result[i] = parseFloat(val);
1773
+ else result[i] = val;
1774
+ }
1775
+ return result;
1776
+ }
1777
+ /**
1778
+ * 渲染 Transform 组件及其子元素
1779
+ */
1780
+ function renderTransform(ctx, node) {
1781
+ const element = node.element;
1782
+ const { children } = node;
1783
+ if (!children || children.length === 0) return;
1784
+ const childNode = children[0];
1785
+ const [relativeOx, relativeOy] = resolveTransformOrigin(element.transformOrigin, childNode.layout);
1786
+ const ox = childNode.layout.x + relativeOx;
1787
+ const oy = childNode.layout.y + relativeOy;
1788
+ const targetMatrix = parseTransformValue(element.transform);
1789
+ const finalMatrix = new DOMMatrixCompat().translate(ox, oy).multiply(targetMatrix).translate(-ox, -oy);
1790
+ ctx.save();
1791
+ const composedTransform = ctx.getTransform().multiply(finalMatrix);
1792
+ ctx.setTransform(composedTransform);
1793
+ renderNode(ctx, childNode);
1794
+ ctx.restore();
1795
+ }
1796
+
1251
1797
  //#endregion
1252
1798
  //#region src/render/index.ts
1253
1799
  function renderNode(ctx, node) {
@@ -1270,12 +1816,21 @@ function renderNode(ctx, node) {
1270
1816
  case "text":
1271
1817
  renderText(ctx, node);
1272
1818
  break;
1819
+ case "richtext":
1820
+ renderRichText(ctx, node);
1821
+ break;
1273
1822
  case "image":
1274
1823
  renderImage(ctx, node);
1275
1824
  break;
1276
1825
  case "svg":
1277
1826
  renderSvg(ctx, node);
1278
1827
  break;
1828
+ case "transform":
1829
+ renderTransform(ctx, node);
1830
+ break;
1831
+ case "customdraw":
1832
+ renderCustomDraw(ctx, node);
1833
+ break;
1279
1834
  }
1280
1835
  }
1281
1836