跳到主要内容

相机与锚点缩放

无限画布的相机就是 viewBox(或改一层包裹 <g> 的矩阵,二选一,不要两套一起动)。不要用 CSS transform: scale() 放大整棵 SVG DOM。

根因见 viewport 与 viewBox。这里只给可落地的锚点公式和反模式。

1. 状态

export interface Camera {
/** 视口左上角对应的用户空间原点 */
x: number;
y: number;
/** 用户单位 / CSS 像素;越大看得越广 */
scale: number;
}

export function cameraToViewBox(
camera: Camera,
cssWidth: number,
cssHeight: number,
): string {
return `${camera.x} ${camera.y} ${cssWidth * camera.scale} ${cssHeight * camera.scale}`;
}

export function applyCamera(
svg: SVGSVGElement,
camera: Camera,
cssWidth: number,
cssHeight: number,
): void {
svg.setAttribute("viewBox", cameraToViewBox(camera, cssWidth, cssHeight));
}

CSS 盒子尺寸变了(resize)要按 当前 camera 重写 viewBox,否则纵横比漂。不要把 width/height 属性当相机。

命中仍然走 getScreenCTM:浏览器已经把新 viewBox 编进矩阵。不要在指针路径再除一次 camera.scale

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

export function zoomAtClient(
camera: Camera,
svg: SVGSVGElement,
clientX: number,
clientY: number,
factor: number,
minScale: number,
maxScale: number,
): Camera {
const ctm = svg.getScreenCTM();
if (!ctm) return camera;
const local = new DOMPoint(clientX, clientY).matrixTransform(ctm.inverse());
const scale = Math.min(maxScale, Math.max(minScale, camera.scale * factor));
const k = scale / camera.scale;
return {
scale,
x: local.x - (local.x - camera.x) * k,
y: local.y - (local.y - camera.y) * k,
};
}

scale → 0 会让 CTM 不可逆。夹 minScalefactorwheel 来时处理 deltaMode,监听非 passive 并 preventDefault,否则页面跟着滚。触控板 pinch 在 Chromium 常是 ctrlKey + wheel

平移用相邻两次 用户空间点 的差(都经 clientToLocal(svg, …)),加到 camera.x/y

3. 不要 CSS scale 整棵树

做法结果
viewBox线宽、文字、滤镜区按用户单位走;non-scaling-stroke 仍可钉屏幕线宽
CSS scale<svg> 或根 <g>getBoundingClientRect 变了,部分引擎滤镜缓冲、文字光栅、命中与绘制不一致
每节点 transform 里乘相机和局部编辑矩阵缠死,撤销/导出无法拆

手柄、网格要「屏幕上恒定粗细」:vector-effect: non-scaling-stroke,不要反向把 stroke-width 设成 camera.scale 再乘一遍 CSS。vector-effect 生产只用这一个值,其余 at-risk。

另一条合法相机:根下 <g data-camera>matrixviewBox 固定。和改 viewBox 二选一。混用时 getScreenCTM 仍对,但你自己的「世界 AABB」会算重一份。

4. 适用 / 不适用

适用:白板、地图、矢量编辑器。
不适用:响应式插画——那是 preserveAspectRatio + 固定 viewBox,不是相机。见 05 布局

5. 失败形态

症状原因
缩放往左上角吸只改了 scale / viewBox 宽高,没锚点
缩放后命中全偏指针又手除了一次 scale
线宽爆炸、字发糊CSS scale 整棵树,或滤镜半径跟用户单位放大
越缩越小飞走每帧 transform() 叠乘,不是每帧写绝对 viewBox / matrix
触控板变成页面滚动wheel 监听是 passive

权威资料

核对日期:2026-08-26