跳到主要内容

相机与锚点缩放

GPU 相机是 view × projection 两份矩阵,不是 CSS transform: scale() 整张 canvas。后者会把 drawing buffer、命中、overlay、MSAA 分辨率一起搞乱。2D 无限画布把世界平移/缩放编进 view;透视再乘 proj。和 Canvas 相机 同一套状态机,投递物换成 uniform。

1. 2D 相机状态

export interface Camera2D {
x: number;
y: number;
/** 世界单位 / CSS 像素;越大看得越广 */
scale: number;
}

export function viewMatrix(camera: Camera2D, cssW: number, cssH: number): DOMMatrix {
const w = cssW * camera.scale;
const h = cssH * camera.scale;
return new DOMMatrix()
.translateSelf(-1, 1)
.scaleSelf(2 / w, -2 / h)
.translateSelf(-camera.x, -camera.y);
}

这里把世界直接映到 WebGL clip(Y 向上)。透视场景用标准 lookAt + perspective,不要把 2D scale 和 FOV 混在一个字段里。

每帧把 4×4 写成 uniformMatrix4fv。不要每帧 gl.rotate 式的固定管线——WebGL2 没有那套,自研也不要模仿。

2. 锚点缩放(光标下的世界点不动)

export function zoomAt(
camera: Camera2D,
cssX: number,
cssY: number,
factor: number,
): Camera2D {
const worldX = camera.x + cssX * camera.scale;
const worldY = camera.y + cssY * camera.scale;
const scale = Math.min(64, Math.max(1 / 64, camera.scale * factor));
return {
scale,
x: worldX - cssX * scale,
y: worldY - cssY * scale,
};
}

cssX/cssY 相对 canvas 盒,Y 向下。世界 Y 若与 CSS 同向,上面 worldY 公式成立;若世界 Y 向上,用 cssH - cssY。选一种,命中 / overlay / 绘制全项目统一。

滚轮:factor = 2 ** (deltaY * -0.001),并 preventDefault。不要用 CSS 把 canvas 放大再让浏览器插值 drawing buffer。

3. 禁止 CSS scale 当相机

做法结果
改 view 矩阵几何真值还在世界,命中可逆
canvas.style.transform = scale(...)getBoundingClientRect 变了,drawingBuffer 没变,线宽/点大小/MSAA 全错

UI chrome(按钮、小地图框)用 DOM overlay,不要画进世界再靠 CSS 放大整棵树。

resize:CSS 盒变了必须重算 proj / view,并重设 canvas.width/height 与 viewport,见 DPR

4. 失败形态

症状原因
放大发糊CSS 放大了 canvas,没涨 drawingBuffer
锚点漂移缩放用了 client 坐标当 CSS 盒坐标
命中差一个平移绘制用 view,命中用另一份过期 camera
透视下滚轮乱飞把 2D scale 套到了透视相机

权威资料

核对日期:2026-08-26