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);
});