JavaScriptでのジェスチャーイベントを利用した拡大/縮小・回転

ジェスチャーイベントは、スマートフォンやタブレットのタッチパネルで複数の指によるタッチ操作を行った際に発生するイベントです。ジェスチャーイベントを利用することで、ユーザーがタッチパネル上で行った拡大/縮小や回転といった操作を簡単に取得することができ、より直感的なユーザーインターフェイスの構築に役立ちます。

ジェスチャーイベントを使ったサンプル

ジェスチャーイベントを使った簡単なサンプルを作ってみました。サンプルでは、EaselJSを使ってcanvasの中央に矩形のオブジェクトを配置しています。シングルタッチのドラッグでオブジェクトを移動、マルチタッチのジェスチャーでオブジェクトを拡大/縮小・回転させることができます。このサンプルを通して、JavaScriptでジェスチャーイベントを利用する方法を解説していきたいと思います。

なお、現時点ではAndroidはJavaScriptのジェスチャーイベントをサポートしていないので、Androidではシングルタッチのドラッグのみ動作します。

ジェスチャーイベントの種類とタイミング

ジェスチャーイベントは、スマートフォンやタブレットのタッチパネルでピンチインやピンチアウトのように複数の指でタッチ操作を行った際に発生します。JavaScriptで取得できる主なジェスチャーイベントの種類とイベントの発生するタイミングは下記の通りです。

gesturestart
タッチパネルにマルチタッチ(複数の指をタッチ)した時に発生します。
gesturechange
タッチパネルにマルチタッチしたまま、指を動かした時に発生します。
gestureend
タッチパネルから指を離し、タッチしている指が1本以下になった時に発生します。

ジェスチャーイベントのイベントリスナー

ジェスチャーイベントは、他のイベントと同じようにaddEventListener()でイベントリスナーを追加することができます。サンプルでは、実行環境がタッチイベントやジェスチャーイベントをサポートしているかを調べ、サポートしている場合のみリスナーを追加するようにしています。

JavaScript

if ("ontouchstart" in window) {
  _canvas.addEventListener("touchstart", touchStartHandler, false);
  _canvas.addEventListener("touchmove", touchMoveHandler, false);
  document.addEventListener("touchend", touchEndHandler, false);
  document.addEventListener("touchcancel", touchEndHandler, false);
}
if ("ongesturestart" in window) {
  _canvas.addEventListener("gesturestart", gestureStartHandler, false);
  _canvas.addEventListener("gesturechange", gestureChangeHandler, false);
  document.addEventListener("gestureend", gestureEndHandler, false);
}

シングルタッチでのドラッグ&ドロップ

シングルタッチ時のドラッグ&ドロップの処理を見てみましょう。下記は、touchstart, touchmove, touchend, touchcancelといったタッチイベントが発生した際に実行される関数です。まず、タッチイベントが発生した際にe.preventDefault()でスクロールや拡大/縮小といったブラウザのデフォルトの動作を停止しておきます。

touchstartのリスナー関数では、ユーザーがタッチした座標を算出し、hitTestで矩形オブジェクトとの衝突判定を行っています。タッチ座標とオブジェクトが衝突している場合には、イベントオブジェクトとオフセットを配列_hitTouchesに格納しておきます。

touchmoveのリスナー関数では、ドラッグ&ドロップの処理を行っています。タッチイベントオブジェクトのtouchesプロパティは、タッチパネルに触れている指の情報を配列で持っているので、その中から最初にオブジェクトと衝突したタッチイベントをidentifierの値で判別しています。

touchendのリスナー関数では、タッチイベントオブジェクトのchangedTouchesプロパティより、タッチパネルから離れた指の情報を取得し、配列_hitTouches内にタッチパネルから離れた指の情報があれば削除しています。

なお、タッチイベントについては、前回の投稿「JavaScriptでタッチイベントを取得しよう」でも解説していますので、合わせてご覧下さい。

JavaScript

function touchStartHandler(e) {
  e.preventDefault();
  var touches = e.changedTouches;
  for (var i = 0, l = touches.length; i < l; ++i) {
    var touch = touches[i];
    var target = touch.target;
    var stageX = touch.pageX - target.offsetLeft;
    var stageY = touch.pageY - target.offsetTop;
    var localPoint = _rect.globalToLocal(stageX, stageY);
    if (_rect.hitTest(localPoint.x, localPoint.y)) {
      var offsetX = _rect.x - stageX;
      var offsetY = _rect.y - stageY;
      var offset = new Point(offsetX, offsetY);
      _hitTouches.push({touch:touch, offset:offset});
    }
  }
}

function touchMoveHandler(e) {
  e.preventDefault();
  if (!_dragEnabled || _hitTouches.length === 0) {
    return;
  }
  var touches = e.touches;
  for (var i = 0, l = touches.length; i < l; ++i) {
    var touch = touches[i];
    if (_hitTouches[0].touch.identifier !== touch.identifier) {
      continue;
    }
    var target = touch.target;
    var stageX = touch.pageX - target.offsetLeft;
    var stageY = touch.pageY - target.offsetTop;
    var offset = _hitTouches[0].offset;
    _rect.x = stageX + offset.x;
    _rect.y = stageY + offset.y;
    return;
  }
}

function touchEndHandler(e) {
  e.preventDefault();
  var touches = e.changedTouches;
  outsideLoop: for (var i = 0, l = touches.length; i < l; ++i) {
    var touch = touches[i];
    for (var j = 0, m = _hitTouches.length; j < m; ++j) {
      if (_hitTouches[j].touch.identifier === touch.identifier) {
        _hitTouches.splice(j, 1);
        continue outsideLoop;
      }
    }
  }
}

マルチタッチでの拡大/縮小・回転

それでは、マルチタッチ時の拡大/縮小・回転の処理を見てみましょう。下記は、gesturestart, gesturechange, gestureendといったジェスチャーイベントが発生した際に実行される関数です。

gesturestartのリスナー関数では、マルチタッチ開始時のオブジェクトのscaleとrotationの値を変数に保存しておきます。また、操作性を考えてマルチタッチ時はドラッグ&ドロップできないようにしておきます。

gesturechangeのリスナー関数では、拡大/縮小・回転の処理を行います。イベントオブジェクトのscale, rotationプロパティからユーザーが行ったジェスチャーを取得し、オブジェクトに反映させます。

gestureendのリスナー関数では、_hitTouchesのoffsetを再計算して、再びドラッグ&ドロップできるようにしています。

JavaScript

function gestureStartHandler(e) {
  _dragEnabled = false;
  _startScale = _rect.scaleX;
  _startRotation = _rect.rotation;
}

function gestureChangeHandler(e) {
  if (_hitTouches.length === 0) {
    return;
  }
  var scale = _startScale + e.scale - 1;
  scale = (scale < MIN_SCALE) ? MIN_SCALE : (MAX_SCALE < scale) ? MAX_SCALE : scale;
  _rect.scaleX = _rect.scaleY = scale;
  _rect.rotation = (_startRotation + e.rotation + 360) % 360;
}

function gestureEndHandler(e) {
  _startScale = null;
  _startRotation = null;
  for (var i = 0, l = _hitTouches.length; i < l; ++i) {
    var touch = _hitTouches[i].touch;
    var target = touch.target;
    var stageX = touch.pageX - target.offsetLeft;
    var stageY = touch.pageY - target.offsetTop;
    var offsetX = _rect.x - stageX;
    var offsetY = _rect.y - stageY;
    _hitTouches[i].offset = new Point(offsetX, offsetY);
  }
  _dragEnabled = true;
}