# 如何实现以鼠标为中心的精准画布缩放?

# 一、核心原理:坐标系转换

要实现精准缩放,需要理解两个核心坐标系:

  1. 视口坐标系:用户当前看到的区域(Viewport)
  2. 内容坐标系:画布的实际内容(Content)

缩放的本质就是这两个坐标系的比例变换。我们的目的是:无论缩放比例如何变化,鼠标点对应的内容位置始终保持不变

# 数学关系

设:

  • 当前缩放比例: S
  • 新缩放比例: S'
  • 鼠标的视口坐标: 向量A = (P_x, P_y)
  • 画布偏移量 (相对于视口): 向量B = (O_x, O_y)

将鼠标相对于视口的坐标减去画布的偏移量得到鼠标相对于画布的坐标:
则内容坐标系下对应的点为向量 C:
C_x = (P_x - O_x) / S
C_y = (P_y - O_y) / S
向量C = 向量B - 向量A

缩放后保持该点的位置不变:
相对位置要乘以新的缩放比例 S'
可得 (这时候我们要求的就是新的画布偏移量了)
P_x = O_x' + C_x * S'
P_y = O_y' + C_y * S'
向量A = 向量B + 向量C'

就可以解得新的画布偏移量了 (O_x', O_y')
O_x = Px - C_x * S'
O_y = Py - C_y * S'

# 代码实现:

  1. 坐标系转换
    (pointer - stage) 将视口坐标转换为内容绝对坐标,再除以当前缩放比例得到原始内容坐标系的位置。
  2. 缩放方向处理
    通过 deltaY 判断滚轮方向,结合 ctrlKey 处理快捷键
  3. 位置补偿计算
    pointer - mousePointTo * newScale 实现视觉位置的确定,确保缩放后鼠标点对应的内容位置不变。
// 处理鼠标滚轮缩放
    const handleWheel = useCallback((e: KonvaEventObject<WheelEvent>) => {
        // 1. 阻止原来的默认滚动事件
        e.evt.preventDefault();
        const stage = stageRef.current;
        if (!stage) return;
        console.log(stage.x(), stage.y())
        // 2. 获取当前的缩放状态
        const oldScale = stage.scaleX();
        const pointer = stage.getPointerPosition();
        console.log(pointer)
        if (!pointer) return;
        // 3. 计算鼠标点在内容坐标系的真实位置
        const mousePointTo = {
            x: (pointer.x - stage.x()) / oldScale,
            y: (pointer.y - stage.y()) / oldScale,
        };
        // 4. 确定缩放方向和比例
        let direction = e.evt.deltaY > 0 ? 1 : -1;
        // 如果按住 Ctrl 键,反转方向
        if (e.evt.ctrlKey) {
            direction = -direction;
        }
        const scaleBy = 1.2; // 每次缩放的比例
        let newScale = direction > 0 ? oldScale * scaleBy : oldScale / scaleBy;
        // 5. 限制缩放比例
        newScale = Math.max(0.2, Math.min(4, newScale));
        // 更新缩放比例
        stage.scale({ x: newScale, y: newScale });
        // 计算新的偏移量
        const newPos = {
            x: pointer.x - mousePointTo.x * newScale,
            y: pointer.y - mousePointTo.y * newScale,
        };
        // 应用变换
        stage.position(newPos);
    }, []);

# 可视化理解

假设:

  • 当前缩放: 1x
  • 画布偏移: (0, 0)
  • 鼠标位置: (500, 300)
    当放大到 2x 时:
  1. 计算原始内容位置
(500 - 0) / 1 = 500
(300 - 0) / 1 = 300
  1. 计算新偏移量
newX = 500 - 500 * 2 = -500
newY = 300 - 300 * 2 = -300
  1. 结果:内容向左上角偏移,是的原中心点保持可见

# 总结

通过坐标系转换和数学关系,实现了以鼠标为中心的精准画布缩放。


完整项目代码:https://github.com/aynya/DniMap