As a front-end, it is common to add events to elements. But in the Canvas, everything it draws is not accessible, let alone adding events, so there is nothing we can do about it? Of course not! We have certainly used many Canvas frameworks in our daily projects, and we find that events have been used in these frameworks very mature, and there are no serious problems. One thing we can be sure of is that events are not untouchable in the Canvas.



A fool’s way


We all know that when an element triggers an event, its mouse position is basically above the element, so it’s natural to compare the current mouse position with the position the object occupies to determine whether the object should trigger the event or not. This is easier, so I’m not going to do it in code, but since I’m calling it the dumb-ass approach, it’s obviously not an efficient solution. Because the position occupied by the object does not necessarily is very easy to get, if it is a rectangle, circle, etc. We can also through some simple formulas for its occupy position, but in complex polygons, and even is some of the side of the polygon curve, it is obvious that we obtain its occupied position at this moment is an extremely complex and difficult thing, So this is only good for doing a few demos on your own, not for most situations.


A smarter way


Now that this approach has failed, we have to find another one. While browsing through CanvasAPI, we found a method, isPointInPath, that seemed to be the medicine we were looking for.


Introduce isPointInPath


IsPointInPath does what it does: As the name suggests, we can see intuitively that this method is used to determine whether a point is in a path.


IsPointInPath ([path,]x, y [, fillRule]). The method has four parameters. Path and fillRule are optional, and x and y are mandatory. We introduce four parameters in turn.


Path: I thought it was beginPath or closePath. Unfortunately, these methods do not return a value. After looking up the data, I found that it is the object of Path2D constructor new. How to use the Path2D constructor. However, unfortunately, this method may be due to compatibility problems, some open source frameworks have not been used.


X, y: these two parameters are easy to understand. They are the distance between X-axis and Y-axis. Note that their relative position is the upper left corner of the Canvas.


FillRule: nonzero (default), evenodd. The non-zero wrapping rule and the odd-even rule are the rules in graphics to determine whether a point is in a polygon, where the non-zero wrapping rule is the default rule of Canvas. Want to know these two kinds of rules in detail, you can go to the information, here will not increase the length of the introduction.


The input parameters of isPointInPath are, as you can guess, true and false.


Using isPointInPath


After the isPointInPath method was introduced in the previous section, let’s use it now.


Let’s start with a simple 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))  })Copy the code





As shown in the figure, the gray part is the area occupied by Canvas, and the black is the area where we actually add the event. After we click on the black area, the printed value is true as we expected. It looks like the Canvas event listener is so simple to solve, but things are really that simple. Obviously not! Let’s take another example where there are two regions to which we need to bind different events:


  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))  })Copy the code






At this point, the result is no longer as expected, printing false when clicking on the black area and true when clicking on the red area.


The isPointInPath method only checks if the current point is in the last Path. In this example, the red area is the last Path, so only if you click on the red area, The isPointInPath method is true. Now let’s change the code:


  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))    })  })Copy the code


The above code has been modified to put each Path into a separate function and push them into an array. When the click event is triggered, we empty the Canvas and redraw through the number group. Every time a Path is drawn, we make a judgment. Thus, when calling isPointInPath method, we can obtain the current last Path in real time, and then determine the Path of the current point.


Now that we’ve indirectly implemented listening for individual events on each Path, but in a way that requires redrawing over and over again, is there a way to listen for events without redrawing?


First of all, we need to know that the reason why we redraw over and over again is because the isPointInPath method is the last Path that we listen for, but when we introduced this method, we said that its first argument is a Path object, and when we pass that argument, Instead of taking the last Path, use the Path we passed in. Now let’s do a demo to verify this works:


  const canvas = document.getElementById('canvas')  const ctx = canvas.getContext('2d') const path1 = new Path2D(); Path1. The rect (10, 10, 100100); 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))  })Copy the code





As shown in the image above, we click on the left figure and print true, false; Click on the figure on the right to print false, true. The printed results show that there is no problem, but due to its compatibility needs to be strengthened, so it is recommended to use the redraw mode to listen for events.


conclusion


This is basically enough about Canvas event monitoring. The principle is very simple and everyone should be able to grasp it.

Github address, welcome start




The appendix


A demo written by myself


  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) => {        lettarget = new rectangular(this.ctx, {... config}) this.addRenderList(target)return this      }      this.circle = (config) => {        lettarget = 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()Copy the code