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

第12回スプライトシートでアニメーションをつくる

今回は少し目先が変わって、スプライトシートでアニメーションをつくる。⁠スプライトシート」というのは、アニメーションのひとコマひとコマをひとつにまとめた画像ファイルだ図1⁠。ファイルをひとつにすると、読込みが1回で済むため効率がいい。画像からそれぞれのコマのグラフィックイメージを切り出して、アニメーションとして再生する。パフォーマンスを重く見るゲームコンテンツでも使われる仕組みだ。

図1 スプライトシートの画像ファイル
図1 スプライトシートの画像ファイル

スプライトシートを用意する

これからつくるのは、前掲図1のスプライトシートを使った簡単なアニメーションだ。でき上がりのコンテンツを先にjsdo.itでお見せしよう。

スプライトシートは、ひとつあるいは複数の画像ファイルで成立つ。スプライトシートがつくれるツールはいろいろあり、どれを使ってもよい。本稿では、Flash Professional CCを用いた[1]⁠。大事なのは、画像からコマごとのグラフィックイメージを切り出すということだ。つまり、それぞれのコマについて、予め矩形領域を定めておかなければならない。簡単なのは、すべてのコマを同じ大きさにすることだ図2⁠。また、背景を抜きたいなら画像ファイルはPNG形式がよいだろう。

図2 すべてのコマを同じ大きさでPNGファイルに納める
図2 すべてのコマを同じ大きさでPNGファイルに納める

SpriteSheetとBitmapAnimationクラス

スプライトシートアニメーションをつくるには、SpriteSheetとBitmapAnimationのふたつのクラスを使う。まず、SpriteSheetクラスで、スプライトシートのどこからそれぞれのコマのグラフィックイメージを切り出し、どのような順序でアニメーションにするのか定めたSpriteSheetオブジェクトをつくる。引数にはアニメーションを組み立てるための情報をオブジェクトで渡す。つぎに、そのSpriteSheetオブジェクトをBitmapAnimationクラスのコンストラクタに渡して、ステージに置いて再生や停止などの制御ができるBitmapAnimationオブジェクトをつくるという段取りだ。

new createjs.SpriteSheet(情報オブジェクト)

new createjs.BitmapAnimation(SpriteSheetオブジェクト)

SpriteSheet()コンストラクタの引数に渡す情報オブジェクトには、基本としてimagesとframes、およびanimationsの3つのプロパティを与える表1⁠。ただ、それらに納める中身や定め方にはバリエーションがある。詳しくは、公式リファレンスSpriteSheetの例(SpriteSheet Format)を参照してほしい。

表1 情報オブジェクトに定める基本のプロパティ
プロパティ
images画像イメージのImageオブジェクトまたはURIの文字列をエレメントに納めた配列
framesフレーム領域の情報をプロパティに納めたObjectインスタンス
animationsアニメーション名をプロパティとして、値に再生のための情報を配列またはObjectインスタンスで定める

今回のお題では、次のようなJavaScriptコードを書いた。情報オブジェクト(data)のimagesプロパティには、スプライトシートのパス(file)ひとつを配列に納めて与えた。framesプロパティには、Objectインスタンスで幅と高さ、および基準点の座標を定めた。

{width:幅, height:高さ, regX:水平基準点, regY:垂直基準点}

animationsプロパティの中身については、この後説明する。こうして設定した情報オブジェクトがSpriteSheet()コンストラクタに渡され、できあがったSpriteSheetオブジェクト(mySpriteSheet)は、さらにBitmapAnimation()コンストラクタの引数にして、BitmapAnimationオブジェクト(mySprite)がつくられている。

var data = {};
data.images = [file];
data.frames = {width:82, height:109, regX:41, regY:55};
data.animations = {walk: {
    frames: [0, 0, 1, 2, 2, 3],
    frequency: 3
  }
};
var mySpriteSheet = new createjs.SpriteSheet(data);
var mySprite = new createjs.BitmapAnimation(mySpriteSheet);

改めて、animationsプロパティだ。アニメーションの名前(walk)をプロパティにつけて、値にはObjectインスタンスが与えてある。

そのframesプロパティには、再生するコマ(フレーム)を配列エレメントで定めた。スプライトシートから切り出すグラフィックイメージには、0から始まる連番インデックスが振られる図3⁠。そのインデックスを再生順に配列に納める。同じイメージを複数のコマで続けて見せたければ、その数だけインデックスを加えればよい。なお、このインデックスはフレーム番号と呼ばれることもある。

図3 スプライトシートのグラフィックイメージに振られる連番インデックス
図3 スプライトシートのグラフィックイメージに振られる連番インデックス

プロパティfrequencyは、アニメーションの速さを整数で決める。ただし、値が大きいほど遅くなる。スプライトシートアニメーションは、Ticker.tickイベントで再生が進む。frequencyプロパティの整数値は、そのイベントいくつごとにひとコマ進めるかを定める。アニメーションの世界で何コマおきという指定に当たる。

SpriteSheetとBitmapAnimationのふたつのクラスでスプライトシートアニメーションをつくって再生するのが、次のコード1だ。上述のスプライトシートアニメーションのオブジェクトをつくる処理は、関数(createAnimation())に定めて初期設定の関数(initialize())から呼び出している。また、スプライトシートのアニメーションは、BitmapAnimation.gotoAndPlay()メソッドで再生する。引数には、情報オブジェクトのanimationsプロパティに定めた再生したいアニメーション名を渡す[2]⁠。

BitmapAnimationオブジェクト.gotoAndPlay(アニメーション名)

アニメーションする画面の書替えは、お約束どおりTicker.tickイベントのリスナー関数(animate())で行う。この関数が行っているのは、今のところStage.update()メソッドの呼出しだけだ。これで、スプライトシートのアニメーションがステージのうえで再生される図4⁠。

コード1 SpriteSheetとBitmapAnimationクラスでスプライトシートアニメーションをつくって再生する
var stage;
var animation;
var stageWidth;
var stageHeight;
function initialize() {
  var canvasElement = document.getElementById("myCanvas");
  stageWidth = canvasElement.width;
  stageHeight = canvasElement.height;
  stage = new createjs.Stage(canvasElement);
  animation = createAnimation("images/sprite_sheet.png");
  stage.addChild(animation);
  animation.x = stageWidth / 2;
  animation.y = stageHeight / 2;
  animation.gotoAndPlay("walk");
  createjs.Ticker.addEventListener("tick", animate);
}
function animate(eventObject) {
  stage.update();
}
function createAnimation(file) {
  var data = {};
  data.images = [file];
  data.frames = {width:82, height:109, regX:41, regY:55};
  data.animations = {walk: {
      frames: [0, 0, 1, 2, 2, 3],
      frequency: 3
    }
  };
  var mySpriteSheet = new createjs.SpriteSheet(data);
  var mySprite = new createjs.BitmapAnimation(mySpriteSheet);
  return mySprite;
}
図4 スプライトシートのアニメーションが再生される
図4 スプライトシートのアニメーションが再生される

スプライトアニメーションをマウスポインタが動いた位置につくって落とす

さて、このスプライトアニメーションを前回の第11回コード2に組み込んで、マウスポインタの動きに合わせて弾けさせよう。まず、インスタンスは初めからステージに置くのでなく、Stage.stagemousemoveイベントのリスナー関数(addInstance())でマウスポインタの座標に加えていく。リスナー関数からさらにインスタンスをつくる関数(createInstance())が呼出されるのは、第11回コード2と同じだ。

つぎに、インスタンスをつくる関数(createInstance())の処理も、基本は第11回コード2と変わっていない。ただ、スプライトアニメーションを毎回頭からつくり直すのは煩わしい。そこで、変数(animation)にとっておいたBitmapAnimationオブジェクトにBitmapAnimation.clone()メソッドを呼び出して、同じ中身の新たなオブジェクトを複製することにした。また、インスタンスの大きさは伸縮のプロパティDisplayObject.scaleXDisplayObject.scaleYでランダムな値の範囲(0.4~1)に定めている。新たなBitmapAnimationインスタンスは、BitmapAnimation.gotoAndPlay()メソッドで再生するのを忘れてはいけない。

function initialize() {

  animation = createAnimation("images/sprite_sheet_s.png");
  /*
  stage.addChild(animation);
  animation.x = stageWidth / 2;
  animation.y = stageHeight / 2;
  animation.gotoAndPlay("walk");
  */
  stage.addEventListener("stagemousemove", addInstance);

}
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);
  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 getRandom(min, max) {
  var randomNumber = Math.random() * (max - min) + min;
  return randomNumber;
}

Ticker.tickイベントのリスナー関数(animate())は、第11回コード2そのままだ。BitmapAnimationインスタンスは、ランダムに定めた初速から自由落下し、アルファや垂直位置によりステージから見えなくなったら、Stageオブジェクトの表示リストから除かれる。

function initialize() {

	createjs.Ticker.addEventListener("tick", animate);
}

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();
}

こうして書き直したスクリプト全体は、次のコード2のとおりだ。もっとも、前掲コード1で使ったスプライトシートは、このお題には大き過ぎる。そこで、画像ファイルの大きさは、幅・高さとも半分にした。したがって、スプライトアニメーションのオブジェクトをつくる関数(createAnimation())で、情報オブジェクト(data)のframesプロパティの値が半分に変わっている。

これで、マウスポインタの座標にスプライトシートアニメーションがランダムな設定でつぎつぎとつくられ、アルファを下げながら自由落下する図5⁠。jsdo.itにもコードを掲げた。今回使ったスプライトシートでは、少しばかり鬱陶しい絵面になったかもしれない。だが、でき上がりの見た目は素材次第で変わる。次回は、さらに表現の工夫を凝らしたい。

図5 マウスポインタの動く位置に表れたスプライトアニメーションが自由落下する
図5 マウスポインタの動く位置に表れたスプライトアニメーションが自由落下する
コード2 マウスポインタの動く座標につくられたスプライトアニメーションがランダムな方向に落ちる
var stage;
var animation;
var stageWidth;
var stageHeight;
function initialize() {
  var canvasElement = document.getElementById("myCanvas");
  stageWidth = canvasElement.width;
  stageHeight = canvasElement.height;
  stage = new createjs.Stage(canvasElement);
  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;
  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],
      frequency: 3
    }
  };
  var mySpriteSheet = new createjs.SpriteSheet(data);
  var mySprite = new createjs.BitmapAnimation(mySpriteSheet);
  return mySprite;
}
function getRandom(min, max) {
  var randomNumber = Math.random() * (max - min) + min;
  return randomNumber;
}

おすすめ記事

記事・ニュース一覧