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

 更新时间:2024年03月07日 14:24:23   作者:一枕槐安  
这篇文章主要为大家详细介绍了JS如何利用原生canvas实现图形标注功能,支持矩形、多边形、线段、圆形等已绘制的图形进行缩放,移动,需要的可以参考下
(福利推荐:【腾讯云】服务器最新限时优惠活动,云服务器1核2G仅99元/年、2核4G仅768元/3年,立即抢购>>>:9i0i.cn/qcloud

(福利推荐:你还在原价购买阿里云服务器?现在阿里云0.8折限时抢购活动来啦!4核8G企业云服务器仅2998元/3年,立即抢购>>>:9i0i.cn/aliyun

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

完整功能源代码在这个仓库,感兴趣的可以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="http://9i0i.com/pic.php?p=./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类

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

  • 填充的颜色
  • 矩形的初始位置(即鼠标按下的位置)
  • 矩形的终点位置(即鼠标抬起的位置,也是鼠标移动的最后位置)
  • 矩形需要绘制在哪里(此时需要绘制在canvas上)
  • 矩形的初始位置可能会大于终点位置(鼠标按下后往左上角绘制),此时可用class语法中的get方法获取minX、maxX、minY、maxY
  • 因为同类几何图形的实例可能会有很多,所以每个实例需要一个id

初始代码如下:

/**
 * 生成唯一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图形标注内容请搜索程序员之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持程序员之家!

相关文章

  • Mac系统下Webstorm快捷键整理大全

    Mac系统下Webstorm快捷键整理大全

    webstorm应该是目前最强的js编辑器了,结合sublime text可以很效率的开发项目。下面这篇文章主要给大家整理了一些Mac系统下Webstorm快捷键的相关资料,需要的朋友可以参考借鉴,下面来一起看看吧。
    2017-05-05
  • javascript+css3开发打气球小游戏完整代码

    javascript+css3开发打气球小游戏完整代码

    这是一个简单但是印象深刻的小游戏,打气球小游戏的实现代码,主要基于js和css3,基于css3画气球,具体实现代码大家参考下本文
    2017-11-11
  • javascript二维数组和对象的深拷贝与浅拷贝实例分析

    javascript二维数组和对象的深拷贝与浅拷贝实例分析

    这篇文章主要介绍了javascript二维数组和对象的深拷贝与浅拷贝,结合实例形式分析了JavaScript针对数组与对象的深拷贝及浅拷贝相关操作技巧,需要的朋友可以参考下
    2019-10-10
  • js实现页面跳转的五种方法推荐

    js实现页面跳转的五种方法推荐

    下面小编就为大家带来一篇js实现页面跳转的五种方法推荐。小编觉得挺不错的。现在分享给大家。给大家参考一下。
    2016-03-03
  • javascript自定义函数参数传递为字符串格式

    javascript自定义函数参数传递为字符串格式

    本节主要介绍了通过自定义javascript函数传递参数为字符串格式的,用this传递、引号缺省,示例如下
    2014-07-07
  • 浅聊一下JavaScript中的LHS和RHS查询

    浅聊一下JavaScript中的LHS和RHS查询

    在日常编码中,我们通常关注代码的逻辑和功能,但很少深入思考编译器在幕后的工作,今天我们将学习一下LHS(Left-Hand Side)和RHS(Right-Hand Side)查询,它们在JavaScript编译和执行中的关键作用,以及在我们的日常开发中是如何发挥作用的,需要的朋友可以参考下
    2023-11-11
  • JavaScript每天必学之基础知识

    JavaScript每天必学之基础知识

    JavaScript每天必学之基础知识,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2016-09-09
  • webpack打包中path.resolve(__dirname, 'dist')的含义解析

    webpack打包中path.resolve(__dirname, 'dist')的含义解

    这篇文章主要介绍了webpack打包中path.resolve(__dirname, 'dist')的含义解析,path:path.resolve(__dirname, 'dist')就是在打包之后的文件夹上拼接了一个文件夹,在打包时,直接生成,本文给大家讲解的非常详细,需要的朋友可以参考下
    2023-05-05
  • javaScript实现滚动新闻的方法

    javaScript实现滚动新闻的方法

    这篇文章主要介绍了javaScript实现滚动新闻的方法,涉及javascript实现页面滚动的相关技巧,具有一定参考借鉴价值,需要的朋友可以参考下
    2015-07-07
  • JS弹出窗口插件zDialog简单用法示例

    JS弹出窗口插件zDialog简单用法示例

    这篇文章主要介绍了JS弹出窗口插件zDialog简单用法,结合简单实例形式分析了zDialog插件弹出窗口的基本使用方法与参数含义,需要的朋友可以参考下
    2016-06-06

最新评论

?


http://www.vxiaotou.com