Konva.js 滚轮缩放与平移:以鼠标为中心缩放 Stage 的正确实现

scale() vs width()/height()

缩放单个图片节点有两种方式:

// scale():保持节点尺寸不变,视觉上缩放
imageNode.scale({ x: 2, y: 2 });

// width()/height():真正修改节点尺寸
imageNode.width(600);
imageNode.height(400);

两者的区别在于 scale() 不改变节点的逻辑尺寸(width()/height() 返回值不变),而后者实际修改了尺寸。大多数场景用 scale() 更灵活。

标注工具:缩放整个 Stage

如果 Stage 上同时有图片和标注框(Rect/Line),不要缩放单个图片节点,应缩放整个 Stage:

stage.scale({ x: newScale, y: newScale });
stage.position(newPos);
stage.batchDraw();

这样图片、标注框、文字所有节点都会同步缩放,坐标关系保持一致。CVAT、LabelImg 等标注工具都使用这种方式。

滚轮缩放(以鼠标位置为中心)

const SCALE_BY = 1.05;

stage.on('wheel', (e) => {
    e.evt.preventDefault();

    const oldScale = stage.scaleX();
    const pointer = stage.getPointerPosition();

    // 鼠标在画布坐标系中的位置(排除当前 scale 和偏移)
    const mousePointTo = {
        x: (pointer.x - stage.x()) / oldScale,
        y: (pointer.y - stage.y()) / oldScale,
    };

    const newScale = e.evt.deltaY > 0
        ? oldScale / SCALE_BY   // 向下:缩小
        : oldScale * SCALE_BY;  // 向上:放大

    // 限制缩放范围
    const clampedScale = Math.max(0.1, Math.min(newScale, 20));

    stage.scale({ x: clampedScale, y: clampedScale });

    // 调整偏移,使鼠标下方的点保持不动
    stage.position({
        x: pointer.x - mousePointTo.x * clampedScale,
        y: pointer.y - mousePointTo.y * clampedScale,
    });

    stage.batchDraw();
});

核心思路:缩放前记录鼠标在画布坐标系(未缩放坐标)的位置,缩放后重新计算 Stage 偏移使该点回到鼠标位置,从而实现”以鼠标为中心”的缩放效果。

拖拽平移

stage.draggable(true);

启用后可以直接拖动整个 Stage。如果需要区分拖拽图片和拖拽画布,禁用 stage.draggable() 改为手动处理:

let isDragging = false;
let lastPos = null;

stage.on('mousedown', () => {
    isDragging = true;
    lastPos = stage.getPointerPosition();
});

stage.on('mousemove', () => {
    if (!isDragging) return;
    const pos = stage.getPointerPosition();
    stage.position({
        x: stage.x() + pos.x - lastPos.x,
        y: stage.y() + pos.y - lastPos.y,
    });
    lastPos = pos;
    stage.batchDraw();
});

stage.on('mouseup', () => { isDragging = false; });

坐标系转换:获取原图坐标

点击事件的 stage.getPointerPosition() 返回的是屏幕坐标,需要转换为原图坐标:

stage.on('click', () => {
    const pointer = stage.getPointerPosition();
    const scale = stage.scaleX();

    const imageX = (pointer.x - stage.x()) / scale;
    const imageY = (pointer.y - stage.y()) / scale;

    console.log('原图坐标:', imageX, imageY);
});