简介
使用 Canvas 实现的手画不规则多边形功能。通过鼠标在画面上点击的点作为多边形的顶点,连线形成多边形。除了手画之外,还加入了随机生成和回显,检测多边形横穿,凹凸性的检测。注意:两个点如果靠太近会被认为是同一个点而忽略。闭合区域需要点击图形中的第一个点,或者点击“闭合图形”按钮。
两个 Canvas 图层
由于要实现线条跟随鼠标运动功能,而且 Canvas 的图形无法清除单个绘制命令,因此我用 CSS 的绝对定位,在同一个区域叠加了两个 Canvas 对象。一个在下面,表示已经绘制结束的图形,叫做固定 Canvas;一个在上面,每次鼠标移动就重新绘制跟随鼠标移动的线条,叫做临时 Canvas。canvasTemp 即为临时 Canvas。
<template>
<div
class="canvas-box"
:style="{ width: canvasWidth + 'px', height: canvasHeight + 'px' }"
>
<canvas
ref="canvas"
:width="canvasWidth"
:height="canvasHeight"
class="canvas"
></canvas>
<canvas
ref="canvasTemp"
class="canvas"
:width="canvasWidth"
:height="canvasHeight"
@mousedown="draw"
></canvas>
</div>
</template>
<style lang="less" scoped>
.canvas-box {
margin: 5px auto;
background: #aaa;
position: relative;
.canvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
cursor: crosshair;
}
}
</style>
复制代码
绘制过程
绘画过程简要描述为:
- 在绘画区域点击第 1 个点,随后线条跟随鼠标移动。
- 随后点击第 2 — 到第 n 个点,每个点即为多边形的顶点。
- 点击第一个点闭合图形,或者点击自动闭合图形按钮。
固定 Canvas 绘制
临时 Canvas接收点击事件,得到当前点击的坐标点。
- 如果是第一个点,则清空之前的绘画区域,并且在固定 Canvas 中移动位置到当前点。
- 否则检查符合要求后,在固定 Canvas 中绘制一条上一个点到当前点的线。如果符合条件,则闭合图形。
- 让线跟随鼠标移动。
draw(e) {
// 点一个当前点
let pointDown = {
x: e.offsetX,
y: e.offsetY,
};
// 第一个点
if (this.pointList.length === 0 || this.closeStatus) {
this.clear();
this.canvasObj.beginPath();
this.canvasObj.moveTo(pointDown.x, pointDown.y);
} else {
// 首先检查生成的点是否符合要求
const check = this.checkPoint(pointDown, this.pointList);
switch (check) {
case "closeFirst":
this.closeFigure();
return;
case false:
return;
case true:
break;
}
// 已经有点了,连成线
this.canvasObj.lineTo(pointDown.x, pointDown.y);
this.canvasObj.stroke();
}
this.pointList.push({
...pointDown,
});
// 如果已经到达最大数量,则直接闭合图形
if (this.pointList.length >= this.maxPointNum) {
this.closeFigure();
return;
}
// 让线跟随鼠标移动 后面一节描述
}
复制代码
临时 Canvas 跟随鼠标移动
在鼠标松开后,监听鼠标移动。在每次鼠标移动的时候,清空临时 Canvas。重新绘制一条点击的坐标点到当前鼠标位置的线。
由于鼠标位置移动很频繁,因此加入了简单的防抖:在下一次dom更新数据后,仅绘制鼠标最后移动到的位置。
// 让线跟随鼠标移动
document.onmouseup = () => {
document.onmousemove = (event) => {
// 防抖
if (this.timeout) {
clearTimeout(this.timeout);
}
this.timeout = setTimeout(() => {
this.canvasTempObj.clearRect(
0,
0,
this.canvasWidth,
this.canvasHeight
);
this.canvasTempObj.beginPath();
this.canvasTempObj.moveTo(e.offsetX, e.offsetY);
this.canvasTempObj.lineTo(event.offsetX, event.offsetY);
this.canvasTempObj.stroke();
this.timeout = null;
});
};
};
复制代码
初始化和绘制结束
初始化放在mounted中,主要是设置canvas对象。绘制结束则在检查图形符合要求后,直接闭合图形。检查是否符合要求的方法在后面描述。
// 初始化
mounted() {
this.canvasObj = this.$refs.canvas.getContext("2d");
this.canvasObj.lineWidth = 2;
this.canvasObj.strokeStyle = "red";
this.canvasObj.fillStyle = "rgba(128, 100, 162, 0.5)";
this.canvasTempObj = this.$refs.canvasTemp.getContext("2d");
this.canvasTempObj.lineWidth = 2;
this.canvasTempObj.strokeStyle = "red";
},
复制代码
// 闭合图形
closeFigure() {
// 检查部分
if (!this.checkPointCross(this.pointList[0], this.pointList)) {
this.$message.error("闭合图形时发生横穿线,请重新绘制!");
this.clear();
return;
}
if (!this.checkPointConcave(this.pointList[0], this.pointList, true)) {
this.$message.error("闭合图形时出现凹多边形,请重新绘制!");
this.clear();
return;
}
if (this.pointList.length >= this.minPointNum && !this.closeStatus) {
// 符合要求
this.canvasTempObj.lineTo(this.pointList[0].x, this.pointList[0].y);
this.canvasObj.closePath();
this.canvasObj.stroke();
this.canvasObj.fill();
document.onmousemove = document.onmouseup = null;
this.canvasTempObj.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
this.closeStatus = true;
// 绘制结束,返回数据
this.$emit("drawFinished", this.pointList);
}
},
// 清除图形
clear() {
this.pointList = [];
document.onmousemove = document.onmouseup = null;
this.canvasTempObj.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
this.canvasObj.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
this.closeStatus = false;
},
复制代码
随机生成和回显图形
随机生成两个整数点作为xy坐标形成多边形顶点,判断是否符合要求,符合要求的顶点列表作为多边形。如果符合要求,则调用回显函数。
随机生成图形
可以看到随机生成的图形中有while (1)循环,还有if (j > num * 100) 这种限制。这是因为目前是随机生成点,再判断是否符合要求,在遇到较复杂的图形限制时,随机生成函数很容易陷入死循环,因此加入了循环失败次数限制。
// 辅助函数,获取随机点
getRandomPoint() {
const x = Math.floor(Math.random() * this.canvasWidth + 1);
const y = Math.floor(Math.random() * this.canvasHeight + 1);
return {
x,
y,
};
},
// 随机生成点并绘制图形
randomRegion() {
while (1) {
const num = this.regionNum.min;
const pointList = [this.getRandomPoint()];
let i = 1;
let j = 0;
while (i < num) {
const point = this.getRandomPoint();
// 判断生成的点是否符合要求
if (this.$refs.canvasRegion.checkPoint(point, pointList) !== true) {
++j;
continue;
}
if (j > num * 100) break;
++i;
pointList.push(point);
}
// 判断生成的图形是否符合要求
if (
pointList.length < num ||
!this.$refs.canvasRegion.checkPointCross(pointList[0], pointList) ||
!this.$refs.canvasRegion.checkPointConcave(pointList[0], pointList, true)
) {
continue;
} else {
this.$refs.canvasRegion.handleForeignData(pointList);
break;
}
}
},
复制代码
回显图形
直接在固定 Canvas中绘制即可。
// 处理外来的数据
handleForeignData(canvasData) {
this.clear();
if (
!canvasData ||
canvasData.length < this.minPointNum ||
canvasData.length > this.maxPointNum
) {
this.$message.error("回显数据不符合要求!");
return;
}
this.pointList = canvasData;
this.echoFigure();
},
// 回显图形
echoFigure() {
this.canvasObj.beginPath();
this.canvasObj.moveTo(this.pointList[0].x, this.pointList[0].y);
for (let i = 1; i < this.pointList.length; ++i) {
this.canvasObj.lineTo(this.pointList[i].x, this.pointList[i].y);
}
this.canvasObj.stroke();
this.closeFigure();
},
复制代码
点位置的限制
为了防止用户手抖,在同一个位置多次点击鼠标而产生很多顶点,我们增加了点的位置限制。方法很简单:
- 点击鼠标时,对图形中已有的每个顶点进行循环,每个点判断与鼠标当前位置的距离。如果距离小于要求,则不能生成顶点。
- 如果与图形中的第一个顶点(就是起点)位置小于要求,则认为用户在尝试闭合图形。
// 检查点有没有与当前点位置太近,如果太近就不认为是一个点
checkPointClose(point, pointList) {
let i;
for (i = 0; i < pointList.length; ++i) {
const distance = Math.sqrt(
Math.abs(pointList[i].x - point.x) +
Math.abs(pointList[i].y - point.y)
);
if (distance > 3) {
continue;
}
// 如果是在第一个点附近点的,那就认为是在尝试闭合图形
if (pointList.length >= this.minPointNum && i === 0) {
return "closeFirst";
}
return false;
}
return true;
},
复制代码
相交线的限制
相交线表示多边形的两条边相交。如果允许相交,就会产生下面这种奇怪的图形:
这种图形很明显不像正常的多边形,因此要对相交线进行限制。那么如何判断两条边相交?
小学数学做法
小学数学课的时候,我们都学过如何求两个直线的交点。