坐标系与变换矩阵
Canvas 有一套 当前变换矩阵(CTM)。所有传入 lineTo、fillRect、drawImage 的坐标,都先乘 CTM 再写入 backing store。
getTransform() 返回 DOMMatrix(2D 子集:a b c d e f)。这是命中测试、HTML overlay 对齐、导出裁切的唯一可靠来源,不要靠心算一串 translate/rotate/scale。
1. 矩阵语义
[ a c e ] [ x ] [ a*x + c*y + e ]
[ b d f ] × [ y ] = [ b*x + d*y + f ]
[ 0 0 1 ] [ 1 ] [ 1 ]
e, f:平移a, d:缩放(含翻转:a < 0即水平镜像)b, c:旋转/错切
transform(a,b,c,d,e,f) 是 右乘到当前 CTM(再叠一层)。
setTransform(...) 是 替换 CTM。
resetTransform() 等价于 setTransform(1,0,0,1,0,0)。
热路径里优先 setTransform(worldMatrix),避免「以为在单位阵上 scale,其实已经脏了」。
2. 用户空间 ↔ backing store
export function userToSurface(
ctx: CanvasRenderingContext2D,
x: number,
y: number,
): DOMPoint {
return ctx.getTransform().transformPoint(new DOMPoint(x, y));
}
export function surfaceToUser(
ctx: CanvasRenderingContext2D,
x: number,
y: number,
): DOMPoint {
const inverse = ctx.getTransform().inverse();
if (inverse.is2D === false) {
throw new Error("expected 2D matrix");
}
return inverse.transformPoint(new DOMPoint(x, y));
}
不可逆矩阵(缩放到 0)上 inverse() 会得到带 NaN 的矩阵,命中会全体失败。缩放节点时夹一个最小 scale。
3. 指针事件要先换到 canvas 空间
MouseEvent.offsetX 在部分浏览器对 canvas 可用,但缩放、CSS transform、DPR 下不可靠。生产用包围盒:
export function eventToCanvasUser(
canvas: HTMLCanvasElement,
ctx: CanvasRenderingContext2D,
event: PointerEvent,
): { x: number; y: number } {
const rect = canvas.getBoundingClientRect();
const cssX = ((event.clientX - rect.left) / rect.width) * canvas.width;
const cssY = ((event.clientY - rect.top) / rect.height) * canvas.height;
const p = surfaceToUser(ctx, cssX, cssY);
return { x: p.x, y: p.y };
}
这里 canvas.width 是 backing store。若你在 resize 时做了 ctx.scale(dpr, dpr) 且之后所有绘制都在 CSS 像素用户空间,则应把 canvas.width 换成 rect.width 对应的用户空间宽度,或 不要把 dpr 编进 CTM,只放大 backing store,绘制仍用 backing 像素。两种策略选一种,全项目统一。推荐策略见 DPR 与 backing store:backing store × dpr,CTM scale(dpr),业务坐标用 CSS 像素。
4. Path2D 与变换
Path2D 存的是路径自己的坐标。ctx.fill(path) / ctx.isPointInPath(path, x, y) 会把 path 再乘当前 CTM。
因此场景图应:
- path 用 局部坐标 建一次;
- 绘制时
setTransform(world)再fill(path); - 命中时同样
setTransform(world)再测,或把点变到局部再测未变换的 path。
不要把已经 bake 进 path 的世界坐标再乘一次 world 矩阵,否则图形飞掉。
5. 失败形态
| 症状 | 原因 |
|---|---|
| 旋转后点不准 | 用 CSS 像素去比局部 path,没过 inverse |
| overlay HTML 对不齐 | overlay 用了 client 坐标,canvas 用了 backing 坐标 |
| 画着画着坐标系「累加缩放」 | 用了 transform 而不是每帧 setTransform |
权威资料
核对日期:2026-08-26