App下載

在html5中Canvas如何添加事件?案例方法分享!

猿友 2021-07-29 11:02:01 瀏覽數(shù) (1930)
反饋

在使用前端開發(fā)中對一個元素添加事件是一件比較常見的事情,今天我們就來分享個有關(guān)于:“在html5中Canvas如何添加事件? ”這個問題!

一個傻瓜式的方式

我們都知道一個元素在觸發(fā)一個事件時,其鼠標(biāo)的位置基本處于該元素之上,那么我們就自然而然的想到通過當(dāng)前鼠標(biāo)的位置以及物體所占據(jù)的位置進(jìn)行比對,從而我們就能得出該物體是否應(yīng)觸發(fā)事件。這種方式比較簡單,我就不用代碼演示了,不過既然我叫它傻瓜式的方式,很明顯它不是一個有效的解決方式。因為物體所占據(jù)的位置并不一定是十分容易獲取,如果是矩形、圓形等我們還能通過一些簡單的公式獲取其占據(jù)的位置,可是在復(fù)雜點(diǎn)的多邊形,甚至是多邊形的某些邊是弧線的,顯而易見,我們這時候再獲取其所占據(jù)的位置時是一件極其復(fù)雜且難度極大的事情,所以這種方式只適合自己在做一些demo中使用,并不適用于大多數(shù)的情況。

一個較聰明的方式

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

介紹isPointInPath

isPointInPath的作用:顧名思義,我們很直觀的可以知道該方法用以判斷點(diǎn)是否處于路徑當(dāng)中。

isPointInPath的入?yún)⒊鰠ⅲ篶tx.isPointInPath([path, ]x, y [, fillRule]),該方法的參數(shù)有4個,其中path和fillRule為選填,x和y為必填。我們依次介紹4個參數(shù)。

path:看到這個參數(shù),我開始以為是beginPath或者closePath的返回值,很可惜的是這兩個方法并沒有返回值,在查閱了資料后,發(fā)現(xiàn)是Path2D構(gòu)造函數(shù)new的對象。Path2D構(gòu)造函數(shù)具體用法。不過可惜的是該方法可能由于兼容性的問題,目前看了一些開源框架都還未使用。

x,y:這兩個參數(shù)很好理解,就是x軸和y軸的距離,需要注意的是,其相對位置是Canvas的左上角。

fillRule:nonzero(默認(rèn)),evenodd。非零環(huán)繞規(guī)則和奇偶規(guī)則是圖形學(xué)中判斷一個點(diǎn)是否處于多邊形內(nèi)的規(guī)則,其中非零環(huán)繞規(guī)則是Canvas的默認(rèn)規(guī)則。想具體了解這兩種規(guī)則的,可以自己去查閱資料,這里就不增加篇幅介紹了。

上面介紹完了入?yún)ⅲ敲磇sPointInPath方法的出參想必大家都可以猜到了,就是true和false。

使用isPointInPath

上一節(jié)介紹完isPointInPath方法后,我們現(xiàn)在就來使用它吧。

先來一個簡單的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所占據(jù)的區(qū)域,黑色為我們實際添加事件的區(qū)域,在我們點(diǎn)擊黑色區(qū)域后,實際也的確如我們所愿,打印出來的值為true。貌似Canvas的事件監(jiān)聽就這么簡單的解決了,不過事情真有這么簡單嗎。顯然是不可能的!我們再來舉個例子,這時候有兩個區(qū)域,并且我們需要分別給其綁定不同的事件:

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))
  })

這個時候,結(jié)果就不再如同我們所預(yù)計的一樣,當(dāng)點(diǎn)擊其中黑色區(qū)域時,打印的值為false,點(diǎn)擊紅色區(qū)域時,打印的值為true。

其實原因很簡單,因為上述代碼,我們實際創(chuàng)建了兩個Path,而isPointInPath方法實際只檢測當(dāng)前點(diǎn)是否處于最后一個Path當(dāng)中,而例子中紅色區(qū)域為最后一個Path,所以只有點(diǎn)擊紅色區(qū)域時,isPointInPath方法才能判斷為true。現(xiàn)在我們改造一下代碼:

 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))
    })
  })

上面的代碼我們進(jìn)行了一個很大的改造,我們將每個 Path 放入到一個單獨(dú)的函數(shù)當(dāng)中,并將它們push到一個數(shù)組當(dāng)中。當(dāng)觸發(fā)點(diǎn)擊事件時,我們清空 Canvas,并遍歷數(shù)組重新繪制,每當(dāng)繪制一個 Path 進(jìn)行一次判斷,從而在調(diào)用 isPointInPath 方法時,我們能實時的獲取當(dāng)前的最后一個 Path,進(jìn)而判斷出當(dāng)前點(diǎn)所處的 Path 當(dāng)中。

現(xiàn)在我們已經(jīng)間接的實現(xiàn)了對每個Path的單獨(dú)事件監(jiān)聽,可是其實現(xiàn)的方式需要一次又一次的重繪,那么有辦法不需要重繪就能監(jiān)聽事件嗎?

首先我們需要知道一次又一次重繪的原因是因為isPointInPath方法是監(jiān)聽的最后一個Path,不過我們在介紹這個方法的時候,說過其第一個參數(shù)是一個Path對象,當(dāng)我們傳遞了這個參數(shù)后,Path就不再去取最后一個Path而是使用我們傳遞進(jìn)去的這個Path,現(xiàn)在我們來個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))
  })

如上圖所示,我們點(diǎn)擊了左邊圖形,打印true,false;點(diǎn)擊右邊圖形,打印false,true。打印的結(jié)果表明是沒有問題的,不過由于其兼容性還有待加強(qiáng),所以目前建議還是使用重繪方式來監(jiān)聽事件。

結(jié)語

Canvas的事件監(jiān)聽講到這里基本就差不多了,原理很簡單,大家應(yīng)該都能掌握。
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()

以上就是小編分享的有關(guān)于:“在html5中Canvas如何添加事件? ”這方面的相關(guān)內(nèi)容,當(dāng)然了我們還可以在W3Cschool中進(jìn)個整體的學(xué)習(xí)和了解! 希望小編的分享對大家有所幫助!


0 人點(diǎn)贊