@codehz/draw-call 0.5.2 → 0.6.0

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.
@@ -1,10 +1,8 @@
1
1
  import { DOMMatrix, Path2D, createCanvas as createCanvas$1 } from "@napi-rs/canvas";
2
-
3
2
  //#region src/compat/index.ts
4
3
  function createRawCanvas(width, height) {
5
4
  return createCanvas$1(width, height);
6
5
  }
7
-
8
6
  //#endregion
9
7
  //#region src/types/base.ts
10
8
  function linearGradient(angle, ...stops) {
@@ -75,7 +73,6 @@ function normalizeBorderRadius(value) {
75
73
  ];
76
74
  return value;
77
75
  }
78
-
79
76
  //#endregion
80
77
  //#region src/layout/components/box.ts
81
78
  /**
@@ -188,7 +185,6 @@ function measureBoxSize(element, ctx, availableWidth, measureChild) {
188
185
  height: typeof element.height === "number" ? element.height : intrinsicHeight
189
186
  };
190
187
  }
191
-
192
188
  //#endregion
193
189
  //#region src/layout/components/customDraw.ts
194
190
  /**
@@ -205,7 +201,6 @@ function measureCustomDrawSize(element, ctx, availableWidth, measureChild) {
205
201
  height: 0
206
202
  };
207
203
  }
208
-
209
204
  //#endregion
210
205
  //#region src/layout/components/image.ts
211
206
  /**
@@ -231,7 +226,6 @@ function measureImageSize(element, _ctx, _availableWidth) {
231
226
  height: 0
232
227
  };
233
228
  }
234
-
235
229
  //#endregion
236
230
  //#region src/layout/components/richtext.ts
237
231
  /**
@@ -399,7 +393,6 @@ function wrapRichText(ctx, spans, maxWidth, lineHeightScale = 1.2, elementStyle
399
393
  }];
400
394
  return lines;
401
395
  }
402
-
403
396
  //#endregion
404
397
  //#region src/layout/components/stack.ts
405
398
  /**
@@ -436,7 +429,6 @@ function measureStackSize(element, ctx, availableWidth, measureChild) {
436
429
  height: typeof element.height === "number" ? element.height : intrinsicHeight
437
430
  };
438
431
  }
439
-
440
432
  //#endregion
441
433
  //#region src/layout/components/svg.ts
442
434
  /**
@@ -470,7 +462,6 @@ function measureSvgSize(element, _ctx, _availableWidth) {
470
462
  height: 0
471
463
  };
472
464
  }
473
-
474
465
  //#endregion
475
466
  //#region src/render/utils/font.ts
476
467
  /**
@@ -481,23 +472,42 @@ function measureSvgSize(element, _ctx, _availableWidth) {
481
472
  function buildFontString(font) {
482
473
  return `${font.style ?? "normal"} ${font.weight ?? "normal"} ${font.size ?? 16}px ${font.family ?? "sans-serif"}`;
483
474
  }
484
-
485
475
  //#endregion
486
476
  //#region src/layout/utils/measure.ts
477
+ const MEASURE_CACHE_LIMIT = 256;
487
478
  function createCanvasMeasureContext(ctx) {
479
+ const cache = /* @__PURE__ */ new Map();
480
+ let lastFontString = null;
488
481
  return { measureText(text, font) {
489
- ctx.font = buildFontString(font);
490
- ctx.textBaseline = "middle";
482
+ const fontString = buildFontString(font);
483
+ const key = fontString + "\0" + text;
484
+ const hit = cache.get(key);
485
+ if (hit !== void 0) {
486
+ cache.delete(key);
487
+ cache.set(key, hit);
488
+ return hit;
489
+ }
490
+ if (fontString !== lastFontString) {
491
+ ctx.font = fontString;
492
+ ctx.textBaseline = "middle";
493
+ lastFontString = fontString;
494
+ }
491
495
  const metrics = ctx.measureText(text);
492
496
  const height = metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent;
493
497
  const fontSize = font.size || 16;
494
- return {
498
+ const result = {
495
499
  width: metrics.width,
496
500
  height: height || fontSize,
497
501
  offset: (metrics.actualBoundingBoxAscent - metrics.actualBoundingBoxDescent) / 2,
498
502
  ascent: metrics.actualBoundingBoxAscent,
499
503
  descent: metrics.actualBoundingBoxDescent
500
504
  };
505
+ if (cache.size >= MEASURE_CACHE_LIMIT) {
506
+ const oldest = cache.keys().next().value;
507
+ if (oldest !== void 0) cache.delete(oldest);
508
+ }
509
+ cache.set(key, result);
510
+ return result;
501
511
  } };
502
512
  }
503
513
  function wrapText(ctx, text, maxWidth, font) {
@@ -573,7 +583,6 @@ function truncateText(ctx, text, maxWidth, font, ellipsis = "...") {
573
583
  offset
574
584
  };
575
585
  }
576
-
577
586
  //#endregion
578
587
  //#region src/layout/components/text.ts
579
588
  /**
@@ -599,7 +608,6 @@ function measureTextSize(element, ctx, availableWidth) {
599
608
  height: Math.max(height, lineHeightPx)
600
609
  };
601
610
  }
602
-
603
611
  //#endregion
604
612
  //#region src/layout/components/transform.ts
605
613
  /**
@@ -610,7 +618,6 @@ function measureTextSize(element, ctx, availableWidth) {
610
618
  function measureTransformSize(element, ctx, availableWidth, measureIntrinsicSize) {
611
619
  return measureIntrinsicSize(element.children, ctx, availableWidth);
612
620
  }
613
-
614
621
  //#endregion
615
622
  //#region src/layout/components/index.ts
616
623
  /**
@@ -632,7 +639,6 @@ function measureIntrinsicSize(element, ctx, availableWidth) {
632
639
  };
633
640
  }
634
641
  }
635
-
636
642
  //#endregion
637
643
  //#region src/layout/utils/offset.ts
638
644
  /**
@@ -645,7 +651,6 @@ function applyOffset(node, dx, dy) {
645
651
  node.layout.contentY += dy;
646
652
  for (const child of node.children) applyOffset(child, dx, dy);
647
653
  }
648
-
649
654
  //#endregion
650
655
  //#region src/types/layout.ts
651
656
  function resolveSize(size, available, auto) {
@@ -660,7 +665,6 @@ function sizeNeedsParent(size) {
660
665
  if (typeof size === "string" && size.endsWith("%")) return true;
661
666
  return false;
662
667
  }
663
-
664
668
  //#endregion
665
669
  //#region src/layout/engine.ts
666
670
  /**
@@ -780,7 +784,14 @@ function computeLayoutImpl(element, ctx, constraints, x = 0, y = 0) {
780
784
  }
781
785
  if (layoutElement.type === "richtext") {
782
786
  const lineHeight = layoutElement.lineHeight ?? 1.2;
783
- let lines = wrapRichText(ctx, layoutElement.spans, contentWidth, lineHeight);
787
+ const elementStyle = {
788
+ font: layoutElement.font,
789
+ color: layoutElement.color,
790
+ background: layoutElement.background,
791
+ underline: layoutElement.underline,
792
+ strikethrough: layoutElement.strikethrough
793
+ };
794
+ let lines = wrapRichText(ctx, layoutElement.spans, contentWidth, lineHeight, elementStyle);
784
795
  if (layoutElement.maxLines && lines.length > layoutElement.maxLines) {
785
796
  lines = lines.slice(0, layoutElement.maxLines);
786
797
  if (layoutElement.ellipsis && lines.length > 0) {
@@ -1078,7 +1089,6 @@ function computeLayoutImpl(element, ctx, constraints, x = 0, y = 0) {
1078
1089
  }
1079
1090
  return node;
1080
1091
  }
1081
-
1082
1092
  //#endregion
1083
1093
  //#region src/render/utils/colors.ts
1084
1094
  function isGradientDescriptor$1(color) {
@@ -1114,7 +1124,6 @@ function resolveColor$1(ctx, color, x, y, width, height) {
1114
1124
  if (isGradientDescriptor$1(color)) return resolveGradient$1(ctx, color, x, y, width, height);
1115
1125
  return color;
1116
1126
  }
1117
-
1118
1127
  //#endregion
1119
1128
  //#region src/render/utils/shadows.ts
1120
1129
  function applyShadow$1(ctx, shadow) {
@@ -1136,7 +1145,6 @@ function clearShadow$1(ctx) {
1136
1145
  ctx.shadowBlur = 0;
1137
1146
  ctx.shadowColor = "transparent";
1138
1147
  }
1139
-
1140
1148
  //#endregion
1141
1149
  //#region src/render/utils/shapes.ts
1142
1150
  function roundRectPath(ctx, x, y, width, height, radius) {
@@ -1153,7 +1161,6 @@ function roundRectPath(ctx, x, y, width, height, radius) {
1153
1161
  ctx.quadraticCurveTo(x, y, x + tl, y);
1154
1162
  ctx.closePath();
1155
1163
  }
1156
-
1157
1164
  //#endregion
1158
1165
  //#region src/render/components/box.ts
1159
1166
  function renderBox(ctx, node) {
@@ -1182,125 +1189,90 @@ function renderBox(ctx, node) {
1182
1189
  }
1183
1190
  if (element.opacity !== void 0 && element.opacity < 1) ctx.globalAlpha = 1;
1184
1191
  }
1185
-
1186
1192
  //#endregion
1187
- //#region src/render/components/ProxiedCanvasContext.ts
1188
- /**
1189
- * ProxiedCanvasContext - Canvas 上下文代理类
1190
- *
1191
- * 该类提供对真实 CanvasRenderingContext2D 的代理,有以下功能:
1192
- * 1. 管理 save/restore 的平衡(计数器)
1193
- * 2. 追踪相对变换而不是绝对变换
1194
- * 3. 在析构时自动恢复所有未恢复的状态
1195
- * 4. 转发所有其他 Canvas API 调用
1196
- */
1197
- var ProxiedCanvasContext = class {
1198
- /**
1199
- * 真实的 Canvas 上下文
1200
- */
1201
- ctx;
1202
- /**
1203
- * 基础变换矩阵(初始化时设置,保持不变)
1204
- */
1193
+ //#region src/render/components/CustomDrawContext.ts
1194
+ function cloneMatrix(matrix) {
1195
+ return new DOMMatrix([
1196
+ matrix.a,
1197
+ matrix.b,
1198
+ matrix.c,
1199
+ matrix.d,
1200
+ matrix.e,
1201
+ matrix.f
1202
+ ]);
1203
+ }
1204
+ function toRelativeMatrix(transform) {
1205
+ if (transform === void 0) return new DOMMatrix();
1206
+ if (transform instanceof DOMMatrix) return cloneMatrix(transform);
1207
+ return new DOMMatrix(transform);
1208
+ }
1209
+ var ManagedCustomDrawContext = class {
1210
+ canvas;
1205
1211
  baseTransform;
1206
- /**
1207
- * 相对变换矩阵(用户通过 setTransform 设置)
1208
- */
1209
1212
  relativeTransform;
1210
- /**
1211
- * save/restore 计数器
1212
- */
1213
+ transformStack = [];
1213
1214
  saveCount = 0;
1214
- /**
1215
- * 构造函数
1216
- * @param ctx 真实的 CanvasRenderingContext2D
1217
- * @param baseTransform 初始的基础变换矩阵
1218
- */
1219
- constructor(ctx, baseTransform) {
1220
- this.ctx = ctx;
1221
- this.baseTransform = baseTransform;
1215
+ constructor(canvas, baseTransform) {
1216
+ this.canvas = canvas;
1217
+ this.baseTransform = cloneMatrix(baseTransform);
1222
1218
  this.relativeTransform = new DOMMatrix();
1223
1219
  }
1224
- /**
1225
- * save() - 保存当前状态并增加计数
1226
- */
1227
1220
  save() {
1228
1221
  this.saveCount++;
1229
- this.ctx.save();
1222
+ this.transformStack.push(cloneMatrix(this.relativeTransform));
1223
+ this.canvas.save();
1230
1224
  }
1231
- /**
1232
- * restore() - 恢复上一个状态并减少计数
1233
- */
1234
1225
  restore() {
1235
- if (this.saveCount > 0) {
1236
- this.saveCount--;
1237
- this.ctx.restore();
1238
- }
1226
+ if (this.saveCount === 0) return;
1227
+ this.saveCount--;
1228
+ this.relativeTransform = this.transformStack.pop() ?? new DOMMatrix();
1229
+ this.canvas.restore();
1230
+ this.applyRelativeTransform();
1239
1231
  }
1240
- /**
1241
- * setTransform() - 设置相对变换
1242
- */
1243
- setTransform(...args) {
1244
- let matrix;
1245
- if (args.length === 1 && args[0] instanceof DOMMatrix) matrix = args[0];
1246
- else if (args.length === 6) matrix = new DOMMatrix([
1247
- args[0],
1248
- args[1],
1249
- args[2],
1250
- args[3],
1251
- args[4],
1252
- args[5]
1253
- ]);
1254
- else return;
1255
- this.relativeTransform = matrix;
1256
- const actualTransform = this.baseTransform.multiply(matrix);
1257
- this.ctx.setTransform(actualTransform);
1258
- }
1259
- /**
1260
- * getTransform() - 返回相对变换(而不是绝对变换)
1261
- */
1262
1232
  getTransform() {
1263
- return this.relativeTransform;
1233
+ return cloneMatrix(this.relativeTransform);
1234
+ }
1235
+ setTransform(transform) {
1236
+ this.relativeTransform = toRelativeMatrix(transform);
1237
+ this.applyRelativeTransform();
1238
+ }
1239
+ resetTransform() {
1240
+ this.relativeTransform = new DOMMatrix();
1241
+ this.applyRelativeTransform();
1242
+ }
1243
+ translate(x, y) {
1244
+ this.relativeTransform = this.relativeTransform.translate(x, y);
1245
+ this.applyRelativeTransform();
1246
+ }
1247
+ rotate(angle) {
1248
+ this.relativeTransform = this.relativeTransform.rotate(angle * 180 / Math.PI);
1249
+ this.applyRelativeTransform();
1250
+ }
1251
+ scale(x, y) {
1252
+ this.relativeTransform = this.relativeTransform.scale(x, y ?? x);
1253
+ this.applyRelativeTransform();
1254
+ }
1255
+ transform(a, b, c, d, e, f) {
1256
+ this.relativeTransform = this.relativeTransform.multiply(new DOMMatrix([
1257
+ a,
1258
+ b,
1259
+ c,
1260
+ d,
1261
+ e,
1262
+ f
1263
+ ]));
1264
+ this.applyRelativeTransform();
1264
1265
  }
1265
- /**
1266
- * 析构函数级的清理 - 自动恢复所有未恢复的 save
1267
- */
1268
1266
  destroy() {
1269
- while (this.saveCount > 0) {
1270
- console.log("destroy restore", this.saveCount);
1271
- this.saveCount--;
1272
- this.ctx.restore();
1273
- }
1267
+ while (this.saveCount > 0) this.restore();
1268
+ }
1269
+ applyRelativeTransform() {
1270
+ this.canvas.setTransform(this.baseTransform.multiply(this.relativeTransform));
1274
1271
  }
1275
1272
  };
1276
- function createProxiedCanvasContext(ctx, baseTransform) {
1277
- const proxy = new ProxiedCanvasContext(ctx, baseTransform);
1278
- return new Proxy(proxy, {
1279
- get(target, prop, receiver) {
1280
- if (prop === "save" || prop === "restore" || prop === "setTransform" || prop === "getTransform" || prop === "destroy") return Reflect.get(target, prop, receiver).bind(proxy);
1281
- const ownValue = Reflect.get(target, prop, receiver);
1282
- if (ownValue !== void 0) return ownValue;
1283
- const contextValue = target.ctx[prop];
1284
- if (typeof contextValue === "function") return contextValue.bind(target.ctx);
1285
- return contextValue;
1286
- },
1287
- set(target, prop, value, _receiver) {
1288
- target.ctx[prop] = value;
1289
- return true;
1290
- },
1291
- has(target, prop) {
1292
- if (prop === "save" || prop === "restore" || prop === "setTransform" || prop === "getTransform" || prop === "destroy") return true;
1293
- return prop in target.ctx;
1294
- },
1295
- ownKeys(target) {
1296
- return Reflect.ownKeys(target.ctx);
1297
- },
1298
- getOwnPropertyDescriptor(target, prop) {
1299
- return Reflect.getOwnPropertyDescriptor(target.ctx, prop);
1300
- }
1301
- });
1273
+ function createCustomDrawContext(canvas, baseTransform) {
1274
+ return new ManagedCustomDrawContext(canvas, baseTransform);
1302
1275
  }
1303
-
1304
1276
  //#endregion
1305
1277
  //#region src/render/components/customDraw.ts
1306
1278
  /**
@@ -1311,7 +1283,7 @@ function renderCustomDraw(ctx, node) {
1311
1283
  const element = node.element;
1312
1284
  ctx.save();
1313
1285
  ctx.translate(node.layout.x, node.layout.y);
1314
- const proxyCtx = createProxiedCanvasContext(ctx, ctx.getTransform());
1286
+ const customCtx = createCustomDrawContext(ctx, ctx.getTransform());
1315
1287
  const inner = () => {
1316
1288
  if (node.children && node.children.length > 0) {
1317
1289
  ctx.save();
@@ -1320,15 +1292,14 @@ function renderCustomDraw(ctx, node) {
1320
1292
  ctx.restore();
1321
1293
  }
1322
1294
  };
1323
- element.draw(proxyCtx, {
1295
+ element.draw(customCtx, {
1324
1296
  inner,
1325
1297
  width: node.layout.contentWidth,
1326
1298
  height: node.layout.contentHeight
1327
1299
  });
1328
- proxyCtx.destroy();
1300
+ customCtx.destroy();
1329
1301
  ctx.restore();
1330
1302
  }
1331
-
1332
1303
  //#endregion
1333
1304
  //#region src/render/components/image.ts
1334
1305
  function renderImage(ctx, node) {
@@ -1414,7 +1385,6 @@ function renderImage(ctx, node) {
1414
1385
  }
1415
1386
  if (element.opacity !== void 0 && element.opacity < 1) ctx.globalAlpha = 1;
1416
1387
  }
1417
-
1418
1388
  //#endregion
1419
1389
  //#region src/render/components/richtext.ts
1420
1390
  function renderRichText(ctx, node) {
@@ -1465,7 +1435,6 @@ function renderRichText(ctx, node) {
1465
1435
  currentY += line.height;
1466
1436
  }
1467
1437
  }
1468
-
1469
1438
  //#endregion
1470
1439
  //#region src/render/components/svg.ts
1471
1440
  function isGradientDescriptor(color) {
@@ -1737,7 +1706,6 @@ function renderSvg(ctx, node) {
1737
1706
  for (const child of element.children) renderSvgChild(ctx, child, transform, bounds, baseTransform);
1738
1707
  ctx.restore();
1739
1708
  }
1740
-
1741
1709
  //#endregion
1742
1710
  //#region src/render/components/text.ts
1743
1711
  function renderText(ctx, node) {
@@ -1772,7 +1740,6 @@ function renderText(ctx, node) {
1772
1740
  }
1773
1741
  if (element.shadow) clearShadow$1(ctx);
1774
1742
  }
1775
-
1776
1743
  //#endregion
1777
1744
  //#region src/render/components/transform.ts
1778
1745
  /**
@@ -1848,7 +1815,6 @@ function renderTransform(ctx, node) {
1848
1815
  renderNode(ctx, childNode);
1849
1816
  ctx.restore();
1850
1817
  }
1851
-
1852
1818
  //#endregion
1853
1819
  //#region src/render/index.ts
1854
1820
  function renderNode(ctx, node) {
@@ -1888,7 +1854,6 @@ function renderNode(ctx, node) {
1888
1854
  break;
1889
1855
  }
1890
1856
  }
1891
-
1892
1857
  //#endregion
1893
1858
  //#region src/canvas.ts
1894
1859
  /**
@@ -1958,7 +1923,6 @@ function createCanvas(options) {
1958
1923
  }
1959
1924
  };
1960
1925
  }
1961
-
1962
1926
  //#endregion
1963
1927
  //#region src/components/Box.ts
1964
1928
  function Box(props) {
@@ -1967,7 +1931,6 @@ function Box(props) {
1967
1931
  ...props
1968
1932
  };
1969
1933
  }
1970
-
1971
1934
  //#endregion
1972
1935
  //#region src/components/CustomDraw.ts
1973
1936
  function CustomDraw(props) {
@@ -1976,7 +1939,6 @@ function CustomDraw(props) {
1976
1939
  ...props
1977
1940
  };
1978
1941
  }
1979
-
1980
1942
  //#endregion
1981
1943
  //#region src/components/Image.ts
1982
1944
  function Image(props) {
@@ -1985,7 +1947,6 @@ function Image(props) {
1985
1947
  ...props
1986
1948
  };
1987
1949
  }
1988
-
1989
1950
  //#endregion
1990
1951
  //#region src/components/RichText.ts
1991
1952
  function RichText(props) {
@@ -1994,7 +1955,6 @@ function RichText(props) {
1994
1955
  ...props
1995
1956
  };
1996
1957
  }
1997
-
1998
1958
  //#endregion
1999
1959
  //#region src/components/Stack.ts
2000
1960
  function Stack(props) {
@@ -2003,7 +1963,6 @@ function Stack(props) {
2003
1963
  ...props
2004
1964
  };
2005
1965
  }
2006
-
2007
1966
  //#endregion
2008
1967
  //#region src/components/Svg.ts
2009
1968
  function Svg(props) {
@@ -2050,7 +2009,6 @@ const svg = {
2050
2009
  ...props
2051
2010
  })
2052
2011
  };
2053
-
2054
2012
  //#endregion
2055
2013
  //#region src/components/Text.ts
2056
2014
  function Text(props) {
@@ -2059,7 +2017,6 @@ function Text(props) {
2059
2017
  ...props
2060
2018
  };
2061
2019
  }
2062
-
2063
2020
  //#endregion
2064
2021
  //#region src/components/Transform.ts
2065
2022
  function Transform(props) {
@@ -2068,7 +2025,6 @@ function Transform(props) {
2068
2025
  ...props
2069
2026
  };
2070
2027
  }
2071
-
2072
2028
  //#endregion
2073
2029
  //#region src/layout/utils/print.ts
2074
2030
  /**
@@ -2128,6 +2084,5 @@ function printLayout(node) {
2128
2084
  function layoutToString(node, _indent = " ") {
2129
2085
  return printLayoutToString(node, "", true).join("\n");
2130
2086
  }
2131
-
2132
2087
  //#endregion
2133
- export { Box, CustomDraw, Image, RichText, Stack, Svg, Text, Transform, computeLayout, createCanvas, createCanvasMeasureContext, layoutToString, linearGradient, printLayout, radialGradient, svg };
2088
+ export { Box, CustomDraw, Image, RichText, Stack, Svg, Text, Transform, computeLayout, createCanvas, createCanvasMeasureContext, layoutToString, linearGradient, printLayout, radialGradient, svg };
package/examples/card.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  * 示例:使用 draw-call 绘制一个卡片
3
3
  * 运行: bun examples/card.ts
4
4
  */
5
- import { Box, createCanvas, linearGradient, printLayout, Svg, svg, Text } from "@codehz/draw-call";
5
+ import { Box, createCanvas, linearGradient, printLayout, Svg, svg, Text } from "@/index";
6
6
  import { GlobalFonts } from "@napi-rs/canvas";
7
7
  import { fileURLToPath } from "bun";
8
8
 
@@ -3,7 +3,7 @@
3
3
  * 展示如何使用 CustomDraw 进行简单的自定义绘制
4
4
  * 运行: bun examples/customdraw-basic.ts
5
5
  */
6
- import { Box, createCanvas, CustomDraw, printLayout, Text } from "@codehz/draw-call";
6
+ import { Box, createCanvas, CustomDraw, printLayout, Text } from "@/index";
7
7
  import { GlobalFonts } from "@napi-rs/canvas";
8
8
  import { fileURLToPath } from "bun";
9
9
 
@@ -20,13 +20,14 @@ const SimpleRect = CustomDraw({
20
20
  width: 150,
21
21
  height: 80,
22
22
  draw(ctx, { width, height }) {
23
- ctx.fillStyle = "#667eea";
24
- ctx.fillRect(0, 0, width, height);
25
- ctx.fillStyle = "#000000";
26
- ctx.font = "bold 16px sans-serif";
27
- ctx.textAlign = "center";
28
- ctx.textBaseline = "middle";
29
- ctx.fillText("Simple Rect", width / 2, height / 2);
23
+ const canvas = ctx.canvas;
24
+ canvas.fillStyle = "#667eea";
25
+ canvas.fillRect(0, 0, width, height);
26
+ canvas.fillStyle = "#000000";
27
+ canvas.font = "bold 16px sans-serif";
28
+ canvas.textAlign = "center";
29
+ canvas.textBaseline = "middle";
30
+ canvas.fillText("Simple Rect", width / 2, height / 2);
30
31
  },
31
32
  });
32
33
 
@@ -35,20 +36,21 @@ const RotatedRect = CustomDraw({
35
36
  width: 150,
36
37
  height: 80,
37
38
  draw(ctx, { width, height }) {
39
+ const canvas = ctx.canvas;
38
40
  // save/restore 会自动平衡,即使不显式调用也能正确恢复
39
41
  ctx.save();
40
42
  ctx.translate(width / 2, height / 2);
41
43
  ctx.rotate((Math.PI / 4) * 0.3); // 15 度
42
- ctx.fillStyle = "#764ba2";
43
- ctx.fillRect(-width / 2, -height / 2, width, height);
44
+ canvas.fillStyle = "#764ba2";
45
+ canvas.fillRect(-width / 2, -height / 2, width, height);
44
46
  ctx.restore();
45
47
 
46
48
  // 恢复后可以正常绘制
47
- ctx.fillStyle = "#000000";
48
- ctx.font = "16px sans-serif";
49
- ctx.textAlign = "center";
50
- ctx.textBaseline = "middle";
51
- ctx.fillText("Rotated", width / 2, height / 2);
49
+ canvas.fillStyle = "#000000";
50
+ canvas.font = "16px sans-serif";
51
+ canvas.textAlign = "center";
52
+ canvas.textBaseline = "middle";
53
+ canvas.fillText("Rotated", width / 2, height / 2);
52
54
  },
53
55
  });
54
56
 
@@ -57,24 +59,25 @@ const CircleProgress = CustomDraw({
57
59
  width: 140,
58
60
  height: 140,
59
61
  draw(ctx, { width, height, inner }) {
62
+ const canvas = ctx.canvas;
60
63
  const centerX = width / 2;
61
64
  const centerY = height / 2;
62
65
  const radius = Math.min(width, height) / 2 - 8;
63
66
 
64
67
  // 绘制背景圆
65
- ctx.strokeStyle = "rgba(0, 0, 0, 0.1)";
66
- ctx.lineWidth = 8;
67
- ctx.beginPath();
68
- ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
69
- ctx.stroke();
68
+ canvas.strokeStyle = "rgba(0, 0, 0, 0.1)";
69
+ canvas.lineWidth = 8;
70
+ canvas.beginPath();
71
+ canvas.arc(centerX, centerY, radius, 0, Math.PI * 2);
72
+ canvas.stroke();
70
73
 
71
74
  // 绘制进度圆(65% 完成)
72
75
  const percentage = 65;
73
- ctx.strokeStyle = "#ff6b6b";
74
- ctx.lineCap = "round";
75
- ctx.beginPath();
76
- ctx.arc(centerX, centerY, radius, -Math.PI / 2, -Math.PI / 2 + (percentage / 100) * Math.PI * 2);
77
- ctx.stroke();
76
+ canvas.strokeStyle = "#ff6b6b";
77
+ canvas.lineCap = "round";
78
+ canvas.beginPath();
79
+ canvas.arc(centerX, centerY, radius, -Math.PI / 2, -Math.PI / 2 + (percentage / 100) * Math.PI * 2);
80
+ canvas.stroke();
78
81
 
79
82
  // 调用 inner() 在中央渲染子元素(百分比文本)
80
83
  inner?.();
@@ -99,14 +102,15 @@ const StarShape = CustomDraw({
99
102
  width: 150,
100
103
  height: 150,
101
104
  draw(ctx, { width, height }) {
105
+ const canvas = ctx.canvas;
102
106
  const centerX = width / 2;
103
107
  const centerY = height / 2;
104
108
  const points = 5;
105
109
  const outerRadius = Math.min(width, height) / 2 - 5;
106
110
  const innerRadius = outerRadius * 0.4;
107
111
 
108
- ctx.fillStyle = "#ffd93d";
109
- ctx.beginPath();
112
+ canvas.fillStyle = "#ffd93d";
113
+ canvas.beginPath();
110
114
 
111
115
  for (let i = 0; i < points * 2; i++) {
112
116
  const radius = i % 2 === 0 ? outerRadius : innerRadius;
@@ -115,19 +119,19 @@ const StarShape = CustomDraw({
115
119
  const y = centerY + radius * Math.sin(angle);
116
120
 
117
121
  if (i === 0) {
118
- ctx.moveTo(x, y);
122
+ canvas.moveTo(x, y);
119
123
  } else {
120
- ctx.lineTo(x, y);
124
+ canvas.lineTo(x, y);
121
125
  }
122
126
  }
123
127
 
124
- ctx.closePath();
125
- ctx.fill();
128
+ canvas.closePath();
129
+ canvas.fill();
126
130
 
127
131
  // 描边
128
- ctx.strokeStyle = "#fa8c16";
129
- ctx.lineWidth = 2;
130
- ctx.stroke();
132
+ canvas.strokeStyle = "#fa8c16";
133
+ canvas.lineWidth = 2;
134
+ canvas.stroke();
131
135
  },
132
136
  });
133
137
 
@@ -246,7 +250,7 @@ const layout = canvas.render(
246
250
  }),
247
251
  Text({
248
252
  content:
249
- "CustomDraw 支持直接调用 Canvas API。内部的 save/restore 会自动平衡,transform 也会自动相对处理。使用 inner() 方法可以在自定义绘制中渲染子元素。",
253
+ "CustomDraw 提供受控的 transform/save/restore 能力,并通过 ctx.canvas 暴露原生 Canvas API。使用 inner() 方法可以在自定义绘制中渲染子元素。",
250
254
  font: { size: 12, family: "unifont" },
251
255
  color: "#1890ff",
252
256
  wrap: true,