@codehz/draw-call 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -2
- package/canvas.d.cts +62 -6
- package/canvas.d.mts +62 -6
- package/examples/customdraw-basic.ts +269 -0
- package/examples/customdraw.ts +339 -0
- package/examples/image-smoothing.ts +46 -0
- package/examples/transform.ts +437 -0
- package/index.cjs +22 -0
- package/index.d.cts +7 -14
- package/index.d.mts +7 -14
- package/index.mjs +21 -1
- package/node.cjs +2 -0
- package/node.mjs +2 -0
- package/package.json +1 -1
- package/render.cjs +325 -13
- package/render.mjs +325 -13
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
|
/**
|
|
@@ -550,6 +567,17 @@ function measureTextSize(element, ctx, availableWidth) {
|
|
|
550
567
|
};
|
|
551
568
|
}
|
|
552
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
|
+
|
|
553
581
|
//#endregion
|
|
554
582
|
//#region src/layout/components/index.ts
|
|
555
583
|
/**
|
|
@@ -563,6 +591,8 @@ function measureIntrinsicSize(element, ctx, availableWidth) {
|
|
|
563
591
|
case "stack": return measureStackSize(element, ctx, availableWidth, measureIntrinsicSize);
|
|
564
592
|
case "image": return measureImageSize(element, ctx, availableWidth);
|
|
565
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);
|
|
566
596
|
default: return {
|
|
567
597
|
width: 0,
|
|
568
598
|
height: 0
|
|
@@ -920,6 +950,54 @@ function computeLayout(element, ctx, constraints, x = 0, y = 0) {
|
|
|
920
950
|
}
|
|
921
951
|
if (isReverse) node.children.reverse();
|
|
922
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
|
+
}
|
|
923
1001
|
}
|
|
924
1002
|
return node;
|
|
925
1003
|
}
|
|
@@ -1028,6 +1106,163 @@ function renderBox(ctx, node) {
|
|
|
1028
1106
|
if (element.opacity !== void 0 && element.opacity < 1) ctx.globalAlpha = 1;
|
|
1029
1107
|
}
|
|
1030
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
|
+
|
|
1031
1266
|
//#endregion
|
|
1032
1267
|
//#region src/render/components/image.ts
|
|
1033
1268
|
function renderImage(ctx, node) {
|
|
@@ -1165,17 +1400,6 @@ function renderRichText(ctx, node) {
|
|
|
1165
1400
|
}
|
|
1166
1401
|
}
|
|
1167
1402
|
|
|
1168
|
-
//#endregion
|
|
1169
|
-
//#region src/compat/DOMMatrix.ts
|
|
1170
|
-
const DOMMatrixCompat = (() => {
|
|
1171
|
-
if (typeof DOMMatrix !== "undefined") return DOMMatrix;
|
|
1172
|
-
try {
|
|
1173
|
-
return require("@napi-rs/canvas").DOMMatrix;
|
|
1174
|
-
} catch {
|
|
1175
|
-
throw new Error("DOMMatrix is not available. In Node.js, install @napi-rs/canvas.");
|
|
1176
|
-
}
|
|
1177
|
-
})();
|
|
1178
|
-
|
|
1179
1403
|
//#endregion
|
|
1180
1404
|
//#region src/compat/Path2D.ts
|
|
1181
1405
|
const Path2DCompat = (() => {
|
|
@@ -1251,8 +1475,14 @@ function applyTransform(base, transform) {
|
|
|
1251
1475
|
}
|
|
1252
1476
|
if (transform.scale !== void 0) if (typeof transform.scale === "number") result = result.scale(transform.scale);
|
|
1253
1477
|
else result = result.scale(transform.scale[0], transform.scale[1]);
|
|
1254
|
-
if (transform.skewX !== void 0)
|
|
1255
|
-
|
|
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
|
+
}
|
|
1256
1486
|
return result;
|
|
1257
1487
|
}
|
|
1258
1488
|
function applyStroke(ctx, stroke, bounds) {
|
|
@@ -1488,6 +1718,82 @@ function renderText(ctx, node) {
|
|
|
1488
1718
|
if (element.shadow) clearShadow$1(ctx);
|
|
1489
1719
|
}
|
|
1490
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
|
+
|
|
1491
1797
|
//#endregion
|
|
1492
1798
|
//#region src/render/index.ts
|
|
1493
1799
|
function renderNode(ctx, node) {
|
|
@@ -1519,6 +1825,12 @@ function renderNode(ctx, node) {
|
|
|
1519
1825
|
case "svg":
|
|
1520
1826
|
renderSvg(ctx, node);
|
|
1521
1827
|
break;
|
|
1828
|
+
case "transform":
|
|
1829
|
+
renderTransform(ctx, node);
|
|
1830
|
+
break;
|
|
1831
|
+
case "customdraw":
|
|
1832
|
+
renderCustomDraw(ctx, node);
|
|
1833
|
+
break;
|
|
1522
1834
|
}
|
|
1523
1835
|
}
|
|
1524
1836
|
|