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

第13回モーションブラーと弾むアニメーション

前回つくったマウスポインタに合わせて弾けるスプライトアニメーションに、さらにふたつ表現を加えたい。第1は、アニメーションの残像に、落ちる動きのぼかしを加える。いわゆる「モーションブラー」だ。第2に、ステージの下端まで落ちたインスタンスを弾ませる。簡単な物理の考え方を採入れる。

CreateJSがアップデートされた

先にお伝えしたいのは、CreateJSが2013年9月25日付でアップデートされたことだ。EaselJSは、0.6.1から0.7.0にマイナーバージョンが上がった。今回から早速新たなライブラリを使うことにしよう。すると、script要素に読込むEaselJSライブラリのJavaScriptファイルは差替えなければならない。

<!--
<script src="http://code.createjs.com/easeljs-0.6.1.min.js"></script>
-->
<script src="http://code.createjs.com/easeljs-0.7.0.min.js"></script>

また、前回のスクリプトで用いたクラスBitmapAnimationの名前が、EaselJS 0.7.0からSpriteに変わる。今回のJavaScriptコードでは、新たなクラス名に書替える(後掲コード1参照⁠⁠。

// var mySprite = new createjs.BitmapAnimation(mySpriteSheet);
var mySprite = new createjs.Sprite(mySpriteSheet);

そしてもうひとつ、SpriteSheet()コンストラクタに渡す引数のオブジェクト(data)で、アニメーションの速さを決めるプロパティが変わる。これまでのfrequencyは、Ticker.tick「イベントいくつごとにひとコマ進めるかを定め」(第12回「スプライトシートでアニメーションをつくる」SpriteSheetとBitmapAnimationクラス参照⁠⁠。これがspeedプロパティに改められ、デフォルトの何倍速で進めるのかという数値を与える。具体的には、frequencyの逆数(1 / frequency)となり、数値は大きいほどアニメーションが速くなる(後掲コード1参照⁠⁠。

var data = {};

data.animations = {walk: {
      frames: [0, 0, 1, 2, 2, 3],
      // frequency: 3
      speed: 1 / 3
    }
  };
var mySpriteSheet = new createjs.SpriteSheet(data);

アニメーションの残像にモーションブラーを加える

まずは、アニメーションの残像にモーションブラーを加えよう図1⁠。ただ厳密にいうと、CreateJSにモーションブラーのぼかし機能はない。ぼかし幅が水平と垂直それぞれに定められるので、垂直方向のぼかしを強くしてそれらしく見せる。

図1 残像にモーションブラーを加える
図1 残像にモーションブラーを加える

ぼかしの前に、残像をどうつくるかというと、これはプロパティひとつで定められる。Stage.autoClearというプロパティがそれだ。デフォルト値はtrueで、ステージを描き替える前にCanvasにあるものは自動的に消される。プロパティをfalseにすると、古い描画が残ったまま重ね描きされる。つまり、残像ができることになる図2⁠。

図2 Stage.autoClearプロパティをfalseにするとCanvasに残像が重なる
図2 Stage.autoClearプロパティをfalseにするとCanvasに残像が重なる

では、残像のモーションブラーに移ろう。Canvasに直接残像を描いたことから想像がつくように、ぼかしはCanvasに掛ける。オブジェクトをひとつひとつぼかしていたら負荷が高い。とはいえ、Canvas全体をぼかすのも決して軽くはない。ぼかした残像を加えたアニメーションは、大きくつぎの3つの手順で進める。

  1. Canvasの残像にモーションブラーを加える。
  2. Canvasをアルファが加わった背景色で塗る。
  3. マウスポインタの動きに合わせた新たなインスタンスを加える。

第1に、残像の描かれたCanvas全体に、EaselJSのフィルタでモーションブラーを加える。第2に、Canvas全体をアルファが加わった背景色で塗る。Canvasに同じ大きさの半透明のシートを重ねる感覚だ図3⁠。そうすると、古い残像ほどアルファつき背景色が塗り重ねられて消えてゆく。そのうえで、最後にマウスポインタの動きに合わせた新たなインスタンスを加えればよい。

図3 モーションブラーが加わったCanvasにアルファつきの背景色を塗り重ねる
図3 モーションブラーが加わったCanvasにアルファつきの背景色を塗り重ねる 図3 モーションブラーが加わったCanvasにアルファつきの背景色を塗り重ねる

使うメソッドを確かめていこう。まず、ぼかしについては、第8回「ぼかしフィルタとアルファマスク」BoxBlurFilterクラスで画像イメージをぼかすで解説した。ただし、EaselJS 0.7.0でクラス名がBlurFilterに変わる。引数の定め方は同じだ。前述のとおり、垂直方向のぼかしを強める。また、オブジェクトひとつひとつが小さいアニメーションなので、ぼかし品質は低くして負荷を下げる。

new BlurFilter(水平ぼかし, 垂直ぼかし, ぼかし品質)

つぎに、ぼかしはCanvasに加える。その場合、canvas要素からコンテキストという描画のオブジェクトを得る。そのメソッドがgetContext()だ。メソッドの引数は、取出すコンテキストを定める識別子の文字列である。2次元の描画は"2d"を渡せばよい[1]⁠。

canvas要素.getContext("2d")

CanvasはCreateJSのオブジェクトではないので、DisplayObject.filtersプロパティは使えない。だが、BlurFilterクラスには、Canvasの2Dコンテキストにフィルタを掛けるBlurFilter.applyFilter()メソッドが備わっている。第1引数が2Dコンテキスト、後の4引数は適用する矩形領域を与える。

BlurFilterオブジェクト.applyFilter(2Dコンテキスト, x座標, y座標, 幅, 高さ)

これで道具立ては整った。前回でき上がった第12回コード2マウスポインタの動く座標につくられたスプライトアニメーションがランダムな方向に落ちるに、JavaScriptコードを書き加えていこう。初期設定の関数(initialize())では、getContext()メソッドで2Dコンテキストの参照を変数(context)にとっておく。そして、Stage.autoClearプロパティはfalseに定めて残像を重ねる。

var context;

function initialize() {

  context = canvasElement.getContext("2d");

  stage.autoClear = false;

}

Canvasの残像をぼかしつつ背景色で消してゆく処理は新たな関数(fadeAndBlur())で定め、アニメーションの関数(animate())から呼出す。CanvasはBlurFilter.applyFilter()メソッドで垂直方向を強めにぼかす。BlurFilterインスタンスは使い回せるので、予め変数(blurFilter)に入れておいた。そして、背景色でCanvasを塗るのは、CreateJSでなくCanvasのプロパティおよびメソッドを用いた。fillStyleプロパティでアルファ(0.2)つきの塗り色(0xFFFFFF)を定め、fillRect()メソッドは与えられた矩形領域をその色で塗る(詳しくは、<canvas>要素で定めた領域に図形を描くをお読みいただきたい⁠⁠。

var blurFilter = new createjs.BlurFilter(2, 4, 1);

function animate(eventObject) {

  fadeAndBlur();

}

function fadeAndBlur() {
  blurFilter.applyFilter(context, 0, 0, stageWidth, stageHeight);
  context.fillStyle = createjs.Graphics.getRGB(0xFFFFFF, 0.2);
  context.fillRect(0, 0, stageWidth, stageHeight);
}

これでマウスポインタに合わせて落ちるオブジェクトのアニメーションに残像が重なり、モーションブラーも加わる(前掲図1参照⁠⁠。書替えたスクリプト全体は、つぎのコード1のとおりだ。

コード1 オブジェクトが落ちるアニメーションの残像にモーションブラーを加える
var stage;
var animation;
var stageWidth;
var stageHeight;
var context;
var blurFilter = new createjs.BlurFilter(2, 4, 1);
function initialize() {
  var canvasElement = document.getElementById("myCanvas");
  context = canvasElement.getContext("2d");
  stageWidth = canvasElement.width;
  stageHeight = canvasElement.height;
  stage = new createjs.Stage(canvasElement);
  stage.autoClear = false;
  animation = createAnimation("images/sprite_sheet_s.png");
  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 = animation.clone();
  instance.x = x;
  instance.y = y;
  instance.scaleX = instance.scaleY = getRandom(0.4, 1);
  instance.velocityX = Math.cos(angle) * speed;
  instance.velocityY = Math.sin(angle) * speed;
  instance.velocityAlpha = getRandom(-0.07, -0.01);
  instance.gotoAndPlay("walk");
  stage.addChild(instance);
}
function animate(eventObject) {
  var count = stage.getNumChildren() - 1;
  fadeAndBlur();
  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 createAnimation(file) {
  var data = {};
  data.images = [file];
  data.frames = {width:41, height:55, regX:20, regY:27};
  data.animations = {walk: {
      frames: [0, 0, 1, 2, 2, 3],
      speed: 1 / 3
    }
  };
  var mySpriteSheet = new createjs.SpriteSheet(data);
  var mySprite = new createjs.Sprite(mySpriteSheet);
  return mySprite;
}
function fadeAndBlur() {
  blurFilter.applyFilter(context, 0, 0, stageWidth, stageHeight);
  context.fillStyle = createjs.Graphics.getRGB(0xFFFFFF, 0.2);
  context.fillRect(0, 0, stageWidth, stageHeight);
}
function getRandom(min, max) {
  var randomNumber = Math.random() * (max - min) + min;
  return randomNumber;
}

EaselJS 0.7.0について、もう少し補っておく。このバージョンのコンパクト版JavaScript(JS)ファイルeaseljs-0.7.0.min.jsには、フィルタのクラスも含まれるようになった。そのため、フィルタクラスのJSファイルをひとつひとつ読込まないでよい。また、BlurFilterになって、パフォーマンスも上がったという。

オブジェクトをステージ下端で弾ませる

オブジェクトがステージの下端まで落ちたら、上に弾ませたい。考え方は簡単で、垂直方向の速度(velocityY)を逆転つまり-1を乗じればよい。また、ステージ下端を超えて下がってしまった垂直座標は、その分上に持ち上げる。手を加えるのは、アニメーションの関数(animate())だ。

まず、前掲コード1では、オブジェクトがステージ下端座標(stageHeight)を超えたら、Stageオブジェクトの表示リストから除くことにしている。この条件は外す。替わりに、水平座標が両端の外に出たことを新たな条件に加えよう。オブジェクトがつぎに移る水平座標は変数(newX)にとっておく。

function animate(eventObject) {
  var count = stage.getNumChildren() - 1;

  for (var i = count; i > -1; i--) {
    var child = stage.getChildAt(i);
    var newX = child.x + child.velocityX;   // 追加
    var newY = child.y + child.velocityY;
    var newAlpha = child.alpha + child.velocityAlpha;
    // if (newAlpha <= 0 || newY > stageHeight) {
    if (newAlpha <= 0 || newX > stageWidth || newX < 0) {
      stage.removeChildAt(i);
    } else {

    }
  }

}

つぎに、オブジェクトの垂直座標がステージ下端を超えたら弾ませる。垂直方向の速度(velocityY)を逆転するとともに、少し勢いが衰えるよう-0.8を乗じた。ステージ下端をはみ出た垂直位置の大きさは、ステージの高さ(stageHeight)との剰余をとる。その大きさだけ、垂直位置をステージ下端から持ち上げればよい。その後のオブジェクトを落とす処理は基本的に変わらない。

function animate(eventObject) {

  if (newAlpha <= 0 || newX > stageWidth || newX < 0) {

  } else {
    if (newY > stageHeight) {
      child.velocityY *= -0.8;
      newY = stageHeight - newY % stageHeight;
    }
    // child.x += child.velocityX;
    child.x = newX;
    child.y = newY;
    child.alpha = newAlpha;
    child.velocityX *= 0.98;
    child.velocityY += 2;
  }

}

これで、落ちたオブジェクトは、ステージ下端で弾むアニメーションになる図4⁠。残像のモーションブラーも加わった。今回のお題が目指した表現はひとまずでき上がった。スクリプト全体は以下のコード2のとおりだ。

図4 落ちたオブジェクトはステージ下端で弾む
図4 落ちたオブジェクトはステージ下端で弾む
コード2 ステージ下端まで落ちたオブジェクトを弾ませる
var stage;
var animation;
var stageWidth;
var stageHeight;
var context;
var blurFilter = new createjs.BlurFilter(2, 1, 1);
function initialize() {
  var canvasElement = document.getElementById("myCanvas");
  context = canvasElement.getContext("2d");
  stageWidth = canvasElement.width;
  stageHeight = canvasElement.height;
  stage = new createjs.Stage(canvasElement);
  stage.autoClear = false;
  animation = createAnimation("images/sprite_sheet_s.png");
  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 = animation.clone();
  instance.x = x;
  instance.y = y;
  instance.scaleX = instance.scaleY = getRandom(0.4, 1);
  instance.velocityX = Math.cos(angle) * speed;
  instance.velocityY = Math.sin(angle) * speed;
  instance.velocityAlpha = getRandom(-0.07, -0.01);
  instance.gotoAndPlay("walk");
  stage.addChild(instance);
}
function animate(eventObject) {
  var count = stage.getNumChildren() - 1;
  fadeAndBlur();
  for (var i = count; i > -1; i--) {
    var child = stage.getChildAt(i);
    var newX = child.x + child.velocityX;
    var newY = child.y + child.velocityY;
    var newAlpha = child.alpha + child.velocityAlpha;
    if (newAlpha <= 0 || newX > stageWidth || newX < 0) {
      stage.removeChildAt(i);
    } else {
      if (newY > stageHeight) {
        child.velocityY *= -0.8;
        newY = stageHeight - newY % stageHeight;
      }
      child.x = newX;
      child.y = newY;
      child.alpha = newAlpha;
      child.velocityX *= 0.98;
      child.velocityY += 2;
    }
  }
  stage.update();
}
function createAnimation(file) {
  var data = {};
  data.images = [file];
  data.frames = {width:41, height:55, regX:20, regY:27};
  data.animations = {walk: {
      frames: [0, 0, 1, 2, 2, 3],
      speed: 1 / 3
    }
  };
  var mySpriteSheet = new createjs.SpriteSheet(data);
  var mySprite = new createjs.Sprite(mySpriteSheet);
  return mySprite;
}
function fadeAndBlur() {
  blurFilter.applyFilter(context, 0, 0, stageWidth, stageHeight);
  context.fillStyle = createjs.Graphics.getRGB(0xFFFFFF, 0.2);
  context.fillRect(0, 0, stageWidth, stageHeight);
}
function getRandom(min, max) {
  var randomNumber = Math.random() * (max - min) + min;
  return randomNumber;
}

jsdo.itにもサンプルコードを掲げた。スプライトアニメーションを数多くつくったうえ、残像やぼかしを加えたので、予告したとおり重い動きになった。EaselJS 0.7.0でBlurFilterクラスのパフォーマンスが上がったとはいえ、もう少し動きを軽くできないだろうか。次回はそのあたりについて解説しよう。

おすすめ記事

記事・ニュース一覧