首页 > 其他分享 >如何在Canvas中添加事件

如何在Canvas中添加事件

时间:2023-09-27 11:13:37浏览次数:45  
标签:Canvas isPointInPath const top ctx canvas 添加 事件 lineTo

如何在Canvas中添加事件

作为一个前端,给元素添加事件是一件司空见惯的事情。可是在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://segmentfault.com/a/1190000019239594 

标签:Canvas,isPointInPath,const,top,ctx,canvas,添加,事件,lineTo
From: https://www.cnblogs.com/heibaiqi/p/17732197.html

相关文章

  • H5监听浏览器返回事件
    在日常开发工作中,我们经常会涉及点击返回、后退、上一页等按钮实现自己的关闭页面、调整到指定页面、确认离开页面或执行一些其它操作的需求。可以使用popstate事件进行监听返回、后退,返回指定页操作。具体代码如下:$(function(){functionpushHistory(){varsta......
  • SE C# 添加 事件监听 --选择对象切换监听
    usingSystem;usingSystem.Collections.Generic;usingSystem.ComponentModel;usingSystem.Data;usingSystem.Drawing;usingSystem.Linq;usingSystem.Text;usingSystem.Windows.Forms;usingSystem.Runtime.InteropServices.ComTypes;usingSystem.Runtime.InteropServi......
  • 极光笔记 | 聊一聊推送系统中事件驱动架构的应用
    微服务间通信方式主要有2种:RPC和消息传递。通常来说在请求/响应的场景下使用RPC更加合适,具体实现通常是RESTAPI或者基于长链接的协议(例如gRPC/Thrift/ZeroICE等)。两个服务有比较强的依赖关系,调用者依赖被调用者的处理结果,调用者处理该请求被堵塞以等待响应结果,同时还要进行负载......
  • vue~封装一个文本框添加与删除的组件
    标签组件的效果如下组件作用这是一个div,包含了两个文本框,后面是添加和删除按钮添加按钮复制出新的div,除了文本框没有内容,其它都上面一样删除按钮将当前行div删除组件实现<template><div><templatev-for="(item,index)intags"><el-row:gutter="4"styl......
  • file文件上传后 添加水印 并且生成file文件 使用formData上传
    functionhecheng(){//创建一个canvasconstd2=testCanvas.getContext('2d');//准备图片1consturl=URL.createObjectURL(file.files[0]);varimg=docu......
  • WebApi中添加Jwt鉴权
    前言JSONWebToken(JWT)是一个非常轻巧的规范。这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息。一个JWT实际上就是一个字符串,它由三部分组成,头部、载荷与签名。前两部分需要经过Base64编码,后一部分通过前两部分Base64编码后再加密而成。针对前后端分离的......
  • 使用SpringBoot+Vue3的形式实现管理系统的添加功能
    1、查看页面形式2、使用element-plus组件为添加页面引入form表单成功引入form表单组件:3、更改成自己需要的形式4、测试是否可以拿到文本框的数据拿到数据啦!!(记得vue3这里:console.log(form)//不要写this```)#5、vue界面的添加代码<el-form-item>......
  • Winform中使用System.Windows.Forms.Timer多次启动停止计时器时绑定事件会重复多次执
    场景C#中实现计时器功能(定时任务和计时多长时间后执行某方法):https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article/details/106274074以上关于定时器的使用。在实现点击按钮启动定时器,点击停止按钮停止定时器时发现,重复多次后会导致定时器方法累计重复执行。联想到如下情况......
  • 学习服务卡片事件能力
    【本文正在参加2023「盲盒」+码有奖征文活动】,活动链接https://ost.51cto.com/posts/25284简要介绍ArkTS卡片内部和提供方应用间的交互,可以通过在卡片一端调用postCardAction来实现,当前支持router、message和call三种类型的事件。我们根据实际应用分别从五个方面简单学习一下......
  • kingbaseES主备集群添加/删除节点
    测试环境 IPVIPOSDB主库168.3.1.212168.3.1.214rhel7.6KingbaseESV008R006C007B0012备库1168.3.1.213168.3.1.214rhel7.6KingbaseESV008R006C007B0012备库2168.3.1.215168.3.1.214rhel7.6KingbaseESV008R006C007B0012测试记录1.检查当前集群状态是否正常repmgrclustershow2.......