使用 Canvas 手画不规则多边形,并限制相交线和凹多边形

简介

使用 Canvas 实现的手画不规则多边形功能。通过鼠标在画面上点击的点作为多边形的顶点,连线形成多边形。除了手画之外,还加入了随机生成和回显,检测多边形横穿,凹凸性的检测。注意:两个点如果靠太近会被认为是同一个点而忽略。闭合区域需要点击图形中的第一个点,或者点击“闭合图形”按钮。

截取图片_20220307191149.png

两个 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. 在绘画区域点击第 1 个点,随后线条跟随鼠标移动。
  2. 随后点击第 2 — 到第 n 个点,每个点即为多边形的顶点。
  3. 点击第一个点闭合图形,或者点击自动闭合图形按钮。

固定 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();
},
复制代码

点位置的限制

为了防止用户手抖,在同一个位置多次点击鼠标而产生很多顶点,我们增加了点的位置限制。方法很简单:

  1. 点击鼠标时,对图形中已有的每个顶点进行循环,每个点判断与鼠标当前位置的距离。如果距离小于要求,则不能生成顶点。
  2. 如果与图形中的第一个顶点(就是起点)位置小于要求,则认为用户在尝试闭合图形。
// 检查点有没有与当前点位置太近,如果太近就不认为是一个点
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;
},
复制代码

相交线的限制

相交线表示多边形的两条边相交。如果允许相交,就会产生下面这种奇怪的图形:

1.png
这种图形很明显不像正常的多边形,因此要对相交线进行限制。那么如何判断两条边相交?

小学数学做法

小学数学课的时候,我们都学过如何求两个直线的交点。

(x1,y1)(x2,y2)构造直线方程y=ax+b(x3,y3)(x4,y4)构造直线方程y=cx+d联立求解(x,y)\begin{matrix} (x1,y1)&(x2,y2)\quad\overrightarrow{\scriptsize 构造直线方程}\quad y=ax+b\\(x3,y3)&(x4,y4)\quad\overrightarrow{\scriptsize 构造直线方程}\quad y=cx+d\end{matrix}\quad \overrightarrow{\scriptsize 联立求解}\quad(x,y)

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享