如何在Canvas中添加事件

做爲一個前端,給元素添加事件是一件司空見慣的事情。但是在Canvas中,其所畫的任何東西都是沒法獲取的,更別說添加事件,那麼咱們對其就一籌莫展了嗎?固然不是的!咱們在平時項目中確定都用過許多Canvas的框架,咱們發現事件在這些框架中已經使用的十分紅熟了,並且並無出現特別嚴重的問題。那麼咱們能夠確定的是,事件在Canvas中並非一個沒法觸及的事情。

一個傻瓜式的方式


咱們都知道一個元素在觸發一個事件時,其鼠標的位置基本處於該元素之上,那麼咱們就天然而然的想到經過當前鼠標的位置以及物體所佔據的位置進行比對,從而咱們就能得出該物體是否應觸發事件。這種方式比較簡單,我就不用代碼演示了,不過既然我叫它傻瓜式的方式,很明顯它不是一個有效的解決方式。由於物體所佔據的位置並不必定是十分容易獲取,若是是矩形、圓形等咱們還能經過一些簡單的公式獲取其佔據的位置,但是在複雜點的多邊形,甚至是多邊形的某些邊是弧線的,顯而易見,咱們這時候再獲取其所佔據的位置時是一件極其複雜且難度極大的事情,因此這種方式只適合本身在作一些demo中使用,並不適用於大多數的狀況。

一個較聰明的方式


既然上面這種方式碰壁了,那麼咱們只能另闢蹊徑。在翻閱CanvasAPI的時候,找到了一個方法isPointInPath,貌似正是咱們苦苦尋找的良藥。

介紹isPointInPathjavascript


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()複製代碼

相關文章
相關標籤/搜索