マガジン

タッチとマウスが同時に来る:Pointer Eventsが整理するもの

遊びを見つける

執筆にClaudeを使用しています。

キーボードとタッチの二つの入力が、同じ駒のジャンプにつながる様子を描いたイラスト。
記事のテーマをもとに制作したAI生成イラスト。

スマートフォンの画面を指で1回タップしたとき、ブラウザの中ではイベントが1つだけ起きているとは限りません。仕様の上では、タッチ用のイベント、マウス用のイベント、そしてその両方を包むポインター用のイベントが、続けて発生することがあります。自作のゲームに「タップで発射」を、mousedown と touchstart の両方に登録して作ると、1回のタップが2回の発射になる場面が考えられます。 題名の問いは、「なぜ1つの操作が複数のイベントになるのか」と「Pointer Eventsはそれをどう整理するのか」の2つです。この記事では、イベントが重なる理由を整理します。続いて、自分のブラウザで順序を確かめるための最小のログページを示し、結果の読み方と起こりやすい誤りを説明します。結果そのものは、読者の端末ごとに違い得るので書きません。 アソバースの公式作品の操作設計も、確認済みの範囲で取り上げます。

この記事の目次
  1. 1回の操作が複数のイベントになる理由
  2. 自分のブラウザで順序を確かめる
  3. 記録の取り方と結果の読み方
  4. 二重処理を防ぐ3つの方針
  5. 公式作品の操作設計から読み取れること
  6. 次の行動
  7. 参考資料

1回の操作が複数のイベントになる理由

歴史的な順序で見ると、先にマウスのイベント(mousedown、mouseup、click など)がありました。のちにスマートフォンが普及し、指の操作を扱う touchstart、touchend などのタッチイベントが加わりました。

ところが、マウス前提で作られた既存のページは、タッチ専用のイベントを知りません。そこでブラウザは、タッチ操作のあとにマウス操作のふりをしたイベントを発生させ、古いページも指で動くようにしました。これが「互換マウスイベント」です。

つまり、タップ1回で次の2系統が重なり得ます。

  • タッチ系:touchstart と touchend
  • 互換マウス系:mousedown、mouseup、click

Pointer Eventsは、マウス・タッチ・ペンを同じ形で扱うために後から加わった仕組みです。pointerdown、pointermove、pointerup、pointercancel といったイベントを使い、入力の種類は pointerType という値("mouse"、"touch"、"pen")で見分けます。

ただし、Pointer Eventsを足しても古いイベントが消えるわけではありません。ポインター系が加わった分、1回のタップで観察できるイベントの種類は、むしろ増えます。「整理する」とは、イベントの数を減らすことではありません。ポインター系だけを見ればよい状態を作る、という意味です。

自分のブラウザで順序を確かめる

次のHTMLを1ファイルに保存してブラウザで開くか、手元の試作環境に貼ってください。外部ライブラリは使いません。

html
<!doctype html>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<style>
#pad{height:200px;background:#cde;touch-action:none}
#log{font:12px monospace;white-space:pre;height:240px;overflow:auto}
</style>
<div id="pad">ここを操作</div>
<pre id="log"></pre>
<script>
const pad = document.getElementById('pad');
const log = document.getElementById('log');
let t0 = 0;
const names = ['pointerdown','pointerup','pointercancel',
  'touchstart','touchend','mousedown','mouseup','click'];
names.forEach(n => pad.addEventListener(n, e => {
  if (!t0) t0 = e.timeStamp;
  const info = e.pointerType
    ? ` ${e.pointerType} id=${e.pointerId} primary=${e.isPrimary}` : '';
  log.textContent += `${Math.round(e.timeStamp - t0)}ms ${n}${info}\n`;
  log.scrollTop = log.scrollHeight;
}, { passive: true }));
</script>

このページは、青い領域で起きたイベントを、名前と時刻(最初のイベントからのミリ秒)つきで並べます。ポインター系のイベントには、入力の種類・ポインターID・主ポインターかどうかも添えます。

記録の取り方と結果の読み方

次の表の条件ごとに、同じ操作を3回ずつ行い、ログを書き写してください。

  • A. パソコンのマウス、操作:青い領域を1回クリック
  • B. スマートフォンやタブレットの指、操作:青い領域を1回タップ
  • C. Bと同じ端末で2本指、操作:2本指で同時にタッチ

見る点は3つです。

  1. イベントの種類と順序:ポインター系・タッチ系・マウス系のどれが、どの順で並ぶか。
  2. 時刻の差:タップの終わりからマウス系までに間があるか。
  3. pointerType と primary:条件Cで、指ごとに別のIDが付くか。主ポインターは何本か。

読み方の目安は次のとおりです。

  • タップ1回の中に touchstart と mousedown が両方あれば、両方に処理を書いた場合は二重に動きます。実際に出るかどうかは端末とブラウザ次第なので、ログで判断してください。
  • 条件Aのマウスでは、ポインター系とマウス系は並ぶ一方、タッチ系は出ないはずです。出るかどうかもログで確かめます。
  • 条件Cで指ごとにIDが分かれていれば、同時に動く複数の入力を区別できる状態です。

誤りやすい点もあります。端末の開発者ツールにある「モバイル表示」の模擬では、実機と同じイベントにならないことがあります。判断は実機のログで行ってください。また、3回の記録が揃わないときは、1回の結果を一般化せず、条件を揃えてやり直してください。

二重処理を防ぐ3つの方針

ログを見て重なりが確認できたら、対策は次のように考えられます。

方針1:ポインター系だけに統一する

pointerdown、pointermove、pointerup だけで入力を組み立て、touchstart や mousedown は登録しません。最新の主要ブラウザで広く使える前提ですが、対象ブラウザは公式ドキュメントで確認してください。

方針2:イベントを止めて互換マウスを抑える

pointerdown の中で preventDefault() を呼ぶと、そのあとの互換マウスイベントが抑えられる、と仕様に書かれています。ただし click など別の経路は残り得ます。この方法は、ログで確かめながら使ってください。

方針3:ドラッグ操作では `touch-action` を決める

ページを指でなぞると、ブラウザはスクロールを始めたいと判断し、ポインター系に pointercancel を送ることがあります。ゲーム用の領域に CSS の touch-action: none を指定すると、その領域ではブラウザ側のスクロールやズームが始まらなくなります。

先のログページで、#pad の touch-action:none を外して指でなぞると、pointercancel が出る様子を確かめられます。指を動かす操作が途切れるときは、まずこれを疑います。

公式作品の操作設計から読み取れること

アソバースの公式作品のうち、「紙ひこうき郵便」は、左右キー・ドラッグ・矢印ボタンの3通りの操縦に対応しています。「潮目のオセロ」は、矢印キーとEnterでも操作できます。

ここから読み取れるのは、同じ目的(紙ひこうきを左右に動かす、石を置く)に、複数の入力手段が用意されているという設計です。入力手段が複数あるゲームでは、「どの装置から来た入力か」ではなく、「何をしたいか」で処理を組むほうが、重なりやすい二重処理を避けやすくなります。

ただし、各作品が内部でどのイベントを使っているかは、確認済み事項にありません。この部分は作品を開いて確かめます。確かめるなら、実機でドラッグしながら画面の反応を観察するのが一つの手です。

投稿側の視点では、作品の投稿が「入力→表示の確認→公開」の3段階です。表示の確認の段階で、パソコンとスマートフォンの両方から、1回のタップが二重に効いていないかを試せます。

次の行動

まず上のログページを、パソコンとスマートフォンの両方で動かし、条件A・B・Cを3回ずつ記録してください。自作のゲームがあれば、入力を pointerdown と pointerup に寄せた版を作り、表示の確認で実機のタップ回数を数えてみてください。

参考資料

  • MDN Web Docs「Pointer events」
  • W3C「Pointer Events」仕様
  • MDN Web Docs「touch-action」