相机与锚点缩放
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