CreateJSでドラッグして回転させる動きに挑戦

CreateJSを使って、中村勇吾さんがFlashで作られたintentionalliesのドラッグして回す動きをHTML5 canvasで作ってみました。中村勇吾さんは、私が最もリスペクトするクリエーターで、初めてこのサイトを見たとき、どうやって処理しているんだろうという好奇心とその動きの気持ち良さに暫く没頭して操作していたのを覚えています。

サンプル

下記のリンクよりサンプルをご覧頂けます。HTML5 canvasに対応したWindows, Mac, iOS, Androidの各ブラウザで動作確認しています。以下、このサンプルの処理について解説していきますが、実際に中村勇吾さんの作られたソースを見た訳ではないので、間違いが多々あると思います。予めご了承ください。

なお、サンプルではCreateJSの他に自作のリキッドレイアウト用ライブラリ LiquidLayouter for CreateJS、Pointクラスを拡張するpoint-ex.jsを使用しています。

ドラッグ時の処理

まず、画像のドラッグ時の処理を見てみましょう。下記は、画像のドラッグ時に実行されるメソッドです。画像の座標currentPosition、現在のマウス座標currentMouse、直前のマウス座標lastMouseから力のモーメントを算出し、そこから回転の加速度accelerationを求めています。この加速度の値を角速度angularSpeedに反映させますが、目標としたサイトの動きに近づけるため、加速度や角速度などの状況に応じて色々と処理を分岐させています。

かなり試行錯誤を重ねたことがソースからも伺えると思いますが、それでも目標としたサイトの動きとは違うと自覚しています。特に画像の中央付近をドラッグしたときの回転方向の正確さが雲泥の差です。正直悔しいですが、現在の私の実力と受け止めて、日々精進していきたいと思います。

JavaScript

p.drag = function() {
  var currentPosition = new createjs.Point(this.entity.x, this.entity.y);
  var currentMouse = new createjs.Point(_stage.mouseX, _stage.mouseY);
  var radius = this.lastMouse.subtract(currentPosition);
  var force = currentMouse.subtract(this.lastMouse);
  var moment = getCrossProduct2D(radius, force);
  var inertiaMoment = this.mass * Math.pow(radius.getLength() + 20, 2);
  var acceleration = moment / inertiaMoment * SENSITIVITY;
  var radian = NaN;
  if (acceleration !== 0) {
    var absAcceleration = Math.abs(acceleration);
    var absAngularSpeed = Math.abs(this.angularSpeed);
    var friction;
    if ((0 < this.angularSpeed && acceleration < 0) || (this.angularSpeed < 0 && 0 < acceleration)) {
      if (absAngularSpeed < 3) {
        friction = rangeConversion(0.02, 1, 0, 20, Math.min(absAngularSpeed, 20));
        this.angularSpeed += acceleration * friction;
        this.angularSpeed = (Math.abs(this.angularSpeed) < 0.03) ? 0 : this.angularSpeed;
      }
      radian = this.angularSpeed * createjs.Matrix2D.DEG_TO_RAD;
    } else {
      radian = (this.angularSpeed + acceleration) * createjs.Matrix2D.DEG_TO_RAD;
      if (20 < absAcceleration || radius.getLength() < 3) {
        this.angularSpeed += acceleration;
      } else {
        friction = rangeConversion(0.02, 1, 0, 20, Math.min(absAngularSpeed, 20));
        this.angularSpeed += acceleration * friction;
        this.angularSpeed = (Math.abs(this.angularSpeed) < 0.03) ? 0 : this.angularSpeed;
      }
    }
  } else {
    if (this.angularSpeed !== 0) {
      radian = this.angularSpeed * createjs.Matrix2D.DEG_TO_RAD;
    }
  }
  if (isNaN(radian)) {
    this.lastMouse = currentMouse.clone();
    return;
  }
  var matrix = this.entity.getMatrix();
  matrix.translate(-this.lastMouse.x, -this.lastMouse.y);
  matrix.rotate(radian);
  matrix.translate(currentMouse.x, currentMouse.y);
  matrix.decompose(this.entity);
  this.lastMouse = currentMouse.clone();
  if (Math.abs(this.angularSpeed) < 0.1) {
    this.angularSpeed = 0;
  } else {
    this.angularSpeed *= FRICTION;
  }
  this.updateStageRatio();
};

ドラッグ終了時の処理

次にドラッグ終了時の処理を見てみましょう。この処理では主に慣性力を求めています。まず、角速度angularSpeedから作用点workingPointにおける遠心力のベクトルcentrifugalForceを求めます。ドラッグ終了後は、画像の中央を軸に回転させるので、遠心力のベクトルの始点を画像の中央に変更して、さらにマウスの動きforceを加算することにより、慣性力のベクトルinertiaForceを求めています。

JavaScript

p.release = function() {
  var currentPosition = new createjs.Point(this.entity.x, this.entity.y);
  var currentMouse = new createjs.Point(_stage.mouseX, _stage.mouseY);
  var radius = this.lastMouse.subtract(currentPosition);
  var force = currentMouse.subtract(this.lastMouse);
  var workingPoint = new createjs.Point().subtract(radius);
  var radian = this.angularSpeed * createjs.Matrix2D.DEG_TO_RAD;
  var cos = Math.cos(radian);
  var sin = Math.sin(radian);
  var centrifugalForceX = cos * workingPoint.x - sin * workingPoint.y;
  var centrifugalForceY = sin * workingPoint.x + cos * workingPoint.y;
  var centrifugalForce = new createjs.Point(centrifugalForceX, centrifugalForceY);
  centrifugalForce = centrifugalForce.subtract(workingPoint);
  this.inertiaForce = centrifugalForce.add(force);
  this.state = State.COAST;
  var currentX = currentPosition.x;
  var currentY = currentPosition.y;
  if (currentX < 0) {
    this.entity.x = 0;
  } else {
    var stageWidth = _layouter.getStageWidth();
    if (stageWidth < currentX) {
      this.entity.x = stageWidth;
    }
  }
  if (currentY < 0) {
    this.entity.y = 0;
  } else {
    var stageHeight = _layouter.getStageHeight();
    if (stageHeight < currentY) {
      this.entity.y = stageHeight;
    }
  }
  this.updateStageRatio();
};

慣性の処理

次はドラッグ終了後の慣性の処理です。前述の処理で求めた角速度angularSpeedと慣性力inertiaForceをインスタンスに反映させた後、徐々に減衰するようにしています。角速度と慣性力が微小な値になったら、インスタンスを休止状態とし、処理を行わないようにしています。

JavaScript

p.coast = function() {
  var currentX = this.entity.x;
  var currentY = this.entity.y;
  var radian = this.angularSpeed * createjs.Matrix2D.DEG_TO_RAD;
  var matrix = this.entity.getMatrix();
  matrix.translate(-currentX, -currentY);
  matrix.rotate(radian);
  matrix.translate(currentX + this.inertiaForce.x ,currentY + this.inertiaForce.y);
  matrix.decompose(this.entity);
  this.inertiaForce.normalize(this.inertiaForce.getLength() * FRICTION);
  this.angularSpeed *= FRICTION;
  currentX = this.entity.x;
  currentY = this.entity.y;
  if ((currentX < 0 && this.inertiaForce.x < 0) || (_layouter.getStageWidth() < currentX && 0 < this.inertiaForce.x)) {
    this.inertiaForce.x = -this.inertiaForce.x;
  }
  if ((currentY < 0 && this.inertiaForce.y < 0) || (_layouter.getStageHeight() < currentY && 0 < this.inertiaForce.y)) {
    this.inertiaForce.y = -this.inertiaForce.y;
  }
  if (Math.abs(this.angularSpeed) < 0.1 && this.inertiaForce.getLength() < 0.1) {
    this.angularSpeed = 0;
    delete this.inertiaForce;
    this.state = State.REST;
    this.shape.visible = false;
  }
  this.updateStageRatio();
};