【HTML+JS】タッチイベントでCanvas をタッチ操作にも対応させる方法

##  はじめに
前回は、HTML の Canvasを使って、お絵かきアプリを実装しました。

https://qiita.com/SoraoKosaka/items/18e17871b7cbcec8f3d2

が、手持ちのペンタブで絵を描こうと思ったところ、できない...
調べたところタッチ操作にはタッチイベント<sup>(1)</sup>を実装しなければいけないらしく、今回はその実装になります。

## タッチイベントを実装するためのポイント
### イベントの違い
#### マウスイベント
・mousedown: マウスボタンを押したとき
・mousemove: マウスを動かしているとき
・mouseup: マウスボタンを離したとき

#### タッチイベント
・touchstart: 指が画面に触れたとき
・touchmove: 画面を指で動かしているとき
・touchend: 指が画面から離れたとき

これらのイベントを Canvas 上で取得し、描画処理を分けて書くのが基本的な考え方です。

### 座標の取り方の違い
#### マウスイベント
・e.clientX / e.clientY でブラウザ表示領域内の座標が取得できます。
・多くの場合 offsetX / offsetY がサポートされています。

### タッチイベント
・e.touches[0].clientX / e.touches[0].clientY でタッチした位置を取得します。
・offsetX / offsetY が無効な場合が多いため<sup>(1)</sup>、自分で Canvas 要素との相対座標を計算する必要があります。

### 相対座標の取得方法
Canvas 要素の左上を基準とした相対的な座標を取得する場合は、getBoundingClientRect() を使います<sup>(2)</sup>。

## 今回の実装の概要
大きく分けて次の追加ポイントがあります。

1. タッチイベント（touchstart、touchmove、touchend、touchcancel）のハンドラーを追加
2. 相対座標を取得する関数 getRelativePos() を用意して共通化

## ソースコード
以下が今回実装したサンプルコードです。
```
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Canvasお絵描き（マウス＆タッチ対応）</title>
  <style>
    #canvas {
      border: 1px solid #000;
      touch-action: none; /* タッチスクロールなどを無効化（Pointer Events利用時などに有効） */
    }
  </style>
</head>
<body>
  <h1>Canvasでお絵描き（マウス＆タッチ対応）</h1>
  <canvas id="canvas" width="500" height="400"></canvas>
  <br>
  <button id="resetButton">リセット</button>

  <script>
    const canvas = document.getElementById('canvas');
    const ctx = canvas.getContext('2d');
    const resetButton = document.getElementById('resetButton');

    let drawing = false;

    // ---- 共通：canvas上の相対座標を得るユーティリティ関数 ----
    function getRelativePos(e) {
      const rect = canvas.getBoundingClientRect();
      // PointerEvent / MouseEvent
      if (e.clientX !== undefined) {
        return {
          x: e.clientX - rect.left,
          y: e.clientY - rect.top
        };
      }
      // TouchEvent
      if (e.touches && e.touches.length > 0) {
        return {
          x: e.touches[0].clientX - rect.left,
          y: e.touches[0].clientY - rect.top
        };
      }
      return null;
    }

    // ---- マウス対応 ----
    canvas.addEventListener('mousedown', (e) => {
      drawing = true;
      ctx.beginPath();
      const pos = getRelativePos(e);
      if (pos) ctx.moveTo(pos.x, pos.y);
    });

    canvas.addEventListener('mousemove', (e) => {
      if (!drawing) return;
      const pos = getRelativePos(e);
      if (pos) {
        ctx.lineTo(pos.x, pos.y);
        ctx.stroke();
      }
    });

    canvas.addEventListener('mouseup', () => {
      drawing = false;
    });

    canvas.addEventListener('mouseleave', () => {
      drawing = false;
    });

    // ---- タッチ対応 ----
    // タッチを開始したら描画開始
    canvas.addEventListener('touchstart', (e) => {
      e.preventDefault(); // スクロールやピンチ操作を抑制(必要に応じて)
      drawing = true;
      ctx.beginPath();
      const pos = getRelativePos(e);
      if (pos) ctx.moveTo(pos.x, pos.y);
    }, { passive: false });

    // タッチ移動中は線を描く
    canvas.addEventListener('touchmove', (e) => {
      e.preventDefault();
      if (!drawing) return;
      const pos = getRelativePos(e);
      if (pos) {
        ctx.lineTo(pos.x, pos.y);
        ctx.stroke();
      }
    }, { passive: false });

    // タッチ終了で描画終了
    canvas.addEventListener('touchend', () => {
      drawing = false;
    });

    // 画面外に指が離れた等の場合も描画終了させておく
    canvas.addEventListener('touchcancel', () => {
      drawing = false;
    });

    // ---- リセットボタン ----
    resetButton.addEventListener('click', () => {
      ctx.clearRect(0, 0, canvas.width, canvas.height);
    });
  </script>
</body>
</html>
```


## おわりに
これで、スマホやタブレットのようなタッチデバイスでもお絵かきができるようになりました。

この記事が参考になりましたらぜひ「いいね」「フォロー」など励みになるのでよろしくお願いします！

## 参考文献
1. [タッチイベント (Touch events) - MDN](https://developer.mozilla.org/ja/docs/Web/API/Touch_events)
2. [Element.getBoundingClientRect() - MDN](https://developer.mozilla.org/ja/docs/Web/API/Element/getBoundingClientRect)