问题:mousemove 中累加导致误差
// 错误写法:每次 mousemove 都叠加
canvas.addEventListener('mousemove', (e) => {
if (!isDragging) return;
const dx = e.clientX - lastX;
const dy = e.clientY - lastY;
bbox.x += dx; // 浮点累加,误差逐帧积累
bbox.y += dy;
lastX = e.clientX;
lastY = e.clientY;
});
每帧的 dx 存在浮点舍入,经过几百次 mousemove 后,bbox 位置与鼠标实际位置产生偏差。
解决:startBox 模式
mousedown 时保存初始状态,mousemove 始终基于初始状态计算绝对位置:
let dragState = null;
canvas.addEventListener('mousedown', (e) => {
const mouseX = getCanvasX(e);
const mouseY = getCanvasY(e);
if (isInsideBbox(mouseX, mouseY, bbox)) {
dragState = {
startX: mouseX,
startY: mouseY,
startBox: { ...bbox } // 拷贝初始位置
};
}
});
canvas.addEventListener('mousemove', (e) => {
if (!dragState) return;
const mouseX = getCanvasX(e);
const mouseY = getCanvasY(e);
const dx = mouseX - dragState.startX;
const dy = mouseY - dragState.startY;
// 每帧都基于 startBox 计算,不累加
bbox.x = dragState.startBox.x + dx;
bbox.y = dragState.startBox.y + dy;
redraw();
});
canvas.addEventListener('mouseup', () => {
dragState = null;
});
bbox.x = startBox.x + dx 而不是 bbox.x += dx,误差不再累积。
offsetX 模式:自然拖拽手感
上面的写法有个体验问题:点击框的右下角时,鼠标移动会让框的左上角跳到鼠标位置。
offsetX 模式记录点击点与框左上角的偏移量,保持鼠标与抓取点的相对位置不变:
canvas.addEventListener('mousedown', (e) => {
const mouseX = getCanvasX(e);
const mouseY = getCanvasY(e);
if (isInsideBbox(mouseX, mouseY, bbox)) {
dragState = {
offsetX: mouseX - bbox.x, // 鼠标在框内的相对位置
offsetY: mouseY - bbox.y
};
}
});
canvas.addEventListener('mousemove', (e) => {
if (!dragState) return;
const mouseX = getCanvasX(e);
const mouseY = getCanvasY(e);
bbox.x = mouseX - dragState.offsetX;
bbox.y = mouseY - dragState.offsetY;
redraw();
});
鼠标无论点击框内哪个位置,拖拽过程中该点始终跟随鼠标,手感自然。
三种方案对比
| 方案 | 误差 | 拖拽手感 | 适用场景 |
|---|---|---|---|
bbox.x += dx(lastX 累加) | 有累计误差 | 一般 | 不推荐 |
| startBox 模式 | 无误差 | 左上角跳到鼠标 | 精确位移要求高 |
| offsetX 模式 | 无误差 | 自然,保持抓取点 | 用户交互体验优先 |
大多数标注工具选择 offsetX 模式,既无误差又有自然手感。
坐标转换
Canvas 内部坐标需要考虑缩放和滚动:
function getCanvasX(e) {
const rect = canvas.getBoundingClientRect();
return (e.clientX - rect.left) / scale;
}
function getCanvasY(e) {
const rect = canvas.getBoundingClientRect();
return (e.clientY - rect.top) / scale;
}
scale 是 Canvas 的缩放比例,不除以 scale 会导致缩放后拖拽位置错误。
