canvas 画区域,功能支持画多区域,删除选中拖拽 已封装

343 阅读5分钟

canvas 画区域,功能支持画多区域,删除选中拖拽 已封装

前面做了个简单的,这次有时间用es6 的class 封装了一下 本人菜鸡,觉着好用的话点个赞,觉着不好用的勿喷 谢谢!!!

请添加图片描述

init.js

//canvas video 画区域
class CanvasServer {
  constructor(opts) {
    this.option = opts
    // 画布
    this.can = null;

    this.canvasSaves = null;
    // 画布上下文
    this.ctxSave = null;
    this.ctx = null;
    // 临时存放绘制的点
    this.pointArr = []
    //拷贝存放绘制的点
    this.cloneData = []
    //处理后后端需要的点
    this.arrData = []
    //画区域所需要的点
    this.newData = []

    this.startDrawArea = false //true 开始画区域 false 不能操作

    // 重合距离
    this.coincidentDistance = 10,
      //判断鼠标是否移动到起始点处,-1为否,1为是
      this.oIndex = -1,

      //默认线条颜色
      this.defaultLineColor = "rgba(64, 158, 255, 1)"
    //默认填充颜色
    this.defaultFllStyle = "rgba(161,195,255,0.4)"
    //默认选中颜色
    this.defaultSelectStyle = "rgba(255,0,0,0.3)"

    //线条颜色
    this.lineColor = ''
    //填充颜色
    this.fillStyle = '',
      //选中颜色
      this.selectStyle = ''

    //选中区域的下标
    this.moveIndex = null
    //判断鼠标下落方式,
    this.endMethod = false

    this.clickMove = false //是否移动
    this.completedDraw = true //是否完成画框
    this.MovePointArr = [] //移动后的数据
    this.copyArrData = []
    this.currentArrData = []
    // 圆点的定义
    this.arcInfo = {
      radius: 8,
      curPointColor: '#ff0000',
      pointColor: 'blue'
    }


    // 初始化canvas
    this._init(opts);


  }
  //初始
  _init(opts) {
    this.can = opts.canvas;
    this.can.width = opts.width;
    this.can.height = opts.height;
    this.canvasSaves = opts.canvasSave
    this.canvasSaves.width = opts.width
    this.canvasSaves.height = opts.height

    this.lineColor = opts?.lineColor || this.defaultLineColor
    this.fillStyle = opts?.fillStyle || this.defaultFllStyle
    this.selectStyle = opts?.selectStyle || this.defaultSelectStyle

    this.ctx = this.can.getContext(`2d`);
    this.ctx.strokeStyle = this.lineColor; //线条颜色
    this.ctx.fillStyle = this.fillStyle; //填充颜色

    this.ctxSave = this.canvasSaves.getContext(`2d`);
    this.ctxSave.strokeStyle = this.lineColor; //线条颜色
    this.ctxSave.fillStyle = this.fillStyle; //填充颜色
    // 初始化鼠标事件
    this._initCanvasEvent();
  }
  //添加事件
  _initCanvasEvent() {
    this.canvasSaves.addEventListener('mousemove', this._handleCanvasSaveMove);
    this.canvasSaves.addEventListener('click', this._handleCanvasSaveClick);
    this.canvasSaves.addEventListener('mousedown', this._handleMouseDown);
    this.canvasSaves.addEventListener('mouseup', this._handleMouseUp);
  }
  _handleMouseDown = (e) => {
    if (e.button == 2) return
    if (this.completedDraw && this.newData.some(i => i.checked)) {
      this.pointX = e.offsetX
      this.pointY = e.offsetY
      this.clickMove = true
    } else {
      this.completedDraw = false
    }
    // console.log('长按鼠标', e)
  }
  _handleMouseUp = (e, endMethod) => {
    if (e?.button == 2) return
    if (this.clickMove && this.completedDraw && this.currentArrData.length > 0) {
      this.clickMove = false;
      this.newData = JSON.parse(JSON.stringify(this.MovePointArr))
      this.arrData = JSON.parse(JSON.stringify(this.currentArrData))
      this.option.getData(this.arrData)
      this.endMethod = endMethod || false
      console.log('松下鼠标', this.arrData)
    }
    // console.log('松下鼠标', this.arrData)
  }
  //删除按钮操作
  deleteCanvas(delNone) {
    return delNone
  }
  //获取操作完成的数据
  getData(arrData) {
    return arrData
  }
  //区域回显
  echoCanvas = (pointColorArr) => {
    let arrData = pointColorArr.map(i => {
      return i.map(j => {
        let obj = {
          x: j.x,  //(Number(j.x) / this.scaling).toFixed(2),
          y: j.y   //(Number(j.y) / this.scaling).toFixed(2)
        }
        return obj
      })
    })
    this.cloneData = JSON.parse(JSON.stringify(arrData))
    let arr = arrData.map(item => {
      let obj = {
        ponitArr: item,
        checked: false,
        color: this.fillStyle
      }
      return obj
    })
    arr.forEach(item => {
      this.moveChecked(item.ponitArr, item.color)
    })
  }
  getGcd = (a, b) => {
    let n1, n2;
    if (a > b) {
      n1 = a;
      n2 = b;
    } else {
      n1 = b;
      n2 = a;
    }
    let remainder = n1 % n2;
    if (remainder === 0) {
      return n2;
    } else {
      return this.getGcd(n2, remainder)
    }
  }
  //绘制文字
  canvasText(text, point) {
    this.ctxSave.font = "20px Consolas"; //字体样式的属性
    this.ctxSave.textAlign = "center"; //设置文本对齐方式
    this.ctxSave.textBaseline = "middle"; //文本基线
    let textWidth = this.ctxSave.measureText(text).width;
    var canvasWidth = this.can.width;
    this.ctxSave.fillStyle = "red"; //字体颜色
    this.ctxSave.fillText(text, +point.x + 100, +point.y + 100); //绘制文字
    this.ctxSave.arc(point.x, point.y, 3, 0, Math.PI * 2); //基准点
  }

  /**
   * @description 射线法判断点是否在多边形内部
   * @param {Object} point 待判断的点,格式:{ x: X坐标, y: Y坐标 }
   * @param {Array} polygon 多边形顶点,数组成员的格式同 point
   * @return {Boolean} true false
   */
  // 判断点是否在多边形内
  pointInPolygon(targetPoint, targetPoints) {
    var leftPointCount = 0;
    var rightPointCount = 0;//左右点的个数
    var _points = [];
    //第一步:取出所有的点,并计算交点坐标
    for (var i = 0, _length = targetPoints.length - 1; i < _length; i++) {
      var p1 = targetPoints[i], p2 = targetPoints[i + 1];//取出当前点和当前点的下一个点
      var point = this._calcCrossoverPoint(targetPoint, p1, p2);
      //如果交点有效,则保存
      if (point) {
        _points.push(point);
      }
    }
    // 第二步:计算给定的坐标点,左右两边的交点个数,奇数在范围内,偶数则不在
    for (var j = 0, length = _points.length; j < length; j++) {
      var x = _points[j];
      if (x === targetPoint.x) {
        return false;//在线上,直接返回不在范围内
      } else {
        (targetPoint.x !== x && targetPoint.x > x) ? leftPointCount++ : rightPointCount++;
      }
    }
    //判断交点个数
    return (leftPointCount % 2 !== 0 && rightPointCount % 2 !== 0);
  }

  /**
   * 算交点坐标,坐标点在扫描行上或者上方时,交点无效
   * @param targetPoint
   * @param startPoint
   * @param endPoint
   * @returns {*}
   */
  _calcCrossoverPoint(targetPoint, startPoint, endPoint) {
    var crossoverPointX = startPoint.x - ((startPoint.y - targetPoint.y) * (startPoint.x - endPoint.x) / (startPoint.y - endPoint.y));
    //判断交点坐标是否有效,即交点在startPoint,endPoint构成的线段范围内
    if ((startPoint.y < targetPoint.y && endPoint.y >= targetPoint.y) || (endPoint.y < targetPoint.y && startPoint.y >= targetPoint.y)) {
      if ((crossoverPointX >= startPoint.x && crossoverPointX <= endPoint.x) || (crossoverPointX <= startPoint.x && crossoverPointX >= endPoint.x)) {
        return crossoverPointX;
      } else {
        return false;
      }
    } else {
      return false;
    }

  }

  // 通过多边形的各点位获得重心
  centerPoint(pointArr) {
    let X = 0,
      Y = 0;
    for (let i = 0; i < pointArr.length; i++) {
      X = X + +pointArr[i].x;
      Y = Y + +pointArr[i].y;
    }
    X = X / pointArr.length;
    Y = Y / pointArr.length;
    return { x: X, y: Y };
  }
  //鼠标左点击
  _handleCanvasSaveClick = (e) => {
    //判断是左点击并且处于画框状态
    if (!this.startDrawArea && e.button == 2) return
    //判断是选中状态并且没有正在画框
    if (this.newData.some(i => i.checked) && this.completedDraw) {
      return
    }
    //解决拖拽区域后到达边界后区域停止,鼠标继续前进造成click 点击事件触发画区域
    if (this.endMethod) {
      let event = {
        button: 0,
        offsetX: e.offsetX,
        offsetY: e.offsetY
      }
      this._handleMouseUp(event, true)
      return
    }

    if (e.offsetX) {
      this.pointX = e.offsetX;
      this.pointY = e.offsetY;

      var piX, piY;
      if (this.oIndex > 0 && this.pointArr.length > 0) {
        piX = this.pointArr[0].x;
        piY = this.pointArr[0].y;
        //画点
        this.makearc(
          this.ctx,
          piX,
          piY,
          this.GetRandomNum(2, 2),
          0,
          180,
          this.lineColor
        );
        // console.log('piX2', piX,piY)
        this.pointArr.push({ x: piX, y: piY });

        this.canvasSave(this.pointArr); //保存点线同步到另一个canvas
        this.saveCanvas(); //生成画布
      } else {
        piX = this.pointX;
        piY = this.pointY;
        this.makearc(
          this.ctx,
          piX,
          piY,
          this.GetRandomNum(2, 2),
          0,
          180,
          this.lineColor
        );
        // console.log('piX', piX,piY)
        this.pointArr.push({ x: piX, y: piY });

        this.canvasSave(this.pointArr); //保存点线同步到另一个canvas
      }
    }
  }
  //鼠标移入
  _handleCanvasSaveMove = (e) => {
    //先判断是操作状态 ,再判断是不是画区域状态
    if (this.startDrawArea && this.completedDraw) {
      if (this.clickMove) {
        // console.log('进来了', '')
        this.ctxSave.clearRect(0, 0, this.can.width, this.can.height);
        this.ctx.clearRect(0, 0, this.can.width, this.can.height);
        this.MovePointArr = [...this.newData]
        this.copyArrData = JSON.parse(JSON.stringify(this.arrData))
        this.MovePointArr.forEach(i => {
          i.checked = false,
            i.color = this.fillStyle
        })
        let copyArrData = this.copyArrData[this.moveIndex] || []

        let flag = true;
        // 多边形不可出画板
        for (let i = 0; i < copyArrData.length; i++) {
          if (+copyArrData[i].x + e.offsetX - this.pointX >= this.can.width ||
            +copyArrData[i].x + e.offsetX - this.pointX <= 0 ||
            +copyArrData[i].y + e.offsetY - this.pointY >= this.can.height ||
            +copyArrData[i].y + e.offsetY - this.pointY <= 0) {
            flag = false;
            let event = {
              button: 0,
              offsetX: e.offsetX,
              offsetY: e.offsetY
            }
            this._handleMouseUp(event, true)
          }
        }
        if (flag == false) return;
        // 表示点击之后移动
        let MovePointArr = copyArrData.map(ele => {
          return {
            x: (+ele.x + e.offsetX - this.pointX).toFixed(2),
            y: (+ele.y + e.offsetY - this.pointY).toFixed(2),
          };
        })
        let pointArrs = {
          checked: true,
          ponitArr: MovePointArr,
          color: this.selectStyle
        }
        this.moveIndex = JSON.parse(JSON.stringify(this.moveIndex))
        this.MovePointArr.splice(this.moveIndex, 1, pointArrs)
        this.copyArrData.splice(this.moveIndex, 1, MovePointArr)
        this.currentArrData = JSON.parse(JSON.stringify(this.copyArrData))

        this.MovePointArr.forEach(item => {
          this.moveChecked(item.ponitArr, item.color)
        })
        // console.log('MovePointArr',this.MovePointArr)
        // this.newData = [...this.MovePointArr]

      } else {
        this.ctxSave.clearRect(0, 0, this.can.width, this.can.height);
        this.ctx.clearRect(0, 0, this.can.width, this.can.height);
        this.moveCheckedRegion(e)
      }

      if (this.moveIndex != null) return
    }


    if (e.offsetX) {
      console.log('触发', '')
      this.pointX = e.offsetX;
      this.pointY = e.offsetY;
      var piX, piY;
      /*清空画布*/
      this.ctx.clearRect(0, 0, this.can.width, this.can.height);
      /*鼠标下跟随的圆点*/
      this.makearc(
        this.ctx,
        this.pointX,
        this.pointY,
        this.GetRandomNum(4, 4),
        0,
        180,
        this.lineColor
      );

      if (this.pointArr.length > 0) {
        if (
          this.pointX > this.pointArr[0].x - this.coincidentDistance &&
          this.pointX < this.pointArr[0].x + this.coincidentDistance &&
          this.pointY > this.pointArr[0].y - this.coincidentDistance &&
          this.pointY < this.pointArr[0].y + this.coincidentDistance
        ) {
          if (this.pointArr.length > 1) {
            piX = this.pointArr[0].x;
            piY = this.pointArr[0].y;
            // console.log(piX, piY);

            this.ctx.clearRect(0, 0, this.can.width, this.can.height);
            this.makearc(
              this.ctx,
              piX,
              piY,
              this.GetRandomNum(4, 4),
              0,
              180,
              this.lineColor
            );
            this.oIndex = 1;
          }
        } else {
          piX = this.pointX;
          piY = this.pointY;
          this.oIndex = -1;
        }
        /*开始绘制*/
        this.ctx.beginPath();
        this.ctx.moveTo(this.pointArr[0].x, this.pointArr[0].y);
        if (this.pointArr.length > 1) {
          for (var i = 1; i < this.pointArr.length; i++) {
            this.ctx.lineTo(this.pointArr[i].x, this.pointArr[i].y);
          }
        }
        this.ctx.lineTo(piX, piY);
        this.ctx.fillStyle = this.fillStyle; //填充颜色
        this.ctx.fill(); //填充
        this.ctx.stroke(); //绘制
      }
    }
  }
  moveCheckedRegion(e) {
    //处理回显数据
    if (!this.startDrawArea) return
    let arr = []
    if (this.cloneData.length > 0 && this.arrData.length == 0) {
      arr = this.cloneData.map(i => {
        return i
      })
    }
    this.cloneData = []
    this.arrData = [...this.arrData, ...arr]
    //鼠标滑动数据处理
    if (this.arrData.length > 0) {
      let checkedNum = []
      this.newData = this.arrData.map((i, index) => {
        // console.log('当前坐标',e.offsetX,e.offsetY)
        console.log('区域选中', this.pointInPolygon({ x: e.offsetX, y: e.offsetY, }, i))
        // console.log('选中的当前区域',i)
        // console.log('全部',this.arrData)
        let obj = {
          ponitArr: i,
          checked: this.pointInPolygon({ x: e.offsetX, y: e.offsetY, }, i),
          color: this.pointInPolygon({ x: e.offsetX, y: e.offsetY, }, i) ? this.selectStyle : this.fillStyle
        }
        return obj
      })

      //清除画布
      this.ctxSave.clearRect(0, 0, this.can.width, this.can.height);
      // let checkedNum = []
      //选中添加颜色
      this.newData.forEach((item, index) => {
        //当前鼠标下选中的判断
        if (item.checked) {
          checkedNum.push(item)
          this.moveIndex = index
        } else {
          this.moveChecked(item.ponitArr, item.color)
        }

      })
      // console.log('moveIndex',this.moveIndex)
      // console.log('checkedNum',checkedNum)
      //叠加时,选中数组最后的一个 层级最高
      if (checkedNum.length > 0) {
        if (checkedNum.length > 1) {
          checkedNum.forEach((i, j) => {
            if (checkedNum.length - 1 == j) {
              this.moveChecked(checkedNum[checkedNum.length - 1].ponitArr, checkedNum[checkedNum.length - 1].color)
            } else {
              this.moveChecked(i.ponitArr, this.fillStyle)
            }
          })
        } else {
          this.moveChecked(checkedNum[0].ponitArr, checkedNum[0].color)
        }
      } else {
        //选中后鼠标离开操作
        this.moveIndex = null
        this.option.deleteCanvas('none')
      }
    }
  }

  //删除区域
  delCanvas() {
    this.newData.splice(this.moveIndex, 1)
    this.arrData.splice(this.moveIndex, 1)
    //清除画布
    this.ctxSave.clearRect(0, 0, this.can.width, this.can.height);
    this.newData.forEach(i => {
      this.moveChecked(i.ponitArr, i?.color || this.fillStyle)
    })
    this.option.getData(this.arrData)
  }
  //绘制区域
  moveChecked = (pointArr, color) => {
    if (pointArr.length > 0) {
      /*开始绘制*/
      this.ctxSave.beginPath();
      this.ctxSave.moveTo(pointArr[0].x, pointArr[0].y);
      if (pointArr.length > 1) {
        for (var i = 1; i < pointArr.length; i++) {
          this.ctxSave.lineTo(pointArr[i].x, pointArr[i].y);
        }
      }
      this.ctxSave.lineTo(pointArr[0].x, pointArr[0].y);
      this.ctxSave.fillStyle = color; //填充颜色
      this.ctxSave.fill(); //填充
      this.ctxSave.stroke(); //绘制
    }
  }
  // 存储已生成的点线
  canvasSave = (pointArr) => {
    this.ctxSave.clearRect(0, 0, this.ctxSave.width, this.ctxSave.height);
    this.ctxSave.beginPath();
    if (pointArr.length > 1) {
      this.ctxSave.moveTo(pointArr[0].x, pointArr[0].y);
      for (var i = 1; i < pointArr.length; i++) {
        this.ctxSave.lineTo(pointArr[i].x, pointArr[i].y);
        this.ctxSave.fillStyle = this.fillStyle; //填充颜色
        //ctxSave.fill();
        this.ctxSave.stroke(); //绘制
      }
      this.ctxSave.closePath();


    }
  }
  /*生成画布 结束绘画*/
  saveCanvas() {
    this.ctx.clearRect(0, 0, this.can.width, this.can.height);
    this.ctxSave.closePath(); //结束路径状态,结束当前路径,如果是一个未封闭的图形,会自动将首尾相连封闭起来
    this.ctxSave.fill(); //填充
    this.ctxSave.stroke(); //绘制
    this.arrData.push(this.pointArr)
    this.pointArr = []
    this.completedDraw = true
    this.option.getData(this.arrData)
  }
  /*验证canvas画布是否为空函数*/
  isCanvasBlank(canvas) {
    var blank = document.createElement("canvas"); //创建一个空canvas对象
    blank.width = canvas.width;
    blank.height = canvas.height;
    return canvas.toDataURL() == blank.toDataURL(); //为空 返回true
  }
  /*canvas生成圆点*/
  GetRandomNum(Min, Max) {
    var Range = Max - Min;
    var Rand = Math.random();
    return Min + Math.round(Rand * Range);
  }
  makearc = (ctx, x, y, r, s, e, color) => {
    ctx.clearRect(0, 0, this.can.width, this.can.height); //清空画布
    ctx.beginPath();
    ctx.fillStyle = color;
    ctx.arc(x, y, r, s, e);
    ctx.fill();
  }
  resetCanvas() {
    this.ctx.clearRect(0, 0, this.can.width, this.can.height);
    this.ctxSave.clearRect(0, 0, this.canSave.width, this.canSave.height);
    this.pointArr = [];
  }
  reset() {
    if (this.ctx || this.ctxSave) {
      this.ctx.clearRect(0, 0, this.can.width, this.can.height);
      this.ctxSave.clearRect(0, 0, this.can.width, this.can.height);
      this.pointArr = [];
      this.arrData = []
      this.newData = []
      this.cloneData = []
      this.clickMove = false //是否移动
      this.endMethod = false
      this.completedDraw = true //是否完成画框
      this.MovePointArr = [] //移动后的数据
      this.copyArrData = []
      this.currentArrData = []
      this.startDraw = false
      this.startDrawArea = false
    }
  }
};
export default CanvasServer

index.vue

因为我这个是在video 上操作画区域,所有要视频成功加载后获取video 的真实宽高做的比例计算,请根据自己需求来修改。


      //初始化
      async initCanvas(pointColorArr) {
        if (!this.videoUrl) return;
        this.videoShow = true
        var video = document.getElementById(this.videoId)
        this.ctxSave = document.getElementById('canvas')
        this.canSave = document.getElementById('canvasSave')
        this.del = document.getElementById('del')
        video.oncanplay = () => {
          let videoWidth = video.videoWidth
          let videoHeight = video.videoHeight
          let heights = video.clientHeight //video 的高度
          let Gcd = this.getGcd(videoWidth, videoHeight) //获取最大公约数
          let width = videoWidth / Gcd //宽高的比例
          let height = videoHeight / Gcd //宽高的比例
          let itemHeight = heights / height //1份占的值
          this.scaling = (videoHeight / heights).toFixed(2) //外面高与真实高的比
          let widths = itemHeight * width //得到的最后真实video宽度
          this.CanvasServer = new CanvasServer({
            canvas: this.ctxSave,
            canvasSave: this.canSave,
            width: widths.toFixed(2),
            height: heights.toFixed(2),
            deleteCanvas: (val) => {
              if (this.del) {
                this.del.style.display = val
              }
            },
            getData: (newArr) => {
              this.arrData = newArr
              this.$emit('finish',newArr)
            }
          })
          let arr = pointColorArr.map(i => {
            return i.map(j => {
              let obj = {
                x: (Number(j.x) / this.scaling).toFixed(2),
                y: (Number(j.y) / this.scaling).toFixed(2)
              }
              return obj
            })
          })
          this.CanvasServer.startDraw = true
          this.CanvasServer.echoCanvas(arr || [])
          this.startDraw = this.CanvasServer.startDraw
          this.startDrawArea = this.CanvasServer.startDrawArea
        }
        video.onerror=()=>{
            this.CanvasServer.reset()
        }
      },
       getGcd(a, b) {
        let n1, n2;
        if (a > b) {
          n1 = a;
          n2 = b;
        } else {
          n1 = b;
          n2 = a;
        }
        let remainder = n1 % n2;
        if (remainder === 0) {
          return n2;
        } else {
          return this.getGcd(n2, remainder)
        }
      },