2019/04/16

Canvas上のオブジェクトがクリックされたか検知する

ブラウザゲームなどを開発するとき、Canvas上に描画されたオブジェクト(図形など)をクリックしたいときがある。ただEventTarget.addEventListener('click', listener)のように簡単に処理を追加できるわけではない。

そこで当記事では、「Canvas上にある四角と丸のオブジェクトをクリックしたら色がかわる」というサンプルを元に、オブジェクトのクリック判定やクリックされたときに処理を実行する方法について解説する。


クリック判定を行う


Canvas上ではポインターの座標とオブジェクトのサイズでクリックされたかどうかの判定を行う。

クリック判定のアルゴリズムについては以下の記事、またはスライドの47ページからの「衝突判定」で図解しているので参考にしてほしい。


まずはclickイベントを登録する。
canvas.getBoundingClientRect()を使うことで、Canvasがページ上のどこに配置されていてもマウスの座標とCanvas内の座標を対応付けることができる。
canvas.addEventListener("click", e => {
  // マウスの座標をCanvas内の座標とあわせるため
  const rect = canvas.getBoundingClientRect();
  const point = {
    x: e.clientX - rect.left,
    y: e.clientY - rect.top
  };

  // クリック判定処理
});

次に、矩形と円オブジェクトのクリック判定を行う。
/**
 * 矩形のクリック判定
 */
const square = {
  x: 10, y: 10,  // 座標
  w: 50, h: 50   // サイズ
};

const hit =
      (square.x <= point.x && point.x <= square.x + square.w)  // 横方向の判定
   && (square.y <= point.y && point.y <= square.y + square.h)  // 縦方向の判定

if (hit) { alert('clicked!'); }
/**
 * 円のクリック判定
 */
const square = {
  x: 10, y: 10,  // 座標
  r: 25          // 半径
};

// ピラゴラスの定理をつかってユークリッド距離と半径で判定する
const hit =
    Math.pow(this.x - point.x, 2) + Math.pow(this.y - point.y, 2) <= Math.pow(this.r, 2);

if (hit) { alert('clicked!'); }



実践: クリックされたオブジェクトの色を変える


<main id="app"></main>
/**
 * オブジェクトのベースとなるクラス
 */
class BaseObject {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  // 初期表示
  draw(ctx) {}
  // 自オブジェクトがクリックされたかどうか判定
  testHit(point) {}
  // クリックされたときの処理
  clicked(ctx) {}
}

/**
 * 矩形オブジェクトのクラス
 */
class Box extends BaseObject {
  constructor(x, y, w, h) {
    super(x, y);
    this.w = w;
    this.h = h;
  }

  draw(ctx) {
    ctx.save();
    ctx.fillStyle = "black";
    ctx.fillRect(this.x, this.y, this.w, this.h);
    ctx.restore();
  }

  clicked(ctx) {
    ctx.save();
    ctx.clearRect(this.x, this.y, this.w, this.h);
    ctx.fillStyle = "red";
    ctx.fillRect(this.x, this.y, this.w, this.h);
    ctx.restore();
  }

  testHit(point) {
    return (this.x <= point.x && point.x <= this.x + this.w) &&
           (this.y <= point.y && point.y <= this.y + this.h);
  }
}

/**
 * 円オブジェクトのクラス
 */
class Circle extends BaseObject {
  constructor(x, y, r) {
    super(x, y);
    this.r = r;
  }

  draw(ctx) {
    ctx.save();
    ctx.beginPath();
    ctx.arc(this.x, this.y, this.r, 0, Math.PI * 2, true);
    ctx.fillStyle = "black";
    ctx.fill();
    ctx.restore();
  }

  clicked(ctx) {
    ctx.save();
    ctx.beginPath();
    ctx.arc(this.x, this.y, this.r, 0, Math.PI * 2, true);
    ctx.fillStyle = "red";
    ctx.fill();
    ctx.restore();
  }

  testHit(point) {
    return (
      Math.pow(this.x - point.x, 2) + Math.pow(this.y - point.y, 2) <=
      Math.pow(this.r, 2)
    );
  }
}

/**
 * main処理
 */
const main = () => {
  const canvas = document.createElement("canvas");
  document.getElementById("app").appendChild(canvas);

  canvas.width = 640;
  canvas.height = 480;

  const ctx = canvas.getContext("2d");
  ctx.save();
  ctx.fillStyle = "white";
  ctx.fillRect(0, 0, canvas.width, canvas.height);
  ctx.restore();

  const items = [];

  // ランダムにオブジェクトを配置する
  const getPos = (min, max) => {
    return Math.round(Math.random() * (max + 1 - min) + min);
  };

  [...Array(5)].forEach((_, i) => {
    const x = getPos(0, canvas.width - 50);
    const y = getPos(0, canvas.height - 50);

    const box = new Box(x, y, 50, 50);
    items.push(box);
  });

  [...Array(5)].forEach((_, i) => {
    const x = getPos(0, canvas.width - 50);
    const y = getPos(0, canvas.height - 50);

    const circle = new Circle(x, y, 25);
    items.push(circle);
  });

  // オブジェクトを描画する
  items.forEach(item => item.draw(ctx));

  canvas.addEventListener("click", e => {
    // マウスの座標をCanvas内の座標とあわせるため
    const rect = canvas.getBoundingClientRect();
    const point = {
      x: e.clientX - rect.left,
      y: e.clientY - rect.top
    };

    items.forEach(item => {
      if (item.testHit(point)) {
        item.clicked(ctx);
      }
    });
  });
};

main();

この処理ではランダムな位置に矩形、円オブジェクトをそれぞれ5つずつ表示している。
そして、clickイベント(またはmousedownイベント)が発生したら、カーソルの座標を取得し、クリック可能なすべてのオブジェクトに対してクリック判定処理を実行する。



番外編: addHitRegion


Canvas​Rendering​Context2D.add​HitRegion()を使って、Canvas上のオブジェクトにヒット領域を設定することもできる。
ただし、利用できるブラウザはChromeとFireFoxのみ。またそのままでは使えず、about:configなどでフラグを有効にする必要がある
class Circle {
  /* 略 */
  draw(ctx) {
    ctx.save();
    ctx.beginPath();
    ctx.arc(this.x, this.y, this.r, 0, Math.PI * 2, true);
    ctx.fillStyle = "black";
    ctx.fill();
    ctx.restore();
    ctx.addHitRegion({ id: 'circle' })
  }
  /* 略 */
}

Canvas.addEventListener('click', e => {
  if (e.region === 'circle') {
      circle.clicked(ctx)
  }
})

オブジェクトに対してID付きのヒット領域を設定することで、クリックしたりマウスオーバーしたときにevent.regionにヒット検知したオブジェクトのIDが渡ってくる。



参考サイト





以上

written by @bc_rikko

0 件のコメント :

コメントを投稿