Fabric.js 为自由绘制的线条设计多控制点 锚点编辑

2026-09-20 84 浏览 0 评论 0 字

很多标注/流程图工具需要一个基础能力:画一条直线或箭头,之后想调整某个端点或中间的拐点,而不去动整条线。Fabric.js 的 Polygon 天然支持多顶点,我们要做的是给每个顶点挂一个可拖拽的控制点,同时保证 拖顶点时不把整条线带跑偏 ​。这是整个功能最容易崩的地方。

一、需求拆解

拿一条带两个端点的直线来说,我们的目标是:

  • 两个端点各有一个可拖拽的 小球;
  • 拖其中一个端点,线随之变化,另一个端点不动;
  • 可选:端点旁加 删除线 和 复制线 的快捷把手;
  • 拖完停止时,线的位置整体不漂移。

二、注册多顶点控制点

给每个 points 建一个 fabric.Control ,并塞进对象的 controls :

function initLineControls(obj) {
  const controls = [];
  obj.points.forEach((point, index) => {
    controls[index] = new fabric.Control({
      cursorStyle: 'pointer',
      mouseDownHandler: () => { /* 记录 */ },
      mouseUpHandler: () => { /* 结束 */ },
      positionHandler: positionHandler,   // 决定控制点画在哪里
      actionHandler: wrapper(index - 1, actionHandler), // 拖拽时如何改 points
      actionName: 'modifyPolygon',
      pointIndex: index,
    });
  });
  obj.controls = controls;
  obj.objectCaching = false; // 线条类对象不要开启缓存,保证实时刷新
}

三、控制点画在哪(positionHandler)

控制点要跟随对应顶点走,但 Fabric 的默认控制点都是参考对象局部坐标系 + 变换矩阵的。这里要把"点在 points 里的偏移 + 变换矩阵"算成屏幕坐标:

function positionHandler(dim, finalMatrix, fabricObject) {
  const point = fabricObject.points[this.pointIndex];
  const p = { x: point.x - fabricObject.pathOffset.x, y: point.y - fabricObject.pathOffset.y };
  const m = fabric.util.multiplyTransformMatrices(
    fabricObject.canvas.viewportTransform,
    fabricObject.calcTransformMatrix()
  );
  return fabric.util.transformPoint(p, m);
}

四、拖拽不要带跑整条线(关键)

只更新当前拖的这个点还不够,因为 points 属于全局坐标,一旦这个顶点被改,对象的包围盒会变,反过来把整条线挪走。这就是"整体漂移"的根源。

解决思路是 anchorWrapper :把当前拖的点当成"锚点",拖拽结束后,用锚点所在的绝对坐标把整个对象"钉"回去:

function wrapper(anchorIndex, fn) {
  return function (eventData, transform, x, y) {
    const obj = transform.target;
    // 记录锚点拖拽前的绝对坐标
    const abs = fabric.util.transformPoint(
      { x: obj.points[anchorIndex].x - obj.pathOffset.x, y: obj.points[anchorIndex].y - obj.pathOffset.y },
      obj.calcTransformMatrix()
    );
    const before = fn(eventData, transform, x, y); // 真正改多个 points
    // 拖拽结束后,把锚点对应的包围盒重新放回原来位置
    obj.setPositionByOrigin(abs, 0.5, 0.5);
    return before;
  }
}

这样拖拽过程中只有一个点动,但每次 action 结束后对象被"吸"回正确位置,规避了整条线被带跑的问题。

五、顶点之间的换算

在 actionHandler 里,把鼠标局部坐标除以当前尺寸,再映射回 points 里的"归一化坐标":

function actionHandler(_, transform, x, y) {
  const poly = transform.target;
  const pointIndex = poly.controls[poly.__corner].pointIndex;
  const local = poly.toLocalPoint(new fabric.Point(x, y), 'center', 'center');
  const baseSize = getSizeWithStroke(poly);
  const size = poly._getTransformedDimensions(0, 0);
  poly.points[pointIndex] = {
    x: (local.x * baseSize.x) / size.x + poly.pathOffset.x,
    y: (local.y * baseSize.y) / size.y + poly.pathOffset.y,
  };
  return true;
}

关键一是用 toLocalPoint 把鼠标坐标转到对象局部空间,二是用 getSizeWithStroke 扣除描边宽度对包围盒的影响,否则点的位置会偏。

六、避坑汇总

  1. 一定关 objectCaching :线条走路径老是闪,多半是缓存了旧几何。设 objectCaching=false 。
  2. __corner 一开始是 undefined : transform.target.__corner 会被间接解析,但对内容才填上,因此不能用控件名当下标,要用真实的 pointIndex 。
  3. 描边宽度导致定位偏移 :锐利的线条尤其明显,统一换算到"含描边的尺寸"即可对齐。

七、验证

用三行代码跑起来:创建一条 new fabric.Polygon([{x:0,y:0},{x:100,y:0}]) ,调用 initLineControls ,加入画布并选中。分别拖动两个端点、观察是否有整线漂移。一个空滑块 demo 就能把整套锚点逻辑测透。

FAQ 常见问题

  • 问:拖动某个锚点为什么整条线也跟着跑了?
    Fabric 的 Polygon 会让整个对象一起平移。关键是注册控制点时要基于 顶点的局部坐标 在 positionHandler 里换算,而不是直接改 left/top ;否则一个锚点一动手,整条线就漂移,就是我们第二节踩到的坑。

  • 问:锚点数量一变,中间顶点怎么算?
    增删锚点只需要改 Polygon.points 数组里的坐标项,再 canvas.requestRenderAll() 。注意控制点是按"顶点索引"绑定的,删掉中间锚点后后面索引会自动前移,别拿旧索引去取越界顶点。

  • 问:描边粗细不同时,端点点不到或被挡了怎么办?
    锚点位置应统一换算到"含描边宽度的边界",控制点的命中区用 cornerSize 调大一点、必要时加 cursor:move 反馈,能避免"看似在端点却点不到"的反直觉体验。


发布评论

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

评论列表 0

暂无评论