1. ホーム
  2. Web制作
  3. html5

Canvasでイベントを追加する方法を説明する

2022-01-10 06:30:49

フロントエンドとして、要素にイベントを追加することはよくあることです。しかし、Canvasでは、イベントの追加はおろか、描いたものにもアクセスできません。もちろん、そんなことはありません! 私たちはプロジェクトで多くのCanvasフレームワークを使用してきましたが、これらのフレームワークではイベントは非常に成熟しており、深刻な問題は発生していないことが分かっています。それなら、Canvas においてイベントは決して手の届かないものではないと確信できます。

失敗しない方法

要素がイベントをトリガーするとき、そのマウスの位置は基本的に要素の上にあることは誰でも知っています。ですから、現在のマウスの位置とオブジェクトが占める位置を比較して、オブジェクトがイベントをトリガーすべきかどうかを判断しようと考えるのは自然なことです。この方法は比較的単純なので、コードで実演する必要はありませんが、私はこれをフールプルーフの方法と呼んでいるので、それが有効な解決策でないことは明らかです。オブジェクトが占める位置は必ずしも非常に簡単ではないので、それが長方形、円などであれば、我々はまだいくつかの簡単な式によって占有位置を得ることができますが、より複雑な多角形では、多角形のいくつかの側面が円弧であっても、それは我々がその後非常に複雑で難しいことで占有位置を得ることは明らかなので、この方法はやって自分のためにのみ適しています ほとんどのケースに適していない。

よりスマートな方法

上記のアプローチでは壁にぶつかったので、別の方法を探さなければなりません。Canvas APIを調べていると、isPointInPathというメソッドが見つかり、これが探していた治療法になりそうです。

isPointInPathの紹介

isPointInPathの役割。その名の通り、ある点がパスの中にあるかないかを判断するためのメソッドであることが直感的にわかる。

isPointInPathの入出力パラメータ: ctx.isPointInPath([path, ]x, y [, fillRule]) このメソッドは4つのパラメータを持っており、そのうちpathとfillRuleはオプション、xとyは必須となっています。4つのパラメータを順番に紹介します。

path:このパラメータを見たとき、beginPathかclosePathの戻り値かと思ったが、残念ながらこの2つのメソッドには戻り値がない。情報を調べたところ、Path2Dコンストラクタnewのオブジェクトであることが分かった。 path2Dコンストラクタ 具体的な使用方法 . しかし、残念ながらメソッドは、現在いくつかのオープンソースのフレームワークを見て、互換性の問題に起因する可能性がありますまだ使用されていない。

x, y: この2つのパラメータはよく理解されており、x軸とy軸の間の距離であり、その相対的な位置はキャンバスの左上隅であることに留意する必要があります。

fillRule: nonzero (default), evenodd. nonzero surround rule と parity rule は、ある点がポリゴンの内部にあるかどうかを判断するためのグラフィックスにおけるルールで、nonzero surround rule は Canvas のデフォルトルールとなっています。この2つのルールについて詳しく知りたい方は、ご自身で情報を確認することができますので、ここでは紹介するスペースを割愛させていただきます。

以上の入力パラメータの紹介で、isPointInPathメソッドの出力パラメータがtrueとfalseであることは想像がつくと思います。

isPointInPathの使用

前項でisPointInPathメソッドを紹介した後、今度はそれを使ってみましょう。

まずは簡単なデモから:。

  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という値がプリントアウトされます。明らかに違いますね! 2つの領域があり、それぞれに異なるイベントをバインドする必要があります。

  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と表示され、期待通りの結果ではなくなりました。

理由はとても簡単で、上記のコードでは実際に2つのPathを作成しており、isPointInPathメソッドは実際には現在のポイントが最後のPathにあるかどうかだけを検出し、例の赤いエリアは最後のPathなので、赤いエリアをクリックしたときだけisPointInPathメソッドが真と判定できるためです。

  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を別々の関数に入れ、配列にプッシュしています。クリック イベントがトリガーされると、Canvas をクリアして配列を繰り返し再描画し、Path を描画するたびに判定を行います。これにより、isPointInPath メソッドを呼び出すときに、現在の最後の Path をリアルタイムで取得し、現在のポイントがどの Path にあるのかを判定することができます。

さて、各Pathに対して個別のイベントリスニングを間接的に実装しましたが、この実装方法では何度も再描画する必要があります。再描画せずにイベントをリスニングする方法はないでしょうか?

まず、何度も再描画する理由は、isPointInPathメソッドが最後のPathをリッスンしているからだということを知っておく必要がありますが、このメソッドを紹介したときに、その最初のパラメータはPathオブジェクトであり、このパラメータを渡すと、Pathは最後のPathではなく、渡されたPathを使用するようになると述べました。では、その実現可能性を検証するためにデモをしてみましょう。

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

画像

如上图所示,我们点击了左边图形,打印真,偽;点击右边图形,打印偽,真打的结果表明是没有问题的,不过于其兼容性还有待加强,所以目前建议是重绘方式来监听事件,还是使用重畫法的结果,结果的结果的,结果,结果,结果的,结果的,结果的,真打的、打印的。

结语

キャンバス的事件监听讲到这里基本就差不多了,原理很简单,大家应该都能掌握。

ギズブ地址 欢迎開始

附录

自己写的一个デモ

  const canvas = document.getElementById('canvas')

  class rechteckig {
    Konstruktor (
      ctx, 
      {
        top = 0,
        left = 0,
        width = 30,
        Höhe = 50,
        Hintergrund = 'rot'
      }
    ) {
      this.ctx = ctx
      this.top = oben
      this.left = links
      this.width = Breite
      this.height = Höhe
      this.background = Hintergrund
    }

    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 += links
      this.top += top
    }
  }

  class circle {
    Konstruktor (
      ctx, 
      {
        center = [],
        radius = 10,
        background = 'blau'
      }
    ) {
      this.ctx = ctx
      this.center = [center[0] === undefiniert ? radius : center[0], center[1] === undefiniert ? radius : center[1]]
      this.radius = radius
      this.background = Hintergrund
    }

    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] += links
      this.center[1] += oben
    }
  }

  Klasse demo {
    constructor (canvas) {
      this.canvasInfo = canvas.getBoundingClientRect()
      this.renderList = []
      this.ctx = canvas.getContext('2d')
      this.canvas = Leinwand
      this.rectangular = (config) => {
        let target = new rechteckig(this.ctx, {...config})
        this.addRenderList(ziel)
        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
      lass 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
        das.malen()
        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: 'grün'})
    .circle()
    .circle({center: [100, 30], background: 'rot', radius: 5})
    .painting()

画像