详解如何在Canvas中添加事件的方法


Posted in Javascript onApril 17, 2021

作为一个前端,给元素添加事件是一件司空见惯的事情。可是在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中添加事件的方法

如图所示,灰色部分为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))
  })

详解如何在Canvas中添加事件的方法

这个时候,结果就不再如同我们所预计的一样,当点击其中黑色区域时,打印的值为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))
  })

详解如何在Canvas中添加事件的方法

如上图所示,我们点击了左边图形,打印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()

详解如何在Canvas中添加事件的方法

到此这篇关于详解如何在Canvas中添加事件的方法的文章就介绍到这了,更多相关Canvas添加事件内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章,希望大家以后多多支持三水点靠木!

 
Javascript 相关文章推荐
Javascript 的addEventListener()及attachEvent()区别分析
May 21 Javascript
javascript读写json示例
Apr 11 Javascript
编写高效jQuery代码的4个原则和5个技巧
Apr 24 Javascript
浅谈 javascript 事件处理
Jan 04 Javascript
Bootstrap轮播插件简单使用方法介绍
Jun 21 Javascript
jQuery和hwSlider实现内容响应式可触控滑动切换效果附源码下载(二)
Jun 22 Javascript
微信小程序 wxapp导航 navigator详解
Oct 31 Javascript
Vue网页html转换PDF(最低兼容ie10)的思路详解
Aug 24 Javascript
对vue事件的延迟执行实例讲解
Aug 28 Javascript
Vue中的作用域CSS和CSS模块的区别
Oct 09 Javascript
require.js 加载过程与使用方法介绍
Oct 30 Javascript
js 数组 fill() 填充方法
Nov 02 Javascript
Canvas三种动态画圆实现方法说明(小结)
如何在CocosCreator里画个炫酷的雷达图
jquery插件实现图片悬浮
详解CocosCreator消息分发机制
Apr 16 #Javascript
CocosCreator入门教程之网络通信
Apr 16 #Javascript
JavaScript嵌入百度地图API的最详细方法
用javascript制作qq注册动态页面
You might like
php读取msn上的用户信息类
2008/12/05 PHP
php去除换行符的方法小结(PHP_EOL变量的使用)
2013/02/16 PHP
Linux Apache PHP Oracle 安装配置(具体操作步骤)
2013/06/17 PHP
PHP中批量生成静态html(命令行下运行PHP)
2014/04/19 PHP
PHP实现的简单三角形、矩形周长面积计算器分享
2014/11/18 PHP
Laravel 5.5基于内置的Auth模块实现前后台登陆详解
2017/12/21 PHP
一些常用的Javascript函数
2006/12/22 Javascript
javascript中字符串拼接需注意的问题
2010/07/13 Javascript
8款非常棒的响应式jQuery 幻灯片插件推荐
2012/02/02 Javascript
js或jquery实现页面打印可局部打印
2014/03/27 Javascript
jQuery DOM删除节点操作指南
2015/03/03 Javascript
ubuntu下安装nodejs以及升级的办法
2015/05/08 NodeJs
js中对函数设置默认参数值的3种方法
2015/10/23 Javascript
JavaScript事件类型中UI事件详解
2016/01/14 Javascript
VueJS事件处理器v-on的使用方法
2017/09/27 Javascript
element-ui 中的table的列隐藏问题解决
2018/08/24 Javascript
vue项目中自定义video视频控制条的实现代码
2020/04/26 Javascript
Vue列表如何实现滚动到指定位置样式改变效果
2020/05/09 Javascript
Angular利用HTTP POST下载流文件的步骤记录
2020/07/26 Javascript
python用列表生成式写嵌套循环的方法
2018/11/08 Python
Python实用库 PrettyTable 学习笔记
2019/08/06 Python
Django-migrate报错问题解决方案
2020/04/21 Python
Python基于wordcloud及jieba实现中国地图词云图
2020/06/09 Python
用60行代码实现Python自动抢微信红包
2021/02/04 Python
向全球直邮输送天然健康产品:iHerb.com
2020/05/03 全球购物
Nike俄罗斯官方网站:Nike RU
2021/03/05 全球购物
linux面试题参考答案(9)
2016/01/29 面试题
总经理助理工作职责
2014/02/06 职场文书
解除合同协议书
2014/04/17 职场文书
大学同学会活动方案
2014/08/20 职场文书
解放思想大讨论活动心得体会
2014/09/11 职场文书
商家认证委托书格式
2014/10/16 职场文书
2015年个人思想总结
2015/03/09 职场文书
Python爬取用户观影数据并分析用户与电影之间的隐藏信息!
2021/06/29 Python
JavaScript数组reduce()方法的语法与实例解析
2021/07/07 Javascript
python中的sys模块和os模块
2022/03/20 Python