Fabric.js 画布与图层双指捏合:手势缩放避坑指南

2026-09-22 71 浏览 0 评论 0 字

移动端画布类应用,双指捏合缩放 是高频手势。麻烦在于:这个手势既可以用来缩放 整个画布 (看全图),也可以用来缩放 当前选中图层 (放大某个元素),到底该缩放谁,需要根据状态判定。更麻烦的是,一旦和画笔、擦除等绘制模式混在一起,处理次序错了就会出现 画布的笔刷突然控制不住。

一、识别双指手势

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

五、坑与经验

  1. 忘了记录基准值 :很多人直接拿 e.self.scale * 当前 scale ,但 scale 是"相对手势起点"的比例,直接乘会导致每次缩放都基于上一次的累计值,指数级放大。所以要先在一开始缓存基准 baseZoom/baseScaleX/baseScaleY 。
  2. 缩放上限不设 :双指一拉直接爆表,影响清晰度。务必 clamp。
  3. 手势与单击冲突 :双指松开的瞬间触发了 mouse:down / mouse:up ,导致误触。用一个正在手势的标记位,在手势结束的重置阶段忽略单击。
  4. 图层非正交缩放 :如果要保持等比,就得让 scaleX = scaleY ,即只取一个 scale。否则手势会把图片拉变形。

六、可跑的最小验证

一个 fabric.Canvas ,放若干矩形,绑定上面的事件。测试四种情况:未选图层双指(缩放画布)、选中图层双指(缩小图层)、画笔模式下双指(应临时停笔)、处理边界。四个用例过了,这套手势就算稳。

FAQ(常见问题)

  • 问:双指缩放时先起作用的到底是画布还是图层?
    取决于命中的目标你是否"选中了图层"。选中某个图层时,双指应优先缩放该图层;未选中时才缩放整个画布视口。实现上先判断 getActiveObject() ,再决定把手势交给哪个缩放逻辑,别让两者都响应导致卡顿抖动。

  • 问:手势过程中要临时"锁死"图层,怎么做?
    在 touchstart 双指判定成立时,先把图层设为不可被选择或把本次手势标记为"缩放模式",直到 touchend 才解锁。这样画笔模式下双指捏合才不会把手势当成连续"拖动画笔"。

  • 问:缩放要不要等比?
    如果要保持比例,可直接 scaleX = scaleY (只取其中一个计算),否则双指在 x/y 方向量不一致时会把图片拉变形。


发布评论

发布评论前请先 登录。
0 评论
点赞
收藏

评论列表 0

暂无评论