Fabric.js 旋转磁吸与归正:让对象在松手时自动吸附直角
做设计/标注类画布,用户旋转一个形状后,往往希望 松手那一瞬间如果能接近横平竖直就自动吸过去。不加这个吸附,对齐全靠手抖;加了之后,产品质感立刻不一样。这篇文章讲讲怎么用极简的数学实现这种"磁吸"。
一、监听旋转事件
Fabric.js 只要监听 object:rotating 。这个事件在对象每一次旋转带动画/拖动过程中高频触发,正好适合做吸附判断:
canvas.on('object:rotating', (e) => {
snapToRightAngle(e.target);
});
二、取模判断 距哪边最近
核心思路:任意角度 angle ,对它取 90 的模,剩下的就是"离某个直角方向有多远"。如果余数落在容差内,就把角度吸附到最近的整数倍 90°:
function snapToRightAngle(obj, deviationDeg = 5) {
const bisection = 90;
const angle = ((obj.angle % 360) + 360) % 360; // 归一化到 0~360
const remainder = angle % bisection;
let target = -1;
if (remainder < deviationDeg) {
target = Math.floor(angle / bisection) * bisection; // 前一个直角
} else if (bisection - remainder < deviationDeg) {
target = (Math.floor(angle / bisection) + 1) * bisection; // 下一个直角
}
if (target !== -1) {
obj.rotate(target);
obj.canvas.requestRenderAll();
}
}
看这个逻辑:角度对 90 同余, remainder 表示 这个角度离上一个直角偏了多少。如果 remainder < 5° ,就往回吸附到上一个直角;如果离下一个直角不足 5°(即 90 - remainder < 5 ),就往前吸附到下一个直角。中间范围(>= 5 且 <= 85)不动,尊重用户的自由旋转。
三、负数角度与多圈的处理
旋转会超过 360 或出现负角度,例如用户逆向转了 800°。直接 % 会得到负数,导致 remainder 计算错乱。所以先把角度归一化到 0~360 :
const angle = ((obj.angle % 360) + 360) % 360;
二是在吸附时保留原来的"圈数"(大小),只在就近方向上做最小改动,避免用户明明转了 3 圈却被突然"拉回"到 0°。上面代码里的是"吸附到就近直角方向",已经天然避免整圈回跳。
四、磁吸强度与手感
容差 deviationDeg 是个重要参数:
- 太小(1°~2°):几乎不吸附,形同虚设;
- 太大(15°+):用户想转 10° 都被硬拉回,反而恼火。
- 建议文档编辑 5° 左右,颜值类工具可以再小一点。
更进一步可以做成:吸附发生在 停止转动 时而非转动过程中,避免抖动。做法是给旋转回调用 clearTimeout/setTimeout 做节流,只在停止后执行一次吸附:
let snapTimer = null;
function onRotating(e) {
clearTimeout(snapTimer);
snapTimer = setTimeout(() => snapToRightAngle(e.target), 120);
}
五、坑与经验
- 负角度 bug :
-10 % 90在 JS 里是-10,直接会导致remainder < 5判不了真。归一化是必须的。 - 写入 canvas 再 apply :吸附后
obj.rotate()可能没有立刻反映到矩形几何上,要requestRenderAll刷新,或检测到变化主动renderAll()。 - 与用户自由旋转的矛盾 :磁吸只应在"拖动旋转"时生效,如果程序主动
obj.set({angle})(比如镜像后),不要误触发把角度 冲掉。可用标记位区分来源。
六、可验证 demo
在画布放一个矩形,绑定 object:rotating ,手动转它以 5° 为断电。当接近 0/90/180/270 时应对齐到最近的直角;转到 30° 时应保持 30°。两组测试都通过,归正功能即完成。
FAQ 常见问题
-
问:
-10 % 90在 JS 里算出来不对,吸附判断老失效?
因为 JS 的取模结果带符号,-10 % 90得到-10,remainder < 5就判不了真。做磁吸前一定先把角度归一化(((angle % 90) + 90) % 90),负数角度和小数才不会误判。 -
问:吸附后已经
obj.rotate(...),为什么画的矩形没跟着转?
直接改角度有时不会立刻刷新到几何上。调用后要canvas.requestRenderAll(),或检测到角度变化主动renderAll(),否则画面上还是旧角度。 -
问:程序自己
set({angle})时会误触磁吸吗?
会。磁吸只应在"用户拖动旋转"时生效,程序主动改角度(比如镜像后归正)要用一个标记位区分来源,避免把想要的任意角度又给冲回直角。