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

第14回オブジェクトの使い回しとアニメーション素材の変更

前回の第13回「モーションブラーと弾むアニメーション」で、ひとまずお題の表現はできた第13回コード2⁠。マウスポインタの動きに合わせて、つぎつぎにつくられるスプライトアニメーションのオブジェクトが落ちて弾み、それらの残像にモーションブラーがかかる図1⁠。今回はこのアニメーションにふたつ手を加えたい。表題のとおり、ひとつはオブジェクトを使い回すようにすること。そしてもうひとつは、スプライトシートアニメーションの素材を差し替えてみる。

図1 落ちて弾むアニメーションとモーションブラーのかかった残像
図1 落ちて弾むアニメーションとモーションブラーのかかった残像

ガベージコレクションを減らす

第13回コード2「ステージ下端まで落ちたオブジェクトを弾ませる」では、Stage.stagemousemoveイベントのリスナー関数(addInstance())で、マウスを動かすたびにSpriteインスタンスがSprite.clone()メソッドによりひたすらつくられる。そのために、メモリが費やされたり、処理が重くなったりしないだろうか。

stage.addEventListener("stagemousemove", addInstance);

function addInstance(eventObject) {
  createInstance(stage.mouseX, stage.mouseY, 15);

}

function createInstance(x, y, halfSpeed) {

  var instance = animation.clone();

}

もちろん、インスタンスをつくりっ放しではない。アニメーションさせるTicker.tickイベントのリスナー関数(animate())では、ステージから見えなくなったオブジェクトは、Container.removeChildAt()メソッドでStageオブジェクトの表示リストから除いている。ただ、ことさらメモリから消すという操作はしていない。

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

    if (newAlpha <= 0 || newX > stageWidth || newX < 0) {
      stage.removeChildAt(i);
    }

  }

}

実は、JavaScriptは、要らなくなったオブジェクトを自動的にメモリから消し去ってくれる。この仕組みを「ガベージコレクション」という[1]⁠。自動的にゴミを探して吸い取ってくれるロボット掃除機のようだ。もっとも、楽なのはスクリプティングする人間の側だけで、掃除機(⁠⁠ガベージコレクタ」と呼ばれる)にとっては手間のかかる仕事になる。

プログラマはメモリの扱いはとくに気にすることなく、JavaScriptのコードを書けばよい。けれど、JavaScriptはその処理の合間をぬって、メモリの片づけをしなければならない。なお、オブジェクトが要らなくなったということは、その参照が残っているかどうかで確かめている。

人気料理店のランチタイムを思い浮かべてもらうとよい。注文をとって料理を運ぶのが店員の仕事だ。しかし、食べ終えた客の皿を片づけなければつぎの客が入れられない。つまり、無駄なメモリが費やされる。片づけに追われると、料理を運ぶのが遅れがちになる。処理の負荷が上がってしまうのだ。

熱狂的なファンをもつある人気ラーメン店では、食べ終わった客が丼を片づけてテーブルを拭くという。それなら、店員の手間が省ける。このアイデアをいただこう。ただし、協力してもらうのはつぎの客だ。要らなくなったオブジェクトは、消さずにとっておく。オブジェクトが必要になったら、とっておいたオブジェクトがあればそれを使ってもらい、新たにはつくらない。

図2 ある人気店のラーメン
図2 ある人気店のラーメン
Shijuukurou氏撮影(CC BY-SA 3.0)

こうすれば、ガベージコレクタの手は煩わせずに済む。それだけではない。一般に、オブジェクトを新たにつくるより、すでにあるオブジェクトの設定をし直す方が負荷は低い。第13回コード2をそのように書直してみよう。

つくったオブジェクトを使い回す

まず、要らなくなったSpriteオブジェクトは、変数(sprites)に定めた配列に納めることにする。つぎに、SpriteインスタンスはSprite.clone()メソッドで直ちにつくらず、とっておいたオブジェクトがあったらそれを使い回す。そうしてオブジェクトを返す関数(getClone())は新たに定めよう。また、アニメーションの関数(animate())では、使い終えたSpriteインスタンスをStageオブジェクトの表示リストから除いて、使い回す配列に入れる。

var sprites = [];

function createInstance(x, y, halfSpeed) {

  // var instance = animation.clone();
  var instance = getClone(animation);

}

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

  for (var i = count; i > -1; i--) {
    var child = stage.getChildAt(i);

    if (newAlpha <= 0 || newX > stageWidth || newX < 0) {
      stage.removeChildAt(i);
      sprites.push(child);
    }

  }

}

オブジェクトを使い回す関数(getClone())は、とっておいたオブジェクトが配列(sprites)にあればそれを取り出し、なければそのときはSprite.clone()メソッドでつくって返す。ここまではよかろう。気をつけなければならないのは、使い回そうとして取出したオブジェクトがまだ前のままということだ。新たに用いるために、プロパティを設定し直さなければならない。Sprite.cloneProps()メソッドは、引数のSpriteオブジェクトのプロパティを参照するオブジェクトの値に改める。

Spriteオブジェクト.cloneProps(対象Spriteオブジェクト)
function getClone(original) {
  var mySprite;
  if (sprites.length) {
    mySprite = sprites.pop();
    original.cloneProps(mySprite);
  } else {
    mySprite = original.clone();
  }
  return mySprite;
}

以上の手を加えたスクリプト全体がつぎのコード1だ。jsdo.itのサンプルも添えた。アニメーションの表現そのものは第13回コード2と同じだ。また、残念ながら体感の動きもあまり差がない。ステージが小さいため、つくられるオブジェクトの数がさほど多くないからだろう。かといって、大きくすればモーションブラーの負荷が上がるので、やはり違いは感じにくい。たとえば、シューティングゲームの銃弾などに使えるテクニックとして覚えておくとよい。

コード1 落ちるアニメーションのSpriteオブジェクトを使い回す
var stage;
var animation;
var stageWidth;
var stageHeight;
var context;
var blurFilter = new createjs.BlurFilter(2, 1, 1);
var sprites = [];
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 = getClone(animation);
  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);
      sprites.push(child);
    } 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;
}
function getClone(original) {
  var mySprite;
  if (sprites.length) {
    mySprite = sprites.pop();
    original.cloneProps(mySprite);
  } else {
    mySprite = original.clone();
  }
  return mySprite;
}

Sprite.cloneProps()メソッドについて、ひとつ補っておく。リファレンスでメソッドを見ると、[protected]の表記がある図3⁠。これは、一般的に使うものではなく、おもに内部的に用いられることを示す。そのため、説明は少ない。

図3 リファレンスのSprite.cloneProps()メソッドにある[protected]の表記
図3 リファレンスのSprite.cloneProps()メソッドにある[protected]の表記

リファレンスでこうした項目を探すときは、クラスのページ冒頭にある[protected]のチェックボックスをクリックしておくことも、併せて覚えておこう図4⁠。

図4 Spriteクラスの[protected]のチェックボックス
図4 Spriteクラスの[protected]のチェックボックス

スプライトアニメーションを差替える

ペンギンが歩くアニメーションもよいが、このお題では少し鬱陶しい。素材を差し替えるだけで、表現の印象はかなり変わる。ダウンロードした「EaselJS-release_v0.7.0」ライブラリにはサンプル(⁠⁠examples⁠⁠)が納められており図5上⁠、その素材(⁠⁠assets⁠⁠)としてひとつ小さなスプライトシート(⁠⁠sparkle_21x23.png⁠⁠)がある図5下⁠。

図5 ⁠EaselJS-release_v0.7.0」に納められたサンプルとその素材
図5 「EaselJS-release_v0.7.0」に納められたサンプルとその素材 図5 「EaselJS-release_v0.7.0」に納められたサンプルとその素材

このスプライトシートはきらめく星のアニメーションだ図6。背景が透明だと見にくいので表示上青に変えた⁠⁠。ファイル名に添えられた数字(⁠⁠21x23⁠⁠)がひとコマの大きさを表している。前掲コード1のスプライトシートをこちらに差し替えてみよう。

図6 きらめく星のスプライトシート
図6 きらめく星のスプライトシート

まず、初期設定の関数(initialize())で、読込むファイルの名前(パス)を書替える。スプライトシートアニメーションをつくる関数(createAnimation())では、スプライトシートを定め、SpriteSheet()コンストラクタに渡すオブジェクト(data)のプロパティが変わる。framesプロパティには、新たなスプライトシートの大きさを与える。フレームはただ頭から順に再生するだけなので、animationsプロパティは要らなくなる。

var animation;

function initialize() {

  animation = createAnimation("images/sparkle_21x23.png");

}

function createAnimation(file) {
  var data = {};
  data.images = [file];
  // data.frames = {width:41, height:55, regX:20, regY:27};
  data.frames = {width:21, height:23, regX:10, regY:11};
  /*
  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;
}

つぎに、アニメーション名(animationsのプロパティ)がなくなったので、再生はSprite.gotoAndPlay()でなくSprite.play()メソッドを用いる。

function createInstance(x, y, halfSpeed) {
  var instance = getClone(animation);
  // instance.gotoAndPlay("walk");
  instance.play();
  stage.addChild(instance);
}

そして、Canvasの背景色はmidnightblue(#191970)にしよう。忘れていけないのは、Canvasにモーションブラーをかけて、フェードアウトさせる関数(fadeAndBlur())だ。残像をフェードアウトさせるために塗り重ねる色は、背景色(0x191970)に合わせなければならない。

<style type="text/css">
  canvas {background-color: midnightblue;}
</style>
function fadeAndBlur() {

  // context.fillStyle = createjs.Graphics.getRGB(0xFFFFFF, 0.2);
  context.fillStyle = createjs.Graphics.getRGB(0x191970, 0.2);
  context.fillRect(0, 0, stageWidth, stageHeight);
}

前掲コード1についてこれらを書替えれば、新たなスプライトシートに差し替わる。スクリプト全体は、つぎのコード2のとおりだ。たちまち、ファンタスティックなアニメーションになった図7⁠。jsdo.itのコードも掲げておく。

図7 マウスポインタの動きに合わせてきらめく星が落ちては弾む
図7 マウスポインタの動きに合わせてきらめく星が落ちては弾む
コード2 マウスポインタの動きに合わせてきらめく星のスプライトアニメーションを落として弾ませる
var stage;
var animation;
var stageWidth;
var stageHeight;
var context;
var blurFilter = new createjs.BlurFilter(2, 1, 1);
var sprites = [];
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/sparkle_21x23.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 = getClone(animation);
  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.play();
  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);
      sprites.push(child);
    } 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:21, height:23, regX:10, regY:11};
  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(0x191970, 0.2);
  context.fillRect(0, 0, stageWidth, stageHeight);
}
function getRandom(min, max) {
  var randomNumber = Math.random() * (max - min) + min;
  return randomNumber;
}
function getClone(original) {
  var mySprite;
  if (sprites.length) {
    mySprite = sprites.pop();
    original.cloneProps(mySprite);
  } else {
    mySprite = original.clone();
  }
  return mySprite;
}

次回から、また新たなお題に取り組む。CreateJSの新バージョンに加わったメソッドを使ってみたい。今のところ、このような3次元表現を考えている。

おすすめ記事

記事・ニュース一覧