如何在Canvas中添加事件?

更新日期: 2019-05-21阅读: 1.5k标签: canvas
原文来自:https://segmentfault.com/a/1190000019239594

作为一个前端,给元素添加事件是一件司空见惯的事情。可是在Canvas中,其所画的任何东西都是无法获取的,更别说添加事件,那么我们对其就束手无策了吗?当然不是的!我们在平时项目中肯定都用过许多Canvas的框架,我们发现事件在这些框架中已经使用的十分成熟了,而且并没有出现特别严重的问题。那么我们可以肯定的是,事件在Canvas中并不是一个无法触及的事情。  


一个傻瓜式的方式

我们都知道一个元素在触发一个事件时,其鼠标的位置基本处于该元素之上,那么我们就自然而然的想到通过当前鼠标的位置以及物体所占据的位置进行比对,从而我们就能得出该物体是否应触发事件。这种方式比较简单,我就不用代码演示了,不过既然我叫它傻瓜式的方式,很明显它不是一个有效的解决方式。因为物体所占据的位置并不一定是十分容易获取,如果是矩形、圆形等我们还能通过一些简单的公式获取其占据的位置,可是在复杂点的多边形,甚至是多边形的某些边是弧线的,显而易见,我们这时候再获取其所占据的位置时是一件极其复杂且难度极大的事情,所以这种方式只适合自己在做一些demo中使用,并不适用于大多数的情况。


一个较聪明的方式

既然上面这种方式碰壁了,那么我们只能另辟蹊径。在翻阅Canvasapi的时候,找到了一个方法isPointInPath,貌似正是我们苦苦寻找的良药。


介绍isPointInPath

isPointInPath的作用:顾名思义,我们很直观的可以知道该方法用以判断点是否处于路径当中。

isPointInPath的入参出参:ctx.isPointInPath([path, ]x, y [, fillRule]),该方法的参数有4个,其中path和fillRule为选填,x和y为必填。我们依次介绍4个参数。

path:看到这个参数,我开始以为是beginPath或者closePath的返回值,很可惜的是这两个方法并没有返回值,在查阅了资料后,发现是Path2D构造函数new的对象。Path2D构造函数具体用法。不过可惜的是该方法可能由于兼容性的问题,目前看了一些开源框架都还未使用。

x,y:这两个参数很好理解,就是x轴和y轴的距离,需要注意的是,其相对位置是Canvas的左上角。

fillRule:nonzero(默认),evenodd。非零环绕规则和奇偶规则是图形学中判断一个点是否处于多边形内的规则,其中非零环绕规则是Canvas的默认规则。想具体了解这两种规则的,可以自己去查阅资料,这里就不增加篇幅介绍了。

上面介绍完了入参,那么isPointInPath方法的出参想必大家都可以猜到了,就是true和false。


使用isPointInPath

上一节介绍完isPointInPath方法后,我们现在就来使用它吧。

先来一个简单的demo:

  const canvas = document.getElementById('canvas')
  const ctx = canvas.getContext('2d')

  ctx.beginPath()
  ctx.moveTo(10, 10)
  ctx.lineTo(10, 50)
  ctx.lineTo(50, 50)
  ctx.lineTo(50, 10)
  ctx.fillStyle= 'black'
  ctx.fill()
  ctx.closePath()

  canvas.addEventListener('click', function (e) {
    const canvasInfo = canvas.getBoundingClientRect()
    console.log(ctx.isPointInPath(e.clientX - canvasInfo.left, e.clientY - canvasInfo.top))
  })

如图所示,灰色部分为Canvas所占据的区域,黑色为我们实际添加事件的区域,在我们点击黑色区域后,实际也的确如我们所愿,打印出来的值为true。貌似Canvas的事件监听就这么简单的解决了,不过事情真有这么简单吗。显然是不可能的!我们再来举个例子,这时候有两个区域,并且我们需要分别给其绑定不同的事件:

  const canvas = document.getElementById('canvas')
  const ctx = canvas.getContext('2d')

  ctx.beginPath()
  ctx.moveTo(10, 10)
  ctx.lineTo(10, 50)
  ctx.lineTo(50, 50)
  ctx.lineTo(50, 10)
  ctx.fillStyle= 'black'
  ctx.fill()
  ctx.closePath()

  ctx.beginPath()
  ctx.moveTo(100, 100)
  ctx.lineTo(100, 150)
  ctx.lineTo(150, 150)
  ctx.lineTo(150, 100)
  ctx.fillStyle= 'red'
  ctx.fill()
  ctx.closePath()

  canvas.addEventListener('click', function (e) {
    const canvasInfo = canvas.getBoundingClientRect()
    console.log(ctx.isPointInPath(e.clientX - canvasInfo.left, e.clientY - canvasInfo.top))
  })

这个时候,结果就不再如同我们所预计的一样,当点击其中黑色区域时,打印的值为false,点击红色区域时,打印的值为true。

其实原因很简单,因为上述代码,我们实际创建了两个Path,而isPointInPath方法实际只检测当前点是否处于最后一个Path当中,而例子中红色区域为最后一个Path,所以只有点击红色区域时,isPointInPath方法才能判断为true。现在我们改造一下代码:

  const canvas = document.getElementById('canvas')
  const ctx = canvas.getContext('2d')
  let drawArray = []

  function draw1 () {
    ctx.beginPath()
    ctx.moveTo(10, 10)
    ctx.lineTo(10, 50)
    ctx.lineTo(50, 50)
    ctx.lineTo(50, 10)
    ctx.fillStyle= 'black'
    ctx.fill()
  }

  function draw2 () {
    ctx.beginPath()
    ctx.moveTo(100, 100)
    ctx.lineTo(100, 150)
    ctx.lineTo(150, 150)
    ctx.lineTo(150, 100)
    ctx.fillStyle= 'red'
    ctx.fill()
    ctx.closePath()
  }

  drawArray.push(draw1, draw2)  

  drawArray.forEach(it => {
    it()
  })

  canvas.addEventListener('click', function (e) {
    ctx.clearRect(0, 0, 400, 750)
    const canvasInfo = canvas.getBoundingClientRect()
    drawArray.forEach(it => {
      it()
      console.log(ctx.isPointInPath(e.clientX - canvasInfo.left, e.clientY - canvasInfo.top))
    })
  })

上面的代码我们进行了一个很大的改造,我们将每个Path放入到一个单独的函数当中,并将它们push到一个数组当中。当触发点击事件时,我们清空Canvas,并遍历数组重新绘制,每当绘制一个Path进行一次判断,从而在调用isPointInPath方法时,我们能实时的获取当前的最后一个Path,进而判断出当前点所处的Path当中。

现在我们已经间接的实现了对每个Path的单独事件监听,可是其实现的方式需要一次又一次的重绘,那么有办法不需要重绘就能监听事件吗?

首先我们需要知道一次又一次重绘的原因是因为isPointInPath方法是监听的最后一个Path,不过我们在介绍这个方法的时候,说过其第一个参数是一个Path对象,当我们传递了这个参数后,Path就不再去取最后一个Path而是使用我们传递进去的这个Path,现在我们来个demo来验证其可行性:

  const canvas = document.getElementById('canvas')
  const ctx = canvas.getContext('2d')

  const path1 = new Path2D();
  path1.rect(10, 10, 100,100);
  ctx.fill(path1)
  const path2 = new Path2D();
  path2.moveTo(220, 60);
  path2.arc(170, 60, 50, 0, 2 * Math.PI);
  ctx.stroke(path2)

  canvas.addEventListener('click', function (e) {
    console.log(ctx.isPointInPath(path1, e.clientX, e.clientY))
    console.log(ctx.isPointInPath(path2, e.clientX, e.clientY))
  })

如上图所示,我们点击了左边图形,打印true,false;点击右边图形,打印false,true。打印的结果表明是没有问题的,不过由于其兼容性还有待加强,所以目前建议还是使用重绘方式来监听事件。


结语

Canvas的事件监听讲到这里基本就差不多了,原理很简单,大家应该都能掌握。github地址,欢迎start


附录

自己写的一个demo

  const canvas = document.getElementById('canvas')

  class rectangular {
    constructor (
      ctx, 
      {
        top = 0,
        left = 0,
        width = 30,
        height = 50,
        background = 'red'
      }
    ) {
      this.ctx = ctx
      this.top = top
      this.left = left
      this.width = width
      this.height = height
      this.background = background
    }

    painting () {
      this.ctx.beginPath()
      this.ctx.moveTo(this.left, this.top)
      this.ctx.lineTo(this.left + this.width, this.top)
      this.ctx.lineTo(this.left + this.width, this.top + this.height)
      this.ctx.lineTo(this.left, this.top + this.height)
      this.ctx.fillStyle = this.background
      this.ctx.fill()
      this.ctx.closePath()
    }

    adjust (left, top) {
      this.left += left
      this.top += top
    }
  }

  class circle {
    constructor (
      ctx, 
      {
        center = [],
        radius = 10,
        background = 'blue'
      }
    ) {
      this.ctx = ctx
      this.center = [center[0] === undefined ? radius : center[0], center[1] === undefined ? radius : center[1]]
      this.radius = radius
      this.background = background
    }

    painting () {

      this.ctx.beginPath()
      this.ctx.arc(this.center[0], this.center[1], this.radius, 0, Math.PI * 2, false)
      this.ctx.fillStyle = this.background
      this.ctx.fill()
      this.ctx.closePath()
    }

    adjust (left, top) {
      this.center[0] += left
      this.center[1] += top
    }
  }

  class demo {
    constructor (canvas) {
      this.canvasInfo = canvas.getBoundingClientRect()
      this.renderList = []
      this.ctx = canvas.getContext('2d')
      this.canvas = canvas
      this.rectangular = (config) => {
        let target = new rectangular(this.ctx, {...config})
        this.addRenderList(target)
        return this
      }

      this.circle = (config) => {
        let target = new circle(this.ctx, {...config})
        this.addRenderList(target)
        return this
      }
      this.addEvent()
    }

    addRenderList (target) {
      this.renderList.push(target)
    }

    itemToLast (index) {
      const lastItem = this.renderList.splice(index, 1)[0]

      this.renderList.push(lastItem)
    }

    painting () {
      this.ctx.clearRect(0, 0, this.canvasInfo.width, this.canvasInfo.height)
      this.renderList.forEach(it => it.painting())
    }

    addEvent () {
      const that = this
      let startX, startY

      canvas.addEventListener('mousedown', e => {
        startX = e.clientX
        startY = e.clientY
        let choosedIndex = null
        this.renderList.forEach((it, index) => {
          it.painting()
          if (this.ctx.isPointInPath(startX, startY)) {
            choosedIndex = index
          }
        })
        
        if (choosedIndex !== null) {
          this.itemToLast(choosedIndex)
        }

        document.addEventListener('mousemove', mousemoveEvent)
        document.addEventListener('mouseup', mouseupEvent)
        this.painting()
      })

      function mousemoveEvent (e) {
        const target = that.renderList[that.renderList.length - 1]
        const currentX = e.clientX
        const currentY = e.clientY
        target.adjust(currentX - startX, currentY - startY)
        startX = currentX
        startY = currentY
        that.painting()
      }

      function mouseupEvent (e) {
        const target = that.renderList[that.renderList.length - 1]
        const currentX = e.clientX
        const currentY = e.clientY

        target.adjust(currentX - startX, currentY - startY)
        startX = currentX
        startY = currentY
        that.painting()
        document.removeEventListener('mousemove', mousemoveEvent)
        document.removeEventListener('mouseup', mouseupEvent)
      }
    }
  }

  const yes = new demo(canvas)
    .rectangular({})
    .rectangular({top: 60, left: 60, background: 'blue'})
    .rectangular({top: 30, left: 20, background: 'green'})
    .circle()
    .circle({center: [100, 30], background: 'red', radius: 5})
    .painting()

链接: https://www.fly63.com/article/detial/3391

HTML5 Canvas绘图基本使用方法, H5使用Canvas绘图

Canvas 是H5的一部分,允许脚本语言动态渲染图像。Canvas 定义一个区域,可以由html属性定义该区域的宽高,javascript代码可以访问该区域,通过一整套完整的绘图功能(API),在网页上渲染动态效果图。

web图片前端裁剪功能实现_利用html5 canvas技术实现图片裁剪

上传截图很多做法是把图像发送到后端,把裁剪后的结果发送给浏览器,这种方式会增加处理时延。用canvas提供的API实现纯前端的剪切:这里头关键有三步:显示未经处理的图片,得到裁剪区域,显示裁剪后的区域。

原生js使用canvas实现图片格式webp/png/jpeg在线转换

javascript完成图片格式转换: 通过input上传图片,使用FileReader将文件读取到内存中。将图片转换为canvas,canvas.toDataURL()方法设置为我们需要的格式,最后将canvas转换为图片。

离屏Canvas — 使用Web Worker提高你的Canvas运行速度

现在因为有了离屏Canvas,你可以不用在你的主线程中绘制图像了!Canvas 是一个非常受欢迎的表现方式,同时也是WebGL的入口。它能绘制图形,图片,展示动画,甚至是处理视频内容

canvas高效绘制10万图形,你必须知道的高效绘制技巧

最近的一个客户项目中,简化的需求是绘制按照行列绘制很多个圆圈。需求看起来不难,上手就可以做,写两个for循环。,IT行业的知识更新越来越快,能够以不变应万变的人,就是拥有良好的学习力、创造力、判断力和思考力的人。这些能力会让你在变换万千的技术海洋中,屹立不倒,不被淹没。

利用canvas将网页元素生成图片并保存在本地

利用canvas将网页元素生成图片并保存在本地,首先引入三个文件,createElementNS() 方法可创建带有指定命名空间的元素节点。 createElementNS(ns,name) > createElementNS() 方法与 createElement() 方法相似

基于 HTML5 Canvas 的智能安防 SCADA 巡逻模块

随着大数据时代的来临,物联网的日益发展,原先的 SCADA 系统本身也在求新求变,从最开始的专业计算机和操作系统,到通用计算机和相关软件,再到现在基于 HTML5 Canvas 的新型组态开发,其应用的范围也从最初的电力

js用canvas实现简单的粒子运动

在写下合格粒子运动时要先清楚你的思路,不能一开始就盲目的开始写,首先先要确定思路然后在去一步步的实现,在写的过程要注意细节,要思考js有些知识是跟数学知识相关的要注意观察

Canvas 点线动画案例

canvas 画的圆不是圆,是椭圆。不要在style里指定 Canvas 的宽度,Canvas 画布的尺寸的大小和显示的大小是有很大的区别的,在 canvas 里面设置的是才是 Canvas 本身的大小。不要企图通过闭合现有路径来开始一条新路径

Canvas在移动端绘制模糊的原因与解决办法

由于一些移动端的兼容性原因,我们某个项目需要前端将pdf转换成在移动端页面可直接观看的界面。为了方便解决,我们采用了pdf.js这个插件,该插件可以将pdf转换成canvas绘制在页面上

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!