ピンチズームのジェスチャー

アプリにジェスチャーを追加すると、使い勝手が大幅に向上します。単純なシングルタッチのスワイプジェスチャーから、タッチポイント(別名ポインター)が異なる方向に移動する、より複雑なマルチタッチのツイストジェスチャーまで、さまざまな種類のジェスチャーがあります。

この例では、ピンチ/ズームジェスチャーを検出する方法を示します。 これは、ポインターイベントを使用して、ユーザーが 2 つのポインターを互いに近づけるか遠ざけるかを検出します。

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

この例では、ポインターイベントを使用して、指、マウス、ペンなど、あらゆる種類の 2 つのポインティングデバイスを同時に検出します。 2 つのポインターを互いに近づけるピンチイン(ズームアウト、縮小)ジェスチャーでは、ターゲット要素の背景色が水色 (lightblue) に変わります。 2つのポインターを互いに遠ざけるピンチアウト(ズームイン、拡大)ジェスチャーでは、ターゲット要素の背景色がピンク (pink) に変わります。

タッチターゲットの定義

アプリは <div> を使用してポインターのターゲット領域を定義します。

<style>
  div {
    margin: 0em;
    padding: 2em;
  }
  #target {
    background: white;
    border: 1px solid black;
  }
</style>

グローバルな状態

2 つのポインターのジェスチャーをサポートするには、さまざまなイベントフェーズでポインターのイベント状態を保存する必要があります。このアプリは、イベント状態をキャッシュするために 2 つのグローバル変数を使用します。

// イベント状態をキャッシュするグローバル変数
const evCache = [];
const prevDiff = -1;

イベントハンドラーの登録

イベントハンドラーは pointerdownpointermovepointerup のポインターイベントに登録します。 pointerup ハンドラーは pointercancelpointeroutpointerleave のイベントにも使用します。 これら 4 つのイベントは、このアプリでは同じ意味を持っているからです。

function init() {
  // ポインターターゲット用のイベントハンドラーをインストールする
  const el = document.getElementById("target");
  el.onpointerdown = pointerdownHandler;
  el.onpointermove = pointermoveHandler;

  // pointer{up,cancel,out,leave} イベントの意味は - このアプリでは -
  // 同じであるため、これらのイベントに同じハンドラーを使用する。
  el.onpointerup = pointerupHandler;
  el.onpointercancel = pointerupHandler;
  el.onpointerout = pointerupHandler;
  el.onpointerleave = pointerupHandler;
}

ポインターダウン

pointerdown イベントは、ポインター (マウス、ペン/スタイラス、タッチ画面上のタッチポイント) が接触面に接触したときに発生します。 このアプリでは、このダウンイベントが 2 つのポインターのピンチ/ズームジェスチャーの一部である場合に備えて、イベントの状態をキャッシュする必要があります。

function pointerdownHandler(ev) {
  // pointerdown イベントは、タッチ操作の開始を知らせます。
  // このイベントは2本指ジェスチャーをサポートするためにキャッシュされます
  evCache.push(ev);
  log("pointerDown", ev);
}

ポインター移動

pointermove イベントハンドラーは、ユーザーが2ポインターのピンチ/ズームジェスチャーを呼び出しているかどうかを検出します。 2つのポインターが下がっていてポインター間の距離が増加している場合(ピンチアウトまたはズームイン)、要素の背景色がピンク (pink) に変わり、ポインター間の距離が減少している場合(ピンチインまたはズームアウト) 背景色が水色 (lightblue) に変わります。 より洗練されたアプリでは、ピンチインまたはピンチアウトの決定を使用してアプリ固有の意味論を適用できます。

このイベントが処理されると、ターゲットの境界線が破線 (dashed) に設定され、要素が移動イベントを受け取ったことを明確に視覚的に示します。

function pointermoveHandler(ev) {
  // この関数は、 2 つのポインターの水平ピンチ/ズームジェスチャーを実装しています。
  //
  // 2つのポインター間の距離が広がると(ズームイン)、
  // ターゲット要素の背景色は "pink" に変わり、
  // 縮まると(ズームアウト)、色は "lightblue" に変わります。
  //
  // この関数は、ポインターのターゲットが移動イベントを受け取ったことを
  // 視覚的に示すために、ターゲット要素の境界線を "dashed" に設定します。
  log("pointerMove", ev);
  ev.target.style.border = "dashed";

  // キャッシュ内でこのイベントを見つけ、このイベントの記録を更新します
  const index = evCache.findIndex((cachedEv) => cachedEv.pointerId === ev.pointerId);
  evCache[index] = ev;

  // ポインターが 2 つダウンしている場合は、ピンチジェスチャーを確認します
  if (evCache.length === 2) {
    // 2 つのポインター間の距離を計算
    const curDiff = Math.abs(evCache[0].clientX - evCache[1].clientX);

    if (prevDiff > 0) {
      if (curDiff > prevDiff) {
         // 2 つのポインター間の距離が増えた
         log("Pinch moving OUT -> Zoom in", ev);
         ev.target.style.background = "pink";
      }
      if (curDiff < prevDiff) {
        // 2 つのポインター間の距離が減った
        log("Pinch moving IN -> Zoom out",ev);
        ev.target.style.background = "lightblue";
      }
    }

    // 次の移動イベントのために距離をキャッシュします
    prevDiff = curDiff;
  }
}

ポインターアップ

pointerup イベントは、ポインターが接触面から上がると発生します。 これが発生すると、イベントはイベントキャッシュから削除され、ターゲット要素の背景色と境界線は元の値に戻ります。

このアプリでは、このハンドラーは pointercancelpointerleavepointerout のイベントにも使用します。

function pointerupHandler(ev) {
  log(ev.type, ev);
  // このポインターをキャッシュから削除し、
  // ターゲットの背景色と境界線をリセットします
  remove_event(ev);
  ev.target.style.background = "white";
  ev.target.style.border = "1px solid black";

  // ポインター数が2未満の場合は、以前の距離をリセットします
  if (evCache.length < 2) {
    prevDiff = -1;
  }
}

アプリの UI

アプリは、タッチ領域に <div> 要素を使用し、ログ記録を有効にするボタンとログを消去するためのボタンを提供します。

ブラウザーの既定のタッチ動作が、このアプリのポインター処理をオーバーライドしないようにするために、 touch-action プロパティを <body> 要素に適用しています。

<body onload="init();" style="touch-action:none">
  <div id="target">2 つの点に触れたままにして、ピンチインまたはピンチアウトしてください。<br />
    ピンチが開いている場合(ズームイン)は背景色がピンクに変わり、
    ピンチが閉じている場合(ズームアウト)は背景色が水色に変わります。
  </div>
  <!-- UI for logging/debugging -->
  <button id="log" onclick="enableLog(event);">Start/Stop event logging</button>
  <button id="clearlog" onclick="clearLog(event);">Clear the log</button>
  <p></p>
  <output></output>
</body>

その他の関数

これらの関数はアプリをサポートしますが、イベントの流れに直接は関係しません。

キャッシュ管理

この関数はグローバルイベントキャッシュの evCache の管理に役立ちます。

function removeEvent(ev) {
  // このイベントをターゲットのキャッシュから削除する
  const index = evCache.findIndex((cachedEv) => cachedEv.pointerId === ev.pointerId);
  evCache.splice(index, 1);
}

イベントログ

これらの関数は、イベント活動をアプリのウィンドウに送信するために使用します(デバッグとイベントの流れに関する学習をサポートするため)。

// イベントをログ出力するフラグ
let logEvents = false;

// ログ/デバッグ関数
function enableLog(ev) {
  logEvents = !logEvents;
}

function log(prefix, ev) {
  if (!logEvents) return;
  const o = document.getElementsByTagName('output')[0];
  const s = `${name}:<br>`
    + `  pointerID   = ${ev.pointerId}<br>`
    + `  pointerType = ${ev.pointerType}<br>`
    + `  isPrimary   = ${ev.isPrimary}`;
  o.innerHTML += `${s}<br>`;
}

function clearLog(event) {
  const o = document.getElementsByTagName('output')[0];
  o.innerHTML = "";
}

関連情報