Fabric.js 为自由绘制的线条设计多控制点 锚点编辑
很多标注/流程图工具需要一个基础能力:画一条直线或箭头,之后想调整某个端点或中间的拐点,而不去动整条线。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 扣除描边宽度对包围盒的影响,否则点的位置会偏。
六、避坑汇总
- 一定关 objectCaching :线条走路径老是闪,多半是缓存了旧几何。设
objectCaching=false。 __corner一开始是 undefined :transform.target.__corner会被间接解析,但对内容才填上,因此不能用控件名当下标,要用真实的pointIndex。- 描边宽度导致定位偏移 :锐利的线条尤其明显,统一换算到"含描边的尺寸"即可对齐。
七、验证
用三行代码跑起来:创建一条 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反馈,能避免"看似在端点却点不到"的反直觉体验。






