相机与锚点缩放
无限画布的相机就是 改 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 不可逆。夹 minScale。factor 从 wheel 来时处理 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> 写 matrix,viewBox 固定。和改 viewBox 二选一。混用时 getScreenCTM 仍对,但你自己的「世界 AABB」会算重一份。
4. 适用 / 不适用
适用:白板、地图、矢量编辑器。
不适用:响应式插画——那是 preserveAspectRatio + 固定 viewBox,不是相机。见 05 布局。
5. 失败形态
| 症状 | 原因 |
|---|---|
| 缩放往左上角吸 | 只改了 scale / viewBox 宽高,没锚点 |
| 缩放后命中全偏 | 指针又手除了一次 scale |
| 线宽爆炸、字发糊 | CSS scale 整棵树,或滤镜半径跟用户单位放大 |
| 越缩越小飞走 | 每帧 transform() 叠乘,不是每帧写绝对 viewBox / matrix |
| 触控板变成页面滚动 | wheel 监听是 passive |
权威资料
核对日期:2026-08-26