如何在Canvas中添加事件作为一个前端,给元素添加事件是一件很常见的事情。但是在Canvas中,它绘制的任何东西都是得不到的,更不用说添加事件了,所以我们就无能为力了?当然不是!我们在平时的项目中肯定用过很多Canvas框架。我们发现事件已经在这些框架中使用的非常成熟,没有出现特别严重的问题。那么我们就可以确定事件在Canvas中并不是一个遥不可及的东西。傻瓜式我们都知道当一个元素触发事件时,它的鼠标位置基本都在该元素上方,那么我们自然而然会想到将当前鼠标位置与对象所占据的位置进行比较,这样我们就可以断定是否对象应该触发事件。这个方法比较简单,我就不用代码来演示了,但是既然我称之为傻瓜式的方法,那显然不是一个有效的解决方案。因为物体所占的位置不一定很容易得到。如果是矩形、圆形等,我们也可以获取一些简单的公式所占的位置。但是,复杂点处的多边形,甚至多边形的某些边为弧,显然此时我们获取它所处的位置是极其复杂和困难的,所以这种方法只适合在某些情况下使用演示,不适用于大多数情况。更聪明的方法既然上面的方法碰壁了,我们只能另辟蹊径了。在浏览CanvasAPI的时候,我发现了一个方法isPointInPath,这似乎就是我们要找的药。介绍一下isPointInPathisPointInPath的作用:顾名思义,我们可以直观的知道这个方法是用来判断点是否在路径中的。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:非零(默认),evenodd。非零包围规则和奇偶规则是图形中判断一个点是否在多边形内的规则,非零包围规则是Canvas默认的规则。如果想详细了解这两个规则,可以自己查资料,这里就不多加篇幅了。介绍完上面的输入参数,大家肯定猜到了isPointInPath方法的输出参数,分别是true和false。使用isPointInPath在上一节中介绍了isPointInPath方法之后,我们现在来使用它。让我们从一个简单的演示开始:constcanvas=document.getElementById('canvas')constctx=canvas.getContext('2d')ctx.beginPath()ctx.moveTo(10,10)ctx.lineTo(10,50)ctx.lineTo(50,50)ctx.lineTo(50,10)ctx.fillStyle='黑色'ctx.fill()ctx.closePath()canvas.addEventListener('click',function(e){constcanvasInfo=canvas.getBoundingClientRect()console.log(ctx.isPointInPath(e.clientX-canvasInfo.left,e.clientY-canvasInfo.top))})如图,灰色部分为Canvas占用的区域,黑色part是我们实际添加的事件区域,我们点击黑色区域后,它实际上做了我们想要的,打印的值为true。看似Canvas的事件监听解决起来很简单,但真的有那么简单吗?显然不可能!我们再举一个例子。此时有两个区域,我们需要给它们绑定不同的事件:constcanvas=document.getElementById('canvas')constctx=canvas.getContext('2d')ctx.beginPath()ctx.moveTo(10,10)ctx.lineTo(10,50)ctx.lineTo(50,50)ctx.lineTo(50,10)ctx.fillStyle='黑色'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()画布。addEventListener('click',function(e){constcanvasInfo=canvas.getBoundingClientRect()console.log(ctx.isPointInPath(e.clientX-canvasInfo.left,e.clientY-canvasInfo.top))})这时候,结果不再像我们预期的那样。单击黑色区域时,打印值为false,单击红色区域时,打印值为true。其实原因很简单,因为上面的代码,我们实际上创建了两个Path,而isPointInPath方法其实只是检测当前点是否在最后一个Path中,例子中红色区域就是最后一个Path,所以只有当红色区域被点击时,isPointInPath方法才能判断为true。现在让我们修改代码:constcanvas=document.getElementById('canvas')constctx=canvas.getContext('2d')letdrawArray=[]functiondraw1(){ctx.beginPath()ctx.moveTo(10,10)ctx.lineTo(10,50)ctx.lineTo(50,50)ctx.lineTo(50,10)ctx.fillStyle='black'ctx.fill()}functiondraw2(){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)常量canvasInfo=canvas.getBoundingClientRect()drawArray.forEach(it=>{it()console.log(ctx.isPointInPath(e.clientX-canvasInfo.left,e.clientY-canvasInfo.top))})})我们做了一个很重要的转换,我们将每个Path放入一个单独的函数中,并将它们放入一个数组中。当点击事件触发时,我们清空Canvas,遍历数组重绘。我们每次绘制Path都会进行一次判断,所以在调用isPointInPath方法时,可以实时获取到当前上一条Path,进而判断当前点。道中。现在我们已经间接实现了对每个Path单独的事件监听,但是实现的方式需要一次又一次的重绘,那么有没有不重绘的情况下监听事件的方法呢?首先我们要知道,之所以会反复重绘,是因为isPointInPath这个方法是最后一个监听的Path,但是我们在介绍这个方法的时候,说过它的第一个参数是一个Path对象。传入这个参数后,Path将不再取最后一个Path,而是使用我们传入的Path,现在有一个demo来验证它的可行性:constcanvas=document.getElementById('canvas')constctx=canvas.getContext('2d')constpath1=newPath2D();path1.rect(10,10,100,100);ctx.fill(path1)constpath2=newPath2D();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))})如上图所示,我们点击左边的图形,打印真假;单击右侧图形以打印假和真。打印结果显示没有问题,但是由于其兼容性有待加强,目前推荐使用redraw方式监听事件。结束语Canvas的事件监听到这里就基本一样了。原理很简单,大家应该都能掌握。github地址,欢迎开始附录自己写的一个democonstcanvas=document.getElementById('canvas')classrectangular{constructor(ctx,{top=0,left=0,width=30,height=50,background='red'}){this.ctx=ctxthis.top=topthis.left=leftthis.width=widththis.height=heightthis.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.backgroundthis.ctx.fill()this.ctx.closePath()}adjust(left,top){this.left+=leftthis.top+=top}}classcircle{constructor(ctx,{center=[],radius=10,background='blue'}){this.ctx=ctxthis.center=[center[0]===undefined?半径:中心[0]、中心[1]===未定义?radius:center[1]]this.radius=radiusthis.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.backgroundthis.ctx.fill()this.ctx.closePath()}adjust(left,top){this.center[0]+=leftthis.center[1]+=top}}类演示{constructor(canvas){this.canvasInfo=canvas.getBoundingClientRect()this.renderList=[]this.ctx=canvas.getContext('2d')this.canvas=canvasthis.rectangular=(config)=>{让target=newrectangular(this.ctx,{...config})this.addRenderList(target)返回这个}this.circle=(config)=>{让target=newcircle(this.ctx,{...config})这个。addRenderList(target)returnthis}this.addEvent()}addRenderList(target){this.renderList.push(target)}itemToLast(index){constlastItem=this.renderList.splice(index,1)[0]这个。renderList.push(lastItem)}painting(){this.ctx.clearRect(0,0,this.canvasInfo.width,this.canvasInfo.height)this.renderList.forEach(it=>it.painting())}addEvent(){constthat=thisletstartX,startYcanvas.addEventListener('mousedown',e=>{startX=e.clientXstartY=e.clientYletchoosedIndex=nullthis.renderList.forEach((it,index)=>{it.painting()if(this.ctx.isPointInPath(startX,startY)){choosedIndex=index}})if(choosedIndex!==null){this.itemToLast(choosedIndex)}document.addEventListener('mousemove',鼠标移动事件)做cument.addEventListener('mouseup',mouseupEvent)this.painting()})functionmousemoveEvent(e){consttarget=that.renderList[that.renderList.length-1]constcurrentX=e.clientXconstcurrentY=e.clientYtarget.adjust(currentX-startX,currentY-startY)startX=currentXstartY=currentYthat.painting()}functionmouseupEvent(e){consttarget=that.renderList[that.renderList.length-1]constcurrentX=e。clientX常量currentY=e.clientYtarget.adjust(currentX-startX,currentY-startY)startX=currentXstartY=currentYthat.painting()document.removeEventListener('mousemove',mousemoveEvent)document.removeEventListener('mouseup',mouseupEvent)}}}constyes=newdemo(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()
