HTML5のCanvasでつくるダイナミックな表現―CreateJSを使う

第11回マウスポインタの動きに合わせてインスタンスをランダムに落とす

今回から取り組む新たなお題をjsdo.itに公開した。マウスポインタの動きに合わせてインスタンスがつくられ、ランダムな向きと速さで落ちていく図1⁠。インスタンスにはスプライトアニメーションを用いている。スプライトアニメーションは次回に解説するので、今回はShapeオブジェクトを使う。ランダムな方向に初速を与えて、自然落下させる動きが課題になる。

図1 マウスポインタの動きに合わせてつくられたインスタンスがランダムに落ちる
図1 マウスポインタの動きに合わせてつくられたインスタンスがランダムに落ちる

マウスポインタの動く位置にランダムなカラーと大きさのインスタンスを置く

落下のアニメーションに入る前に、マウスポインタの動く位置にランダムなカラーと大きさのShapeインスタンスを置いてみよう図2⁠。使うライブラリはEaselJSなので、script要素に読込んでおく。また、body要素のonload属性で、初期設定の関数(initialize())を呼出すという前提だ。

<script src="http://code.createjs.com/easeljs-0.6.1.min.js"></script>
図2 マウスポインタの動く位置にランダムなカラーと大きさのインスタンスが描かれる
図2 マウスポインタの動く位置にランダムなカラーと大きさのインスタンスが描かれる

初期設定の関数は、つぎのようにStage.stagemousemoveイベントにリスナー関数(addInstance())を加える。そして、リスナー関数から改めてShapeインスタンスをつくる関数(createInstance())が呼ばれている。引数には、インスタンスを描くべきマウスポインタのxy座標が渡される。つまり、マウスポインタを動かすと、その座標にインスタンスがつくられるということだ。

var stage;
function initialize() {
  var canvasElement = document.getElementById("myCanvas");
  stage = new createjs.Stage(canvasElement);
  stage.addEventListener("stagemousemove", addInstance);
}
function addInstance(eventObject) {
  createInstance(stage.mouseX, stage.mouseY);
  stage.update();
}

Shapeインスタンスをつくる関数(createInstance())は、以下のようにランダムなカラーと大きさで円のShapeインスタンスを別の関数(createShape())でつくる。引数には描くインスタンスの半径の最小値と最大値を渡す。そして、関数から返されたShapeインスタンスの位置を定め、Stageオブジェクトの子として表示リストに加えている。

円のShapeインスタンスをつくって返す関数(createShape())は、ランダムな半径とカラーでインスタンスに円形を描く。最大値と最小値の範囲からランダムな数値を得る処理はほかにも使うので、別に関数(getRandom())として定めた。これで、ステージ上で動かすマウスポインタの位置に、ランダムなカラーと大きさの円形のShapeインスタンスがつぎつぎに描かれていく(前掲図2⁠。

function createInstance(x, y) {
  var instance = createShape(2, 10);
  instance.x = x;
  instance.y = y;
  stage.addChild(instance);
}
function createShape(min, max) {
  var instance = new createjs.Shape();
  var radius = getRandom(min, max);
  var color = Math.floor(Math.random() * 0xFFFFFF);
  instance.graphics.beginFill(createjs.Graphics.getRGB(color))
  .drawCircle(0, 0, radius);
  return instance;
}
function getRandom(min, max) {
  var randomNumber = Math.random() * (max - min) + min;
  return randomNumber;
}

インスタンスを垂直に落とすアニメーション

ステージにつぎつぎに置いたインスタンスに、ランダムな速さで垂直に落ちるアニメーションを加えよう。アニメーションはTicker.tickイベントで扱うのがお約束だ。そこで、以下のように初期設定の関数(initialize())でイベントリスナー(animate)を加えた。

落ちる速さはインスタンスごとにランダムにしたい。そのため、インスタンスをつくる関数(createInstance())では、垂直方向の初速に加えてフェードアウトするアルファを、ランダムな値でインスタンスのプロパティ(velocityYとvelocityAlpha)に定めている。初速は正負あり、引数(halfSpeed)でランダムな範囲の数値を受け取ることにした。負の初速は、インスタンスを始め上に飛ばしてから、次第に速度を落として落下させる。

Ticker.tickイベントのリスナー関数(animate())は、Stageオブジェクトの表示リストからすべてのインスタンスを取出して、インスタンスに定めた速度(velocityY)とアルファ(velocityAlpha)のプロパティを用いて、落下とフェードアウトのアニメーションにする。メソッドContainer.getNumChildren()は子オブジェクトの数を返し、Container.getChildAt()で引数のインデックスの子オブジェクトが表示リストから取り出せる。速度のプロパティには、重力加速度を表す定数値(2)を加えている。

function initialize() {

  createjs.Ticker.addEventListener("tick", animate);
}
function addInstance(eventObject) {
  // createInstance(stage.mouseX, stage.mouseY);
  createInstance(stage.mouseX, stage.mouseY, 15);

}
// function createInstance(x, y) {
function createInstance(x, y, halfSpeed) {
  var speed = getRandom(-halfSpeed, halfSpeed);
  var instance = createShape(2, 10);

  instance.velocityY = speed;
  instance.velocityAlpha = getRandom(-0.07, -0.01);
}
function animate(eventObject) {
  var count = stage.getNumChildren() - 1;
  for (var i = count; i > -1; i--) {
    var child = stage.getChildAt(i);
    var newY = child.y + child.velocityY;
    var newAlpha = child.alpha + child.velocityAlpha;
    child.y = newY;
    child.alpha = newAlpha;
    child.velocityY += 2;
  }
  stage.update();
}

これで、マウスポインタの動きに合わせてステージに置かれたインスタンスは、ランダムな初速から落下するようになる図3⁠。しかし、コードとしては問題がある。インスタンスをただひたすらつくるだけだからだ。ステージの下端を超えても、アルファが完全に透明になっても、すべてのインスタンスにアニメーションの処理は行われ続ける。

図3 マウスポインタの動きに合わせてステージに置かれたインスタンスがランダムな速さで落ちていく
図3 マウスポインタの動きに合わせてステージに置かれたインスタンスがランダムな速さで落ちていく

インスタンスのアルファが0以下になるか、位置がステージの下端を超えたら、表示リストから消すことにしよう。表示リストのインデックスを渡して子インスタンスを除くのはContainer.removeChildAt()メソッドだ。Ticker.tickイベントのリスナー関数(animate())には、つぎのようなif文の処理を加える。なお、ステージ下端の座標はグローバルな変数(stageHeight)で宣言して、初期設定の関数(initialize())で値を納めた。

var stageHeight;
function initialize() {
  var canvasElement = document.getElementById("myCanvas");
  stageHeight = canvasElement.height;

}

function animate(eventObject) {
  var count = stage.getNumChildren() - 1;
  for (var i = count; i > -1; i--) {
    var child = stage.getChildAt(i);
    var newY = child.y + child.velocityY;
    var newAlpha = child.alpha + child.velocityAlpha;
    if (newAlpha <= 0 || newY > stageHeight) {
      stage.removeChildAt(i);
    } else {
      child.y = newY;
      child.alpha = newAlpha;
      child.velocityY += 2;
    }
  }
  stage.update();
}

ひとつ説明を補っておく。アニメーションの関数(animate())に加えたfor文は、表示リストの子インスタンスをインデックスの大きい順に取り出している。これは、表示リストのインデックス0から順に調べると、子インスタンスを削除したとき、後から取出すインスタンスのインデックスが1ずつ繰り上がって、ずれてしまうからだ。大きい方から調べれば、子インスタンスを除いても、これから取出すインスタンスの番号は狂わない。

これでアルファや垂直位置がステージで見えなくなれば、インスタンスは表示リストから除かれる。アニメーションの見た目(前掲図3に変わりはなくても、処理の無駄は多いに省けた。script要素全体は、つぎのコード1のとおりだ。

コード1 マウスポインタの動きに合わせて表れたランダムなインスタンスが垂直に落ちる
var stage;
var stageHeight;
function initialize() {
  var canvasElement = document.getElementById("myCanvas");
  stageHeight = canvasElement.height;
  stage = new createjs.Stage(canvasElement);
  stage.addEventListener("stagemousemove", addInstance);
  createjs.Ticker.addEventListener("tick", animate);
}
function addInstance(eventObject) {
  createInstance(stage.mouseX, stage.mouseY, 15);
  stage.update();
}
function createInstance(x, y, halfSpeed) {
  var speed = getRandom(-halfSpeed, halfSpeed);
  var instance = createShape(2, 10);
  instance.x = x;
  instance.y = y;
  instance.velocityY = speed;
  instance.velocityAlpha = getRandom(-0.07, -0.01);
  stage.addChild(instance);
}
function animate(eventObject) {
  var count = stage.getNumChildren() - 1;
  for (var i = count; i > -1; i--) {
    var child = stage.getChildAt(i);
    var newY = child.y + child.velocityY;
    var newAlpha = child.alpha + child.velocityAlpha;
    if (newAlpha <= 0 || newY > stageHeight) {
      stage.removeChildAt(i);
    } else {
      child.y = newY;
      child.alpha = newAlpha;
      child.velocityY += 2;
    }
  }
  stage.update();
}
function createShape(min, max) {
  var instance = new createjs.Shape();
  var radius = getRandom(min, max);
  var color = Math.floor(Math.random() * 0xFFFFFF);
  instance.graphics.beginFill(createjs.Graphics.getRGB(color))
  .drawCircle(0, 0, radius);
  return instance;
}
function getRandom(min, max) {
  var randomNumber = Math.random() * (max - min) + min;
  return randomNumber;
}

インスタンスの動きに水平方向の初速を加える

落ちるインスタンスの動きに、水平方向の初速も加えよう。簡単なのは、水平方向にもランダムな速度を定めることだ。しかし、今回ランダムな速度はひとつにする。その代わり、ランダムな角度を加える。速度と角度から水平・垂直それぞれの値を決めるのだ。そのためには、三角関数を用いる。

原点O(0, 0)を中心に描いた半径1の円(⁠⁠単位円」と呼ぶ)について、原点Oからx軸正方向に対して角度θの直線と交わる点Pのxy座標は三角関数により(cosθ, sinθ)と定められている図4⁠。つまり、原点からの距離は1で角度がθの点のxy座標は(cosθ, sinθ)ということだ。すると、原点からの距離がrなら、座標は(r cosθ, r sinθ)になる。

図4 原点から距離が1で角度θのxy座標は(cosθ, sinθ)
図4 原点から距離が1で角度θのxy座標は(cosθ, sinθ)

この距離rを速さと見立てれば、角度θを与えたときのxy各軸方向の速度が導ける。そこで、以下のインスタンスをつくる関数(createInstance())は、0から2πラジアン(360度)までのランダムな角度(angle)を求め、水平・垂直方向の初速をプロパティ(velocityXとvelocityY)として与えるように書替えた。

また、アニメーションの関数(animate())には、水平方向の動きを加えた。水平には重力は働かないので、基本は等速運動だ。ただ、摩擦などによる減速を表すため、定数(0.98)を乗じた。

function createInstance(x, y, halfSpeed) {
  var speed = getRandom(-halfSpeed, halfSpeed);
  var angle = getRandom(0, Math.PI * 2);
  var instance = createShape(2, 10);

  instance.velocityX = Math.cos(angle) * speed;
  // instance.velocityY = speed;
  instance.velocityY = Math.sin(angle) * speed; 

}
function animate(eventObject) {
  var count = stage.getNumChildren() - 1;
  for (var i = count; i > -1; i--) {
    var child = stage.getChildAt(i);
    var newY = child.y + child.velocityY;

      child.x += child.velocityX;
      child.y = newY;

      child.velocityX *= 0.98;
      child.velocityY += 2;

  }

}

これでマウスポインタの動く位置に表れるインスタンスに、ランダムな方向の初速が与えられる図5⁠。垂直方向には重力加速度が加わりつつ、水平移動は少しずつ減速する。書替えたJavaScript全体は、つぎのコード2のとおりだ。

図5 マウスポインタの動く位置に表れたインスタンスにランダムな方向の初速が与えられる
図5 マウスポインタの動く位置に表れたインスタンスにランダムな方向の初速が与えられる
コード2マウスポインタの動きに合わせて表れたインスタンスがランダムな方向に落ちる
var stage;
var stageHeight;
function initialize() {
  var canvasElement = document.getElementById("myCanvas");
  stageHeight = canvasElement.height;
  stage = new createjs.Stage(canvasElement);
  stage.addEventListener("stagemousemove", addInstance);
  createjs.Ticker.addEventListener("tick", animate);
}
function addInstance(eventObject) {
  createInstance(stage.mouseX, stage.mouseY, 15);
  stage.update();
}
function createInstance(x, y, halfSpeed) {
  var speed = getRandom(-halfSpeed, halfSpeed);
  var angle = getRandom(0, Math.PI * 2);
  var instance = createShape(2, 10);
  instance.x = x;
  instance.y = y;
  instance.velocityX = Math.cos(angle) * speed;
  instance.velocityY = Math.sin(angle) * speed;
  instance.velocityAlpha = getRandom(-0.07, -0.01);
  stage.addChild(instance);
}
function animate(eventObject) {
  var count = stage.getNumChildren() - 1;
  for (var i = count; i > -1; i--) {
    var child = stage.getChildAt(i);
    var newY = child.y + child.velocityY;
    var newAlpha = child.alpha + child.velocityAlpha;
    if (newAlpha <= 0 || newY > stageHeight) {
      stage.removeChildAt(i);
    } else {
      child.x += child.velocityX;
      child.y = newY;
      child.alpha = newAlpha;
      child.velocityX *= 0.98;
      child.velocityY += 2;
    }
  }
  stage.update();
}
function createShape(min, max) {
  var instance = new createjs.Shape();
  var radius = getRandom(min, max);
  var color = Math.floor(Math.random() * 0xFFFFFF);
  instance.graphics.beginFill(createjs.Graphics.getRGB(color))
  .drawCircle(0, 0, radius);
  return instance;
}
function getRandom(min, max) {
  var randomNumber = Math.random() * (max - min) + min;
  return randomNumber;
}

前掲コード2をjsdo.itにサンプルコードとして掲げた。今回は、ここまでにしておこう。次回は少し目先を変えてスプライトシートアニメーションについて解説したうえで、今回の弾けるインタラクティブなアニメーションのコードに組み込んでみる。

おすすめ記事

記事・ニュース一覧