Canvas绘制有效区域

该代码实现了一个四边形绘制组件,用于图像区域标注。用户可绘制四边形并进行拖拽调整,同时组件会校验四边形的有效性,包括不得交叉、面积最小限制及不可超出画布。此外,还提供了判断线段相交、点在路径内等辅助函数。
摘要由CSDN通过智能技术生成

1、有效区域最多5个

2、有效区域面积不小于画布的1/100

3、不可拖拽超出画布

4、绘制的是四边形

<template>
  <div class="ai-draw-area">
    <!--  背景图  -->
    <img :src="captureImg" class="img"/>
   

    <!--  画布  -->
    <canvas id="myCanvas" class="canvas" width="640" height="360"></canvas> 
  </div>
</template>
<script>
  import { hasIntersect, isPointInPath, calcArea } from "./config";
  
  export default {
    name: "ai-draw-area",
    data() {
      return {
        canvas: null, // canvas对象
        isDrawing: false, // 是否正在绘制
        pointArr: [], // 四边形坐标数组
        position: [], // 正在绘制的四边形四个点的坐标
        focusIdx: -1, // 选中的四边形的下标
        isDragging: false, // 能否拖拽
        pointIdx: -1, // 当前选中的顶点下标
        isMoving: false, // 是否可移动四边形
        point: [], // 点击四边形内部当下的点坐标
        isSaving: false // 保存中
      }
    },
    props: {
      // 抓拍的图片
      captureImg: {
        type: String,
        default: ''
      }
    },
    mounted() {
      this.canvas = document.getElementById("myCanvas");
      this.bindEvent();
    },
    methods: {
      // 监听鼠标事件
      bindEvent () {
        this.handleMouseUp();
        this.handleMouseDown();
        this.handleMouseMove();
      },
      // 绘制四边形
      drawQuadrangle(position, color = '#ffffff') {
        const ctx = this.canvas.getContext("2d");
        ctx.strokeStyle = color;
        ctx.beginPath();
        ctx.lineWidth = color === '#ffffff' ? 1 : 3;
        position.forEach((p, idx) => {
          if (idx === 0) {
            ctx.moveTo(...p);
          } else {
            ctx.lineTo(...p);
          }
        });
        ctx.closePath();

        // 填充
        if (position.length === 4) {
          ctx.fillStyle = color === '#ffffff' ? 'rgba(255,255,255,0.3)' : 'rgba(255,153,0,0.3)';
          ctx.fill();
        }
        ctx.stroke();
      },
      // 重新绘制所有之前绘制过的四边形
      reloadQuadrangle() {
        for(let i = 0; i< this.pointArr.length; i++) {
          const color = this.focusIdx === i ? '#ff9900' : '#ffffff';
          this.drawQuadrangle(this.pointArr[i], color);
        }
      },
      // 绘制四个顶点
      drawPoint () {
        if (this.position.length !== 4) {
          return;
        }
        const ctx = this.canvas.getContext("2d");
        this.position.forEach(p => {
          ctx.beginPath();
          ctx.lineWidth = 4;
          ctx.strokeStyle = "#ff9900";
          ctx.arc(...p, 4, 0, 2 * Math.PI);
          ctx.fillStyle = "#ff9900";
          ctx.fill();
          ctx.stroke();
        });
      },
      // 清空画布
      clearCanvas (type) {
        const ctx = this.canvas.getContext("2d");
        ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);

        // 更新画布,所有四边形重新绘制,同时绘制选中四边形的四个顶点
        if (type === "reload") {
          this.reloadQuadrangle();
          this.focusIdx !== -1 && this.drawPoint();
        }
      },
      // 校验绘制的四边形是否合格
      verifyImg() {
        // 不能交叉
        const has = hasIntersect(this.position);
        let msg = has ? 'ai_area_err' : '';
        if (!msg) {
          // 面积不能小于2400px²
          const area = calcArea(this.position);
          msg = area < 2400 ? 'ai_area_sum_err' : '';
        }
        return msg;
      },
      // 鼠标按下事件
      handleMouseDown () {
        const that = this;
        this.canvas.onmousedown = function (e) {
          e = e || event;
          const x = e.offsetX;
          const y = e.offsetY;

          // 绘制图像
          if (that.isDrawing) {
            that.position.push([x, y]);
            that.clearCanvas('reload');
            that.drawQuadrangle(that.position, '#ff9900');

            // 绘制完成变为选中状态
            if (that.position.length === 4) {
              const msg = that.verifyImg();
              if (msg) {
                that.position = [];
                that.clearCanvas('reload');
                that.$Message.error({content: that.$t("component." + msg), duration: 3});
                return;
              }

              that.isDrawing = false;
              that.pointArr.push(that.position);
              that.focusIdx = that.pointArr.length - 1;
              that.drawPoint();
            }
          } else {
            if (that.focusIdx !== -1) {
              // 判断点击的位置是不是顶点,顶点可拖拽
              that.position.forEach((p, idx) => {
                var line = Math.abs(
                  Math.sqrt(Math.pow(p[0] - x, 2) + Math.pow(p[1] - y, 2))
                );

                if (line < 8) {
                  that.isDragging = true;
                  that.pointIdx = idx;
                }
              });
            }

            if (that.isDragging) {
              return;
            }

            const selectIdx = that.judgeSelectElement(x, y);
            // 现在选中的四边形和之前选中的不一样
            if (that.focusIdx !== selectIdx) {
              that.focusIdx = selectIdx;
              that.clearCanvas('reload');

              // 现在未选中任何四边形
              if (selectIdx === -1) {
                that.position = [];
              } else {
                // 绘制选中四边形的四个顶点
                that.position = that.pointArr[selectIdx];
                that.drawPoint();
                that.isDragging = false;
                that.isMoving = true;
                that.point = [x, y];
              }
            } else {
              // 现成选中的四边形和之前选中的一样,点击的是四边形内部则可平移
              that.isMoving = true;
              that.point = [x, y];
            }
          }
        };
      },
      // 拖拽重新绘制
      handleMouseMove () {
        const that = this;
        this.canvas.onmousemove = function (e) {
          e = e || event;
          const x = e.offsetX;
          const y = e.offsetY;
          // 手动绘制
          if (that.isDrawing && that.position.length > 0) {
            that.clearCanvas('reload');
            that.drawQuadrangle(that.position.concat([[x, y]]), '#ff9900');
          }

          // 判断是否可以拖动顶点
          if (that.isDragging) {
            that.position[that.pointIdx] = [x, y];
            if (that.position.length === 4) {
              const msg = that.verifyImg();
              if (msg) {
                that.isDragging = false;
                that.position = [];
                that.pointArr.splice(that.focusIdx, 1);
                that.focusIdx = -1;
                that.$Message.error({content: that.$t("component." + msg), duration: 3});
              }
            }

            that.clearCanvas("reload");
          }

          // 移动整个四边形
          if (that.isMoving) {
            const poins = JSON.parse(JSON.stringify(that.position));
            // 是否超出了画布
            let isOuter = false;
            poins.forEach(p => {
              p[0] += x - that.point[0];
              p[1] += y - that.point[1];

              if (p[0] < 0 || p[0] > 640 || p[1] < 0 || p[1] > 360) {
                isOuter = true;
              }
            });

            if (!isOuter) {
              that.position = poins;
              that.pointArr[that.focusIdx] = that.position;
              that.point = [x, y];
              that.clearCanvas('reload');
            }
          }
        };
      },
      // 停止拖拽
      handleMouseUp () {
        const that = this;
        this.canvas.onmouseup = function () {
          that.isDragging = false;
          that.isMoving = false;
        };
        this.canvas.onmouseout = function () {
          that.isDragging = false;
          that.isMoving = false;
        };
      },
      // 判断当前触动是哪一个四边形
      judgeSelectElement(x, y) {
        if (this.pointArr.length === 0) {
          return -1;
        }

        // 重叠时,移动后绘制的四边形,所以从后往前遍历
        let selectIdx = -1;
        for(let i = this.pointArr.length - 1;i >= 0; i--) {
          const inPath = isPointInPath(x, y, this.pointArr[i]);
          if (inPath) {
            selectIdx = i;
            break;
          }
        }
        return selectIdx;
      },
      // 添加
      startDraw() {
        if (this.pointArr.length === 5) {
          this.$Message.error(this.$t("component.ai_area_num_err"));
          return;
        }
        this.isDrawing = true;
        this.focusIdx = -1;
        this.position = [];
      },
      // 删除 选中的区域
      deleteOne() {
        if (this.focusIdx !== -1) {
          this.position = [];
          this.pointArr.splice(this.focusIdx, 1);
          this.focusIdx = -1;
          this.clearCanvas('reload');
        }
      },
      // 全部删除
      deleteAll() {
        this.clearCanvas();
        this.focusIdx = -1;
        this.pointArr = [];
        this.position = [];
      }
    }
  }
</script>

<style scoped lang="less">
.ai-draw-area {
  width: 640px;
  height: 360px;
  position: absolute;
  z-index: 100;
  background: #fff;
  .img {
    width: 640px;
    height: 360px;
  }
  .canvas {
    position: absolute;
    top: 0;
    left: 0;
  }
}
</style>

校验四边形的各种方法


//判断两条线段是否相交
export const judgeIntersect = function (x1, y1, x2, y2, x3, y3, x4, y4) {
  if (
    !(
      Math.min(x1, x2) <= Math.max(x3, x4) &&
      Math.min(y3, y4) <= Math.max(y1, y2) &&
      Math.min(x3, x4) <= Math.max(x1, x2) &&
      Math.min(y1, y2) <= Math.max(y3, y4)
    )
  ) {
    return false;
  }
  const u = (x3 - x1) * (y2 - y1) - (x2 - x1) * (y3 - y1);
  const v = (x4 - x1) * (y2 - y1) - (x2 - x1) * (y4 - y1);
  const w = (x1 - x3) * (y4 - y3) - (x4 - x3) * (y1 - y3);
  const z = (x2 - x3) * (y4 - y3) - (x4 - x3) * (y2 - y3);
  return u * v <= 0.00000001 && w * z <= 0.00000001;
};

// 判断四边形是否有两条边是相交的,即是否是合格的四边形
export const hasIntersect = function (points) {
  let valid = false;
  for (let i = 0; i < 4; i++) {
    const arr = [0, 1, 2, 3];
    let num = 0;
    arr.forEach(a => {
      if (a !== i) {
        const xj = judgeIntersect(
          ...points[i],
          ...points[(i + 1) % 4],
          ...points[a],
          ...points[(a + 1) % 4]
        );
        if (xj) {
          num++;
        }
      }
    });
    if (num !== 2) {
      valid = true;
      break;
    }
  }
  return valid;
};

// 判断点是否在四边形内
export const isPointInPath = function (testx, testy, position) {
  let i = 0,
    j = 0,
    c = false;
  const nvert = 4;
  const vertx = position.map(p => p[0]);
  const verty = position.map(p => p[1]);

  for (i = 0, j = nvert - 1; i < nvert; j = i++) {
    if (
      verty[i] > testy != verty[j] > testy &&
      testx <
      ((vertx[j] - vertx[i]) * (testy - verty[i])) / (verty[j] - verty[i]) +
      vertx[i]
    ) {
      c = !c;
    }
  }
  return c;
};

// 计算四边形的面积
export const calcArea = function (poins) {
  const lens = [];
  let sum = 0;
  for(let i = 0; i < 4; i++) {
    const j = i+1 === 4 ? 0 : i+1;
    const len = Math.abs(Math.sqrt(Math.pow((poins[i][0] - poins[j][0]), 2) + Math.pow((poins[i][1] - poins[j][1]), 2)));
    lens.push(len);
    sum+=len;
  }
  const z = sum / 2;
  const area = Math.sqrt((z-lens[0])*(z-lens[1])*(z-lens[2])*(z-lens[3]));
  return area;
};

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值