Pointer Events の使用

このガイドでは、ポインタイベントと HTML の <canvas> 要素を使用してマルチタッチ対応の描画アプリを作成する方法について説明します。 この例は、ポインタイベントの入力イベントモデルを使用する点を除いて、Touch events の概要の例に基づいています。 もう1つの違いは、ポインタイベントはポインタデバイスに依存しないため、アプリは同じコードを使用してマウス、ペン、または指先からの座標ベースの入力を受け入れることです。

このアプリは、ポインタイベントをサポートしているブラウザーでのみ動作します。

このアプリのライブ版は GitHub で利用できます。 ソースコードは Github で入手でき、プルリクエストやバグレポートは大歓迎です。

定義

表面(Surface)
タッチを感知できる表面。 これは、トラックパッド、タッチ画面、あるいは物理的な画面とユーザーの机の表面(またはマウスパッド)の仮想的なマッピングかもしれません。
タッチポイント(Touch point)
表面との接点。 これは、指(または肘、耳、鼻など何でも、しかし通常は指)、スタイラス、マウス、または表面上の単一の点を指定するための他の任意の方法かもしれません。

以下のテキストでは、表面との接触を説明するときに「指」という用語を使用していますが、もちろんスタイラス、マウス、その他の場所を指す方法でもかまいません。

キャンバスの作成

ブラウザーがデフォルトのタッチの振る舞いをアプリに適用しないようにするには、touch-action プロパティを none に設定します。

<canvas id="canvas" width="600" height="600" style="border:solid black 1px; touch-action:none">
  Your browser does not support canvas element.
  訳: お使いのブラウザーはキャンバス要素をサポートしていません。
</canvas>
<br>
<button onclick="startup()">Initialize</button>
<br>
Log: <pre id="log" style="border: 1px solid #ccc;"></pre>

イベントハンドラの設定

ページが読み込まれると、<body> 要素の onload 属性によって以下に示す startup() 関数が呼び出されます(ただし、MDN ライブサンプルシステムの制限により、この例ではボタンを使用してトリガーします)。

function startup() {
  var el = document.getElementsByTagName("canvas")[0];
  el.addEventListener("pointerdown", handleStart, false);
  el.addEventListener("pointerup", handleEnd, false);
  el.addEventListener("pointercancel", handleCancel, false);
  el.addEventListener("pointermove", handleMove, false);
  log("initialized.");
} 

これは単に <canvas> 要素のすべてのイベントリスナーを設定するので、タッチイベントが発生したときに処理できます。

新しいタッチの追跡

進行中のタッチを追跡します。

var ongoingTouches = new Array();

pointerdown イベントが発生すると、表面上で新しいタッチが発生したことを示し、次の handleStart() 関数が呼び出されます。

function handleStart(evt) {
  log("pointerdown.");
  var el = document.getElementsByTagName("canvas")[0];
  var ctx = el.getContext("2d");
        
  log("pointerdown: id = " + evt.pointerId);
  ongoingTouches.push(copyTouch(evt));
  var color = colorForTouch(evt);
  ctx.beginPath();
  ctx.arc(touches[i].pageX, touches[i].pageY, 4, 0, 2 * Math.PI, false);  // a circle at the start 訳注: エラーで中断するので、この行を削除すべき
  ctx.arc(evt.clientX, evt.clientY, 4, 0, 2 * Math.PI, false);  // a circle at the start
  ctx.fillStyle = color;
  ctx.fill();
}

イベントの処理の一部を後で処理するために ongoingTouches に格納した後、開始点を小さな円として描画します。 4ピクセル幅の線を使用しているので、4ピクセルの半径の円が見栄えよく表示されます。

ポインタの動きに合わせて描画

1つ以上のポインタが移動するたびに、pointermove イベントが配信され、その結果、次の handleMove() 関数が呼び出されます。 この例におけるその役割は、キャッシュされたタッチ情報を更新し、各タッチの前の位置から現在の位置まで線を引くことです。

function handleMove(evt) {
  var el = document.getElementsByTagName("canvas")[0];
  var ctx = el.getContext("2d");
  var color = colorForTouch(evt);
  var idx = ongoingTouchIndexById(evt.pointerId);

  log("continuing touch: idx =  " + idx);
  if (idx >= 0) {
    ctx.beginPath();
    log("ctx.moveTo(" + ongoingTouches[idx].pageX + ", " + ongoingTouches[idx].pageY + ");");
    ctx.moveTo(ongoingTouches[idx].pageX, ongoingTouches[idx].pageY);
    log("ctx.lineTo(" + evt.clientX + ", " + evt.clientY + ");");
    ctx.lineTo(evt.clientX, evt.clientY);
    ctx.lineWidth = 4;
    ctx.strokeStyle = color;
    ctx.stroke();

    ongoingTouches.splice(idx, 1, copyTouch(evt));  // swap in the new touch record
    log(".");
  } else {
    log("can't figure out which touch to continue: idx = " + idx);
  }
}

この関数はキャッシュされたタッチ情報配列で各タッチに関する以前の情報を探して、描画する各タッチの新しい線分の開始点を決定します。 これは各タッチの PointerEvent.pointerId プロパティを見ることによって行われます。 このプロパティは、各ポインタイベントに対して一意の整数であり、各指が表面と接触している間、各イベントに対して一貫性を保ちます。

これにより、各タッチの前の位置の座標を取得し、適切なコンテキストメソッドを使用して2つの位置を結ぶ線分を描画できます。

線を描画した後、Array.splice() を呼び出して、ongoingTouches 配列内のタッチポイントに関する以前の情報を現在の情報に置き換えます。

タッチの終わりの処理

ユーザーが表面から指を離すと、pointerup イベントが送信されます。 次の handleEnd() 関数を呼び出すことによってこのイベントを処理します。 その仕事は終わったタッチのための最後の線分を引き、進行中のタッチリストからタッチポイントを取り除くことです。

function handleEnd(evt) {
  log("pointerup");
  var el = document.getElementsByTagName("canvas")[0];
  var ctx = el.getContext("2d");
  var color = colorForTouch(evt);
  var idx = ongoingTouchIndexById(evt.pointerId);

  if (idx >= 0) {
    ctx.lineWidth = 4;
    ctx.fillStyle = color;
    ctx.beginPath();
    ctx.moveTo(ongoingTouches[idx].pageX, ongoingTouches[idx].pageY);
    ctx.lineTo(evt.clientX, evt.clientY);
    ctx.fillRect(evt.clientX - 4, evt.clientY - 4, 8, 8);  // and a square at the end
    ongoingTouches.splice(idx, 1);  // remove it; we're done
  } else {
    log("can't figure out which touch to end");
  }
} 

これは前の関数と非常によく似ています。 唯一の大きな違いは、終わりを示すために小さな正方形を描くことと、Array.splice() を呼び出すときに、更新された情報を追加せずに、進行中のタッチリストから古いエントリを削除することです。 その結果、そのタッチポイントの追跡をやめます。

キャンセルされたタッチの処理

ユーザーの指がブラウザーの UI にぶつかったり、あるいはタッチをキャンセルする必要がある場合は、pointercancel イベントが送信され、次の handleCancel() 関数を呼び出します。

function handleCancel(evt) {
  log("pointercancel: id = " + evt.pointerId);
  var idx = ongoingTouchIndexById(evt.pointerId);
  ongoingTouches.splice(idx, 1);  // remove it; we're done
} 

アイデアはすぐにタッチを中止することなので、最後の線分を描かずに進行中のタッチリストから削除します。

便利な関数

この例では、コードの残りの部分をより明確にするために簡単に説明する必要がある、いくつかの便利な関数を使用しています。

タッチごとの色の選択

各タッチの描画を異なるように見せるために、colorForTouch() 関数を使用して、タッチの一意の識別子に基づいて色を選びます。 この識別子は不透明な数字ですが、少なくとも現在アクティブなタッチ間で異なることを頼れます。

function colorForTouch(touch) {
  var r = touch.pointerId % 16;
  var g = Math.floor(touch.pointerId / 3) % 16;
  var b = Math.floor(touch.pointerId / 7) % 16;
  r = r.toString(16); // make it a hex digit
  g = g.toString(16); // make it a hex digit
  b = b.toString(16); // make it a hex digit
  var color = "#" + r + g + b;
  log("color for touch with identifier " + touch.pointerId + " = " + color);
  return color;
}

この関数の結果は、描画色を設定するために <canvas> の関数を呼び出すときに使用できる文字列です。 例えば、PointerEvent.pointerId の値が 10 の場合、結果の文字列は "#aaa" です。

タッチオブジェクトのコピー

ブラウザーによっては、イベント間でタッチオブジェクトを再利用することがあるので、オブジェクト全体を参照するのではなく、気になる部分をコピーするのが最善です。

function copyTouch(touch) {
  return { identifier: touch.pointerId, pageX: touch.clientX, pageY: touch.clientY };
}

進行中のタッチの検索

次の ongoingTouchIndexById() 関数は、ongoingTouches 配列をスキャンして、指定された識別子と一致するタッチを見つけ、そのタッチの配列におけるインデックスを返します。

function ongoingTouchIndexById(idToFind) {
  for (var i = 0; i < ongoingTouches.length; i++) {
    var id = ongoingTouches[i].identifier;
    
    if (id == idToFind) {
      return i;
    }
  }
  return -1;    // not found
} 

何が起こっているのかを示す

function log(msg) {
  var p = document.getElementById('log');
  p.innerHTML = msg + "\n" + p.innerHTML;
}

仕様

仕様 状態 コメント
Pointer Events – Level 2
PointerEvent の定義
勧告 不安定版
Pointer Events
PointerEvent の定義
廃止された 初期定義

ブラウザーの互換性

PointerEvent インターフェイス

Update compatibility data on GitHub
デスクトップモバイル
ChromeEdgeFirefoxInternet ExplorerOperaSafariAndroid webviewAndroid 版 ChromeAndroid 版 FirefoxAndroid 版 OperaiOSのSafariSamsung Internet
PointerEventChrome 完全対応 55Edge 完全対応 12Firefox 完全対応 59
完全対応 59
完全対応 41
無効
無効 From version 41: this feature is behind the dom.w3c_pointer_events.enabled preference (needs to be set to true). To change preferences in Firefox, visit about:config.
IE 完全対応 11
完全対応 11
部分対応 10
接頭辞付き 補足
接頭辞付き MS のベンダー接頭辞が必要
補足 See MSDN Pointer events updates.
Opera 完全対応 42Safari 完全対応 13WebView Android 完全対応 55Chrome Android 完全対応 55Firefox Android 完全対応 41
無効
完全対応 41
無効
無効 From version 41: this feature is behind the dom.w3c_pointer_events.enabled preference (needs to be set to true). To change preferences in Firefox, visit about:config.
Opera Android 完全対応 42Safari iOS 完全対応 13Samsung Internet Android 完全対応 6.0

凡例

完全対応  
完全対応
実装ノートを参照してください。
実装ノートを参照してください。
ユーザーが明示的にこの機能を有効にしなければなりません。
ユーザーが明示的にこの機能を有効にしなければなりません。
使用するには、ベンダー接頭辞または異なる名前が必要です。
使用するには、ベンダー接頭辞または異なる名前が必要です。

関連情報