使用fabric完成多边形选区

使用fabric完成多边形选区

五月 06, 2021

a

之前有过这么个需求, 说是要在图片中选区(车站,广场等),然后选区的要求是不规则凸多边形 按点连接, 之后要获取选区点坐标, 同时也有将生成点坐标回显展示选区的需求, 在一番调研之后决定使用fabric.js来实现功能

Fabric是一个强大而简单的JS Canvas库,我们能通过使用它实现在Canvas上创建、填充图形、给图形填充渐变颜色。 组合图形(包括组合图形、图形文字、图片等)等一系列功能。简单来说我们可以通过使用Fabric从而以较为简单的方式实现较为复杂的Canvas功能

思路

整个的思路大概是先初始化fabric实例,将图片展示到canvas上,根据图片和canvas的尺寸得到缩放比例,方便将后续得到的点坐标按图片尺寸还原.接着通过鼠标点击在canvas上绘制点,将点按照点击顺序连接成线,则得到多边形的边,此时三个点以上则可连接成多边形,当点击完成时将起始点终点连接闭合成多边形.

draw-close

其中核心的问题则是按顺序连接的点,当图形完成,起始点和终点连接时,选区图形是否能成为一个多边形?此处我采用的方式是, 通过鼠标移动,可以落点时鼠标同首尾点连接虚线,当无法绘制所需多边形时隐藏虚线并无法落点,确保已经绘制的点之间可连成多边形

draw-cross

即核心的判断则是:

  • 起始点与鼠标连线:line1,终点与鼠标连线:line2,则line1,line2是否同时与已连接的线段相交, 同时相交则无法落点
    draw2
  • 已连接线段中除首尾两条外, 是否存在line1line2与某条线段相交, 相交则无法落点
    draw3
  • 通过向量叉乘判断线段相交
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // 计算向量叉乘
    function crossMul(v1, v2) {
    return v1.x * v2.y - v1.y * v2.x;
    }
    // 判断两条线段是否相交line1(p1,p2) line2(p3,p4)
    function checkCross(p1, p2, p3, p4) {
    var v1 = { x: p1.x - p3.x, y: p1.y - p3.y },
    v2 = { x: p2.x - p3.x, y: p2.y - p3.y },
    v3 = { x: p4.x - p3.x, y: p4.y - p3.y },
    v = crossMul(v1, v3) * crossMul(v2, v3);
    v1 = { x: p3.x - p1.x, y: p3.y - p1.y };
    v2 = { x: p4.x - p1.x, y: p4.y - p1.y };
    v3 = { x: p2.x - p1.x, y: p2.y - p1.y };
    return v <= 0 && crossMul(v1, v3) * crossMul(v2, v3) <= 0
    ? true
    : false;
    }

实现

首先页面上要有个canvas容器,据此生成fabric的canvas实例,还要将图片展示在canvas中,然后根据图片尺寸,canvas尺寸得到展示的缩放比例radio

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var convasDefaultWidth = 500;
//
var image = new fabric.Image(document.getElementById("image"));
var imageWidth = image.get("width");
var canvasWidth =
imageWidth > convasDefaultWidth ? convasDefaultWidth : imageWidth;
// 缩放比例
var radio = imageWidth / canvasWidth;
// fabfric实例
var canvas = new fabric.Canvas("canvas", {
width: canvasWidth,
height: image.get("height") / radio,
backgroundImage: image.scaleToWidth(canvasWidth),
backgroundColor: 'transparent',
selection:false
});

当绘制完成或回显时, 直接使用fabric绘制多边形即可

1
2
3
4
5
6
7
8
9
10
var polygon = new fabric.Polygon([], {
strokeWidth: 2,
stroke: "green",
fill: "transparent",
objectCaching: false,
// transparentCorners: false,
// cornerColor: "blue",
});
// 添加到画布
canvas.add(polygon);

初始化各个状态,以及将来鼠标显示的连接线等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 已绘制点
var points = [];
// 当前是否可以落点
var canAddPoint = true;
// 已存在线段
var existLines = [];
// 绘制是否完成
var isCompelete = false;
// 线段默认配置
var lineOption = {
strokeWidth: 2,
stroke: "green",
fill: "green",
selectable: false,
evented: false
};
// 连接线使用虚线
var dashLineOption = Object.assign({},lineOption,{strokeDashArray: [10, 15]})
// 鼠标连接线
var combineLineWithFirst = new fabric.Line([0, 0, 0, 0], dashLineOption);
var combineLineWithSecond = new fabric.Line([0, 0, 0, 0], dashLineOption);
// 添加至canvas
canvas.add(combineLineWithFirst);
canvas.add(combineLineWithSecond);

初始化fabric canvas的鼠标事件,即鼠标移动和点击

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// 移动
function canvasHandleMouseMove(options) {
var pointsLen = points.length;
if (pointsLen === 0) return;
// 起点与终点
var firstPoint = points[0];
var lastPoint = points[pointsLen - 1];
// 两点以上
if (pointsLen > 2) {
// 已绘制的线段
var exlines = getLinesArr();
var hasCross = false;
for (var i = 0; i < exlines.length; i++) {
var perline = exlines[i];
var point1 = { x: perline[0], y: perline[1] };
var point2 = { x: perline[2], y: perline[3] };
// 根据之前所说的判断条件
// line1,line2是否同时与已连接的线段相交
// 或 存在line1或line2与某条线段相交
if (
checkCross(options.pointer, firstPoint, point1, point2) &&
checkCross(options.pointer, lastPoint, point1, point2)
) {
hasCross = true;
break;
} else if (i > 0 && i < exlines.length - 1 && (checkCross(options.pointer, firstPoint, point1, point2) ||
checkCross(options.pointer, lastPoint, point1, point2))){
hasCross = true;
break;
}
}
}
// 能落点, 绘制鼠标连接线
if (!hasCross) {
canAddPoint = true;
setLineWithPoint(combineLineWithFirst, options.pointer, firstPoint);
setLineWithPoint(combineLineWithSecond, options.pointer, lastPoint);
} else {
// 隐藏鼠标连接线
canAddPoint = false;
setLineWithPoint(combineLineWithFirst);
setLineWithPoint(combineLineWithSecond);
}
// fabric 绘制
canvas.renderAll();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 点击
function canvasHandleMouseDown(options) {
if (!canAddPoint) return;
//
var pointsLen = points.length;
var lastPoint = points[pointsLen - 1];
if (
lastPoint &&
options.pointer.x === lastPoint.x &&
options.pointer.y === lastPoint.y
)
return;
// console.log(options, options.pointer.x, options.pointer.y);
// 落点, 注意精度
points.push({
x: +options.pointer.x.toFixed(3),
y: +options.pointer.y.toFixed(3),
});
// 落点连接线段
if (lastPoint) {
var line = new fabric.Line(
[lastPoint.x, lastPoint.y, options.pointer.x, options.pointer.y],
lineOption
);
canvas.add(line);
existLines.push(line);
}

canvas.requestRenderAll();
}

当绘制完成或回显时则将鼠标连接线隐藏, 切换完成状态, 通过已绘制的点连接生成多边形,而重置则清空画布并重置状态即可

1
2
3
4
5
// 多边形回显
function drowPolygon(points) {
isCompelete = true;
polygon.set("points", points);
}

后续再优化的话 其实可以开放fabric多边形的锚点拖拽,旋转,形变等等

demo完整代码