Fabric.js 画布与图层双指捏合:手势缩放避坑指南
移动端画布类应用,双指捏合缩放 是高频手势。麻烦在于:这个手势既可以用来缩放 整个画布 (看全图),也可以用来缩放 当前选中图层 (放大某个元素),到底该缩放谁,需要根据状态判定。更麻烦的是,一旦和画笔、擦除等绘制模式混在一起,处理次序错了就会出现 画布的笔刷突然控制不住。
一、识别双指手势
Fabric.js 在移动端提供了一个 touch:gesture 事件,事件对象里有:
e.self.touches:当前所有触点数组e.self.scale:相对起点的缩放比例变化(≥1 放大,<1 缩小)e.self.x / e.self.y:双指中心的坐标
先判断是不是真正的双指:
canvas.on('touch:gesture', (e) => {
if (!e.self || !e.self.touches) return;
if (e.self.touches.length !== 2) return; // 只有双指才处理
// 进入手势处理……
});二、判定:缩放画布还是缩放图层
用一个变量记住手势开始时是否有处于选中状态的图层:
let scaleMode = false;
let activeObject = null;
let baseZoom, baseScaleX, baseScaleY;
canvas.on('touch:gesture', (e) => {
if (e.self.touches.length !== 2) return;
if (!scaleMode) {
// 第一次进入:记录基准
activeObject = canvas.getActiveObject();
if (activeObject) {
baseScaleX = activeObject.scaleX;
baseScaleY = activeObject.scaleY;
lockObject(activeObject, true); // 先把对象锁住,避免冲突
} else {
baseZoom = canvas.getZoom();
}
scaleMode = true;
}
if (activeObject && !isToolConflict()) {
// 缩放图层
const sx = clamp(baseScaleX * e.self.scale, 0.01, 5);
const sy = clamp(baseScaleY * e.self.scale, 0.01, 5);
activeObject.set({ scaleX: sx, scaleY: sy });
canvas.requestRenderAll();
} else {
// 缩放画布
const z = clamp(baseZoom * e.self.scale, 0.05, 5);
canvas.zoomToPoint({ x: e.self.x, y: e.self.y }, z);
canvas.requestRenderAll();
}
});注意:这里有个微妙点——如果当前没有选中图层( activeObject 为空),双指是要缩放画布的。
三、手势过程中的 临时上锁
手势缩放时,如果对象还被允许做平移、旋转、缩放,双指操作的坐标会跟对象自身的变换逻辑打架,导致画面撕裂。所以进入手势时要把对象可交互的开关全部关掉,手势结束再恢复:
function lockObject(obj) {
obj.set({
lockMovementX: true,
lockMovementY: true,
lockScalingX: true,
lockScalingY: true,
lockRotation: true,
draggable: false,
});
}
function unlockObject(obj) {
obj.set({
lockMovementX: false,
lockMovementY: false,
lockScalingX: false,
lockScalingY: false,
lockRotation: false,
draggable: true,
});
}四、与绘制模式的互斥
手势前,如果当前处于 画笔/擦除 这类"正在画"的模式,直接切换画布缩放会打断笔触甚至让笔误画。处理方式:
- 手势开始:把
isDrawingMode关掉,并打一个gestureing标记; - 手势结束:再安全地把
isDrawingMode打开,恢复到正确的绘图状态。
canvas.on('touch:gesture', (e) => {
const drawingToolActive = isDrawingTool();
if (drawingTool && e.self.touches.length === 2) {
canvas.isDrawingMode = false; // 暂停绘制
g_state.isGesture = true;
}
});
// mouse:up
canvas.on('mouse:up', () => {
if (g_state.isGesture) {
if (drawingToolActive) canvas.isDrawingMode = true; // 恢复绘制
g_state.isGesture = false;
}
});五、坑与经验
- 忘了记录基准值 :很多人直接拿
e.self.scale * 当前 scale,但scale是"相对手势起点"的比例,直接乘会导致每次缩放都基于上一次的累计值,指数级放大。所以要先在一开始缓存基准baseZoom/baseScaleX/baseScaleY。 - 缩放上限不设 :双指一拉直接爆表,影响清晰度。务必 clamp。
- 手势与单击冲突 :双指松开的瞬间触发了
mouse:down/mouse:up,导致误触。用一个正在手势的标记位,在手势结束的重置阶段忽略单击。 - 图层非正交缩放 :如果要保持等比,就得让
scaleX = scaleY,即只取一个 scale。否则手势会把图片拉变形。
六、可跑的最小验证
一个 fabric.Canvas ,放若干矩形,绑定上面的事件。测试四种情况:未选图层双指(缩放画布)、选中图层双指(缩小图层)、画笔模式下双指(应临时停笔)、处理边界。四个用例过了,这套手势就算稳。
FAQ(常见问题)
问:双指缩放时先起作用的到底是画布还是图层?
取决于命中的目标你是否"选中了图层"。选中某个图层时,双指应优先缩放该图层;未选中时才缩放整个画布视口。实现上先判断getActiveObject(),再决定把手势交给哪个缩放逻辑,别让两者都响应导致卡顿抖动。问:手势过程中要临时"锁死"图层,怎么做?
在touchstart双指判定成立时,先把图层设为不可被选择或把本次手势标记为"缩放模式",直到touchend才解锁。这样画笔模式下双指捏合才不会把手势当成连续"拖动画笔"。问:缩放要不要等比?
如果要保持比例,可直接scaleX = scaleY(只取其中一个计算),否则双指在 x/y 方向量不一致时会把图片拉变形。




