Fabric.js 从零封装一个可拖拽缩放的画布工作区

2026-09-14 101 浏览 0 评论 0 字

做画布类应用的第一件事,就是让「内容」能在一个有限的可视区域里被自由地移动、放大、缩小。听起来简单,真正动手才发现,坐标换算的坑一个接一个。这篇文章把我沉淀下来的完整方案写出来,附带可直接复现的最小实现。

一、先想清楚:我们到底在操作什么

我们通常用一个 <canvas> 作为"窗口",里面放一张更大的"画纸"(内容)。用户看到的是窗口里的一部分。传统的做法是把 window 裁出来叫"视口",把内容移动缩放全部交给 viewportTransform(视口变换矩阵) 来管。

对比两种方案:

方案 做法 缺点
改元素坐标 拖动时逐个修改每个元素的 left/top 元素多时性能差、坐标随时乱
矩阵改视口 只改一个 6 元素矩阵 所有渲染统一走矩阵,高效且一致

绝大多数成熟库都走第二条路。Fabric.js 里 canvas.viewportTransform 就是那个 [a, b, c, d, e, f] 矩阵,其中 a 是缩放比例, e / f 是平移量。

二、核心三个能力

1. 基于某个点缩放

直接 setZoom 会让画面落在画布左上角,体验很糟。正确做法是"以鼠标所在点为中心缩放",即 zoomToPoint :

function zoomToPoint(canvas, point, zoom) {
  const vpt = canvas.viewportTransform.slice(); // 拷贝,避免直接改
  const zoomChange = zoom / canvas.getZoom();
  vpt[4] = point.x - (point.x - vpt[4]) * zoomChange;
  vpt[5] = point.y - (point.y - vpt[5]) * zoomChange;
  vpt[0] = zoom;
  vpt[3] = zoom;
  canvas.setViewportTransform(vpt);
  canvas.requestRenderAll();
}

关键点:先算 zoomChange ,再按比例修正平移量 e/f ,保证缩放中心不跳。

2. 拖拽平移

按住画布空白处,用"上一帧位置 - 当前位置"的差值累加进 e/f :

canvas.on('mouse:move', (opt) => {
  if (!isDragging) return;
  const e = opt.e;
  if (!e.touches && !e.clientX) return;
  const cur = e.touches ? e.touches[0] : e;
  canvas.viewportTransform[4] += cur.clientX - lastX;
  canvas.viewportTransform[5] += cur.clientY - lastY;
  canvas.requestRenderAll();
  lastX = cur.clientX;
  lastY = cur.clientY;
});

3. 将某对象居中

让画纸精确地摆到窗口正中,需要反推 e/f :

function centerOnObject(canvas, obj) {
  const center = obj.getCenterPoint();
  const vpt = canvas.viewportTransform;
  vpt[4] = canvas.width / 2 - center.x * vpt[0];
  vpt[5] = canvas.height / 2 - center.y * vpt[3];
  canvas.setViewportTransform(vpt);
  canvas.requestRenderAll();
}

三、自适应画纸

导入一张大图时,通常希望自动缩放,让整张画纸完整落在窗口里。本质是求出"宽方向比例"和"高方向比例",取较小者,并留点边距:

function zoomAuto(canvas, obj) {
  const pad = 20;
  const sw = (canvas.width - pad) / obj.width;   // 宽适配比
  const sh = (canvas.height - pad) / obj.height; // 高适配比
  const zoom = Math.min(sw, sh);                 // 取更小的那个
  canvas.setViewportTransform(fabric.iMatrix.concat());
  canvas.zoomToPoint({ x: canvas.width / 2, y: canvas.height / 2 }, zoom);
  centerOnObject(canvas, obj);
  canvas.requestRenderAll();
}

四、避坑合集(真实遇到的)

  1. 拖拽后坐标漂移 :原因多半是 viewportTransform 被直接改了却没有 concat 克隆,导致在旧基础上累加错误。保险做法是先 slice() 一份再改。
  2. 缩放后水印/辅助线不跟 :辅助线、水印等是"覆盖层",必须监听缩放事件,把新 e/f 同步给它们,否则会出现"错位叠合"。
  3. 缩放边界 :不加限制,用户能放大到 10000%,直接马赛克。务必夹住 min/max:
zoom = Math.max(0.05, Math.min(5, zoom));
  1. 触摸与鼠标事件差异 :移动端用 e.touches[0] ,桌面用 e 。务必抽一个统一的坐标取值函数,避免在业务里到处判分支。

五、总要校验

把 ZoomToPoint 做成一个可运行的最小 demo(一个 canvas + mousewheel 缩放 + 拖拽 + 一个按钮居中),跑起来验证三点:缩放中心不跳、拖拽丝滑、居中准确。核心逻辑就这三段代码,其余的只是事件接线。

把这套能力封装成独立模块后,无论是做"素材画布""标注工具"还是"编辑器",都能直接复用。

FAQ(常见问题)

问:为什么缩放后画面偏到边角而不是围绕鼠标点?

因为直接调用了 setZoom ,它会以画布左上角为锚点缩放。要围绕鼠标点缩放,必须用 zoomToPoint 并把缩放前的位置换算进新矩阵,这样缩放中心才不跳。

问: viewportTransform 可以被直接修改吗?

可以,但别在旧引用上原地累加。推荐先 slice() 拷贝一份,再修改 e/f 和缩放值,最后通过 setViewportTransform 写回,避免叠加错误导致坐标漂移。


发布评论

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

评论列表 0

暂无评论