javascript技巧

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript技巧 > JS canvas图形标注

JS利用原生canvas实现图形标注功能

作者:一枕槐安

这篇文章主要为大家详细介绍了JS如何利用原生canvas实现图形标注功能,支持矩形、多边形、线段、圆形等已绘制的图形进行缩放,移动,需要的可以参考下

由于工作需要,项目中要求实现一个功能—在视频或者图片上进行图形标注,支持矩形、多边形、线段、圆形、折线,已绘制的图形可以进行缩放,移动。

完整功能源代码在这个仓库,感兴趣的可以clone下来跑一下。

接下来我将实现一个dmeo来展示其中最简单的图形—矩形的创建。

初始化页面

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>

    <style>
      body {
        margin: 0;
        padding: 0;
        height: 100vh;
        display: flex;
        align-items: center;
        justify-content: center;
      }

      canvas {
        border: 1px solid saddlebrown;
        background-color: beige;
      }
    </style>
  </head>
  <body>
    <canvas id="canvasIdChart" class="canvas">您的浏览器不支持canvas1元素</canvas>
    <script src="./drawClass.js"></script>
    <script>
      const chart = new Chart('canvasIdChart')
    </script>
  </body>
</html>

该页面只有一个canvas,而功能的实现在drawClass里面,这里我实现了一个Chart类,需要传入一个canvas的id,表示后续的绘制功能都是在这个canvas上实现的。

创建基础类Chart

首先梳理下要实现一个图形标注需要涉及到什么事件

用户在canvas上按下鼠标,说明用户想要开始画一个矩形进行标注了,所以canvas上需要绑定一个onmousedown 事件。相应的绘制过程也涉及到了onmousemove 事件,而用户松开鼠标表示绘制已完成,即需要onmouseup事件。同时为了兼容这样一种情况—用户在绘制过程中鼠标移出了canvas的范围(此时的鼠标事件都是绑定在canvas上的),这种情况无法触发canvas的onmouseup事件,所以为canvas加上onmouseout,鼠标移出默认代表绘制完成。

初始化代码如下

class Chart {
  constructor(canvasId) {
    this.cvs = document.getElementById(canvasId)
    this.ctx = this.cvs.getContext('2d')
    this.shapes = [] // 保存图形数据的数组
    this.init() // 初始化canvas得宽高
    this.bindEvent() // 为canvas绑定鼠标事件
    this.draw() // canvas的绘制
    this.isClickDown = false // 当前鼠标是否按下
    this.currentShape = null // 当前选中的图形
  }
  init() {
    const w = 1000,
      h = 600
    this.cvs.width = w
    this.cvs.height = h
  }
  bindEvent() {
    this.cvs.onmousedown = (e) => {
      this.isClickDown = true
			// 鼠标按下的时候创建其他鼠标事件
      that.cvs.onmousemove = function (e){}
      that.cvs.onmouseup = function (e){}
      that.cvs.onmouseout = function (e){}
    }
  }
  draw() {}
}

为了后续拓展,我将各种图形封装成一个个类,如矩形封装成class **Rectangle 。这样做的好处是我们只需要规定这样的图形类内部都有某些属性和方法给Chart中的draw调用就行,比如我们可以把绘制矩形的实现写在Rectangle类的draw函数上,而在Chart中的draw上只需调用new Rectangle().draw() 即可。要注意的是,draw方法是随着mousemove而要不断的刷新canvas重新绘制的,此时最好使用requestAnimationFrame方法让浏览在合适的时机调用draw。此时Chart的draw方法如下:**

class Chart {
	....
	draw() {
    requestAnimationFrame(this.draw.bind(this))
    this.ctx.clearRect(0, 0, this.cvs.width, this.cvs.height) // 首先清空canvas
    // 将shapes里面的图形重新绘制
    for (const s of this.shapes) {
      s.draw()
    }
  }
}

实现Rectangle类

首先看一下矩形需要的几点要素

初始代码如下:

/**
 * 生成唯一ID
 * @param {Number} length  生成id的长度
 * @returns
 */
const genID = (length = 3) => {
  return Number(Math.random().toString().substr(3, length) + Date.now()).toString(36)
}

class Rectangle {
  constructor(el,color, startX, startY) {
    this.el = el // 保存需要绘制的载体
    this.shapeType = 'RECT' // 当前几何图形的类别
    this.color = color // 填充的颜色
    // 初始化起点,默认起点和终点一样
    this.startX = startX
    this.startY = startY
    this.endX = startX
    this.endY = startY
    this.action = 'CREATE'  // 当前的操作类型
    this.id = genID() // 当前实例的唯一id
  }

  get minX() {
    return Math.min(this.startX, this.endX)
  }

  get maxX() {
    return Math.max(this.startX, this.endX)
  }

  get minY() {
    return Math.min(this.startY, this.endY)
  }

  get maxY() {
    return Math.max(this.startY, this.endY)
  }

  draw() {}
}

实现Rectangle类的draw函数 幸运的是canvas得API中已经有了绘制矩形的方法,不用我们一条线一条线的自己画了。该方法是 canvas.rect(startX,startY,width,height) 四个参数分别是矩形的起点横坐标和纵坐标,矩形的宽高,这也是为什么需要minX和minY。代码如下:

class Rectangle {
	...
	 draw() {
    this.el.beginPath()  // 画笔起始点
    this.el.rect(
      this.minX,
      this.minY,
      (this.maxX - this.minX),
      (this.maxY - this.minY)
    )
    this.el.fillStyle = this.color // 填充颜色
    this.el.fill()
    this.el.strokeStyle = '#fff' // 边框的颜色
    this.el.lineCap = 'square'
    this.el.lineWidth = 3 // 边框粗细
    this.el.stroke() // 画笔终点
  }
}

判断鼠标是否点击在矩形内部,用来判断是新建还是拖动矩形。 判断一个坐标点是否落在矩形的内部很好判断,就是通过minX、minY、maxX、maxY和传入的x、y对比就可以了

class Rectangle {
	...

  isInside(x, y) {
    return x >= this.minX && x <= this.maxX && y >= this.minY && y <= this.maxY
  }
}

实现鼠标事件

在鼠标事件中,获取鼠标的坐标点属性有好几种,例如clientX/screenX/offsetX/pageX等,如需分别这几个属性的不同,可参考此篇文章,目前我用的是offsetX,因为这个属性是获取相对于事件对象的位置,此时事件对象就是canvas。

onmousedown事件 在mousedown事件我们需要做两件事

判断鼠标落点是否在已有矩形的内部,如果有就是当前矩形的拖拽事件,如果不是则是新建矩形。 判断是否落在集合图形的内部前面我们在几何图形类中已经约定好了一个isInside方法,此时只需遍历shapes数组,依次调用每一项的isInside 方法就好。

...
// 遍历数组
getShapes(x, y) {
    for (let i = this.shapes.length - 1; i >= 0; i--) {
      const s = this.shapes[i]
      if (s.isInside(x, y)) {
        return s
      }
    }
    return null
  }

如果返回的是null,则进入新建逻辑。新建一个矩形的操作很简单,就是创建一个Rectangle的实例,new Rectangle(...) 并把new出来的实例添加进shapes数组中。并且此时的onmousemove事件也很简单,只需把实例中的endX、endY修改成当前move的坐标点。

// 新建
const shape = new Rectangle(that.ctx, '#f00', clickX, clickY)
that.shapes.push(shape)
that.cvs.onmousemove = function (e) {
  shape.endX = e.offsetX
  shape.endY = e.offsetY
}

此时如果返回的不是null,说明找到了当前鼠标点击的图形,执行的是拖拽逻辑。而拖拽逻辑需要计算矩形被拖拽的偏移量,如下图所示:

此时该矩形的四个坐标都要进行相应的同步修改,并且还要判断是否移动超出了边界。

完整代码如下所示:

class Chart{
	...
	bindEvent() {
		const that = this
    this.cvs.onmousedown = function (e) {
			this.isClickDown = true
      const [clickX, clickY] = [e.offsetX, e.offsetY]
      const shape = that.getShapes(clickX, clickY)
			if (shape) {
        // 如果找到图形,说明是拖拽
        shape.action = 'MOVE'
        const { startX, startY, endX, endY } = shape
        that.cvs.onmousemove = function (e) {
          const disX = e.offsetX - clickX
          const disY = e.offsetY - clickY

          const newStartX = startX + disX
          const newEndX = endX + disX
          const newStartY = startY + disY
          const newEndY = endY + disY
          // 判断是否超出边界(矩形)
          if (newStartX < 0 || newEndX > that.cvs.width || newStartY < 0 || newEndY > that.cvs.height) {
            return
          }
          shape.startX = newStartX
          shape.endX = newEndX
          shape.startY = newStartY
          shape.endY = newEndY
        }
      } else {
        // 没找到,则是新建图形
        const shape = new Rectangle(that.ctx, '#f00', clickX, clickY)
        that.shapes.push(shape)
        that.cvs.onmousemove = function (e) {
          shape.endX = e.offsetX
          shape.endY = e.offsetY
        }
		}
	}

	getShapes(x, y) {
    for (let i = this.shapes.length - 1; i >= 0; i--) {
      const s = this.shapes[i]
      if (s.isInside(x, y)) {
        return s
      }
    }
    return null
  }
}

鼠标按下初始化onmousemove和onmouseup等事件。 onmousemove、onmouseup和onmouseout事件其实很简单,只需要取消鼠标移动事件和鼠标抬起事件即可

    that.cvs.onmouseup = function () {
      this.isClickDown = false
      that.cvs.onmousemove = null
      that.cvs.onmouseup = null
    }
    that.cvs.onmouseout = function () {
      this.isClickDown = false
      that.cvs.onmousemove = null
      that.cvs.onmouseup = null
    }

最终代码

此时,一个矩形的简单绘制和拖拽就完成了,后续的缩放需要的还可以单独开一篇文章讲讲。按照这种方式,其他几何图形我们可以新建相对应地类就行了,拓展起来就很方便了。

完整代码

// drawClass.js
/**
 * 生成唯一ID
 * @param {Number} length  生成id的长度
 * @returns
 */
const genID = (length = 3) => {
  return Number(Math.random().toString().substr(3, length) + Date.now()).toString(36)
}

class Chart {
  constructor(canvasId) {
    this.cvs = document.getElementById(canvasId)
    this.ctx = this.cvs.getContext('2d')
    this.shapes = [] // 保存图形数据的数组
    this.init() // 初始化canvas得宽高
    this.bindEvent() // 为canvas绑定鼠标事件
    this.draw() // canvas的绘制
    this.isClickDown = false // 当前鼠标是否按下
    this.isPolygon = false
    this.currentShape = null // 当前选中的图形
  }

  init() {
    const w = 1000,
      h = 600
    this.cvs.width = w
    this.cvs.height = h
  }

  bindEvent() {
    const that = this
    this.cvs.onmousedown = function (e) {
      this.isClickDown = true
      const [clickX, clickY] = [e.offsetX, e.offsetY]
      const shape = that.getShapes(clickX, clickY)
      if (shape) {
        // 如果找到图形,说明是拖拽
        shape.action = 'MOVE'
        const { startX, startY, endX, endY } = shape
        that.cvs.onmousemove = function (e) {
          const disX = e.offsetX - clickX
          const disY = e.offsetY - clickY

          const newStartX = startX + disX
          const newEndX = endX + disX
          const newStartY = startY + disY
          const newEndY = endY + disY
          // 判断是否超出边界(矩形)
          if (newStartX < 0 || newEndX > that.cvs.width || newStartY < 0 || newEndY > that.cvs.height) {
            return
          }
          shape.startX = newStartX
          shape.endX = newEndX
          shape.startY = newStartY
          shape.endY = newEndY
        }
      } else {
        // 没找到,则是新建图形
        const shape = new Rectangle(that.ctx, '#f00', clickX, clickY)
        that.shapes.push(shape)
        that.cvs.onmousemove = function (e) {
          shape.endX = e.offsetX
          shape.endY = e.offsetY
          const radius = Math.sqrt(
            Math.pow(e.offsetX - clickX, 2) + Math.pow(e.offsetY - clickY, 2)
          )
          shape.radius = radius
        }
      }
      that.cvs.onmouseup = function () {
        this.isClickDown = false
        that.cvs.onmousemove = null
        that.cvs.onmouseup = null
      }
      that.cvs.onmouseout = function () {
        this.isClickDown = false
        that.cvs.onmousemove = null
        that.cvs.onmouseup = null
      }
    }
  }

  getShapes(x, y) {
    for (let i = this.shapes.length - 1; i >= 0; i--) {
      const s = this.shapes[i]
      if (s.isInside(x, y)) {
        return s
      }
    }
    return null
  }

  draw() {
    requestAnimationFrame(this.draw.bind(this))
    this.ctx.clearRect(0, 0, this.cvs.width, this.cvs.height) // 首先清空canvas
    // 将shapes里面的图形重新绘制
    for (const s of this.shapes) {
      s.draw()
    }
  }
}

class Rectangle {
  constructor(el,color, startX, startY) {
    this.el = el // 保存需要绘制的载体
    this.shapeType = 'RECT' // 当前几何图形的类别
    this.color = color // 填充的颜色
    // 初始化起点,默认起点和终点一样
    this.startX = startX
    this.startY = startY
    this.endX = startX
    this.endY = startY
    this.action = 'CREATE'  // 当前的操作类型
    this.id = genID() // 当前实例的唯一id
  }

  get minX() {
    return Math.min(this.startX, this.endX)
  }

  get maxX() {
    return Math.max(this.startX, this.endX)
  }

  get minY() {
    return Math.min(this.startY, this.endY)
  }

  get maxY() {
    return Math.max(this.startY, this.endY)
  }

  draw() {
    this.el.beginPath()  // 画笔起始点
    this.el.rect(
      this.minX,
      this.minY,
      (this.maxX - this.minX),
      (this.maxY - this.minY)
    )
    this.el.fillStyle = this.color // 填充颜色
    this.el.fill()
    this.el.strokeStyle = '#fff' // 边框的颜色
    this.el.lineCap = 'square'
    this.el.lineWidth = 3 // 边框粗细
    this.el.stroke() //画笔的终点
  }

  isInside(x, y) {
    return x >= this.minX && x <= this.maxX && y >= this.minY && y <= this.maxY
  }
}

到此这篇关于JS利用原生canvas实现图形标注功能的文章就介绍到这了,更多相关JS canvas图形标注内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文