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

第5回トゥイーンアニメーションを仕上げる

前回から、TweenJSのデモTWEEN CIRCLESをお題にした。同心円のリングが、ステージ上のクリックした位置に、時間差でトゥイーンする。前回、一応それらしい動きはできた。けれども、Canvasのあちこちを続けざまにクリックすると、同心円にしたリングのトゥイーンがばらばらになってしまった図1⁠。今回は、アニメーションやインタラクションをよく確かめて、スクリプトの仕上げにかかる。

図1 あちこちを続けざまにクリックするとリングのトゥイーンがばらばらになる
図1 あちこちを続けざまにクリックするとリングのトゥイーンがばらばらになる
前回のjsdo.itサンプルTween Circles Trialよりキャプチャ

隊列を乱さない

ステージ上を素早くあちこちクリックすると、なぜリングのトゥイーンは隊列を乱してしまうのか。それは、先頭と後尾でトゥイーンを時間的にずらしているからだ。先頭は前のトゥイーンをすでに終えていて、すぐにつぎのトゥイーンに向かう。しかし、最後尾はまだ前のトゥイーンが終わっていないため、つぎの目的地には向かえず、今のトゥイーンを終えようとする。だから、それぞれの向かう先がばらばらになってしまうのだ。

これを避けるには、新しいトゥイーンを命じるときには、前のトゥイーンは途中であっても止めさせればいい。そのメソッドが、静的メソッドTween.removeTweens()だ。引数には対象となるインスタンスを渡す。すると、そのインスタンスに定められたすべてのトゥイーンは解除される。

Tween.removeTweens(対象オブジェクト);

前回のコード2のトゥイーンを定める関数には、つぎのようなTween.removeTweens()メソッドの呼出しを加えればよい。

function setTween(target, nX, nY, duration) {
  createjs.Tween.removeTweens(target);
  createjs.Tween.get(target)
  .to({x:nX, y:nY}, duration, createjs.Ease.bounceOut);
}

もっとも、新しいトゥイーンを命じたら、前のトゥイーンはつねに無視してよいなら、Tween.get()メソッドの第2引数でも定められる。今回のお題なら、こちらの方が簡単だ。

Tween.get(対象オブジェクト, {プロパティ:設定値, …})

第2引数はObjectインスタンスで渡す。オブジェクトには、設定したいプロパティとその値を納める。新たなトゥイーンを加えるとき前のトゥイーンはすべて除いてしまいたいときは、overrideプロパティにtrueを定めればよい(デフォルト値はfalse⁠。

function setTween(target, nX, nY, duration) {
  //createjs.Tween.get(target)
  createjs.Tween.get(target, {override:true})
  .to({x:nX, y:nY}, duration, createjs.Ease.bounceOut);
}

これで、ステージを素早く続けざまにクリックしても、古いトゥイーンはすべてのリングから除かれるため、全体が新たな目的地に向かってトゥイーンを始める。もはや隊列の乱れはない図2⁠。まだ修正はわずかに引数ひとつではあるが、前回までのおさらいもかねて、つぎのコード1にscript要素全体を掲げる。

図2 ステージを素早く続けざまにクリックしてもリングの隊列は乱れない
図2 ステージを素早く続けざまにクリックしてもリングの隊列は乱れない
コード1 ステージ上のクリックした位置に複数のオブジェクトが時間差をつけてトゥイーンする
<script src="http://code.createjs.com/easeljs-0.6.0.min.js"></script>
<script src="http://code.createjs.com/tweenjs-0.4.0.min.js"></script>
<script>
var stage;
var circles = [];
var circleCount = 20;
var delay = 1 / circleCount;
function initialize() {
  var canvasElement = document.getElementById("myCanvas");
  stage = new createjs.Stage(canvasElement);
  var nWidth = canvasElement.width;
  var nHeight = canvasElement.height;
  for (var i = 0; i < circleCount; i++) {
    var nX = Math.random() * nWidth;
    var nY = Math.random() * nHeight;
    var circle = createCircle(10, "#113355", (i + 1) * 3);
    setAppearance(circle, nX, nY, 1 - i * 0.025, "lighter");
    setTween(circle, nWidth / 2, nHeight / 2, (0.5 + i * delay) * 1500);
    circles.push(circle);
    stage.addChild(circle);
  }
  stage.addEventListener("stagemouseup", startTween);
  createjs.Ticker.addEventListener("tick", stage);
}
function createCircle(stroke, color, radius) {
  var circle = new createjs.Shape();
  var myGraphics = circle.graphics;
  myGraphics.setStrokeStyle(stroke);
  myGraphics.beginStroke(color);
  myGraphics.drawCircle(0, 0, radius);
  return circle;
}
function setAppearance(instance, nX, nY, nAlpha, composite) {
  instance.x = nX;
  instance.y = nY;
  instance.alpha = nAlpha;
  instance.compositeOperation = composite;
}
function startTween(eventObject) {
  for (var i = 0; i < circleCount; i++) {
    var circle = circles[i];
    setTween(circle, stage.mouseX, stage.mouseY, (0.5 + i * delay) * 1500);
  }
}
function setTween(target, nX, nY, duration) {
  createjs.Tween.get(target, {override:true})
  .to({x:nX, y:nY}, duration, createjs.Ease.bounceOut);
}
</script>

無駄遣いはしない

動きとしては、これでよさそうだ。しかし、仕上げに当たっては、他に手をいれるべきことはないか考えたい。参考にしたTweenJSのデモTWEEN CIRCLESのコードを見ると、つぎのような処理が加わっている。何をしているのかというと、無駄な画面の描き替えをなくそうというのだ。

前掲コード1では、Ticker.addEventListener()メソッドでTicker.tickイベントのリスナーにStageオブジェクトを加えた。そうすれば、Stage.update()メソッドを呼出すだけのリスナー関数は書かなくて済む。ただし、誰もクリックせず、アニメーションがなくても、画面は描き続けられる。それは無駄だろうという発想には一理ある。

var circleCount = 20;
var activeCount;

function initialize() {
  // ...[中略]...
  for (var i = 0; i < circleCount; i++) {
    // ...[中略]...
    var circle = createCircle(10, "#113355", (i + 1) * 3);
    setAppearance(circle, nX, nY, 1 - i * 0.025, "lighter");
    setTween(circle, nWidth / 2, nHeight / 2, (0.5 + i * delay) * 1500);
    // ...[中略]...
  }
  activeCount = circleCount;
  stage.addEventListener("stagemouseup", startTween);
  // createjs.Ticker.addEventListener("tick", stage);
  createjs.Ticker.addEventListener("tick", tick);
}

function startTween(eventObject) {
  for (var i = 0; i < circleCount; i++) {
    var circle = circles[i];
    setTween(circle, stage.mouseX, stage.mouseY, (0.5 + i * delay) * 1500);
  }
  activeCount = circleCount;
}
function setTween(target, nX, nY, duration) {
  createjs.Tween.get(target, {override:true})
  .to({x:nX, y:nY}, duration, createjs.Ease.bounceOut)  // ;
  .call(tweenComplete);
}
function tweenComplete(eventObject) {
  activeCount--;
}
function tick() {
  if (activeCount) {
    stage.update();
  }
}

この処理を加えたJavaScriptコード全体は、後にコード2として掲げた。まず、Ticker.addEventListener()メソッドで加えるTicker.tickイベントのリスナーには、Stageオブジェクトに替えて新たな関数(tick())を定めた。そして、初期化の関数(initialize())とマウスクリックのリスナー関数(startTween())がリングのトゥイーンをforループで定めたすぐ後、新たに加えた変数(activeCount)にリングの総数(circleCount)を与えている。

つぎに、トゥイーンを定める関数(setTween())には、Tween.call()メソッドの呼出しが加わった、引数に渡した関数(tweenComplete())は、リングのトゥイーンがひとつ終わるたびに呼出される。そして、その関数は、新たに定めた変数(activeCount)の値をカウントダウン(デクリメント--していく。つまり、この変数は、まだトゥイーンが続いているリングの個数を示す。

そこで、Ticker.tickイベントのリスナー関数(tick())は、if条件でまだトゥイーンしているリングの残り個数(activeCount)があるときだけStage.update()メソッドを呼出す。これで、無駄な画面の書き替えはなくなりそうだ。

コード2 トゥイーンしているリングがあるときだけ画面を描き替える
var stage;
var circles = [];
var circleCount = 20;
var activeCount;
var delay = 1 / circleCount;
function initialize() {
  var canvasElement = document.getElementById("myCanvas");
  stage = new createjs.Stage(canvasElement);
  var nWidth = canvasElement.width;
  var nHeight = canvasElement.height;
  for (var i = 0; i < circleCount; i++) {
    // var nX = Math.random() * nWidth * 50;
    var nX = Math.random() * nWidth;
    var nY = Math.random() * nHeight;
    var circle = createCircle(10, "#113355", (i + 1) * 3);
    setAppearance(circle, nX, nY, 1 - i * 0.025, "lighter");
    setTween(circle, nWidth / 2, nHeight / 2, (0.5 + i * delay) * 1500);
    circles.push(circle);
    stage.addChild(circle);
  }
  activeCount = circleCount;
  stage.addEventListener("stagemouseup", startTween);
  createjs.Ticker.addEventListener("tick", tick);
}
function createCircle(stroke, color, radius) {
  var circle = new createjs.Shape();
  var myGraphics = circle.graphics;
  myGraphics.setStrokeStyle(stroke);
  myGraphics.beginStroke(color);
  myGraphics.drawCircle(0, 0, radius);
  return circle;
}
function setAppearance(instance, nX, nY, nAlpha, composite) {
  instance.x = nX;
  instance.y = nY;
  instance.alpha = nAlpha;
  instance.compositeOperation = composite;
}
function startTween(eventObject) {
  for (var i = 0; i < circleCount; i++) {
    var circle = circles[i];
    setTween(circle, stage.mouseX, stage.mouseY, (0.5 + i * delay) * 1500);
  }
  activeCount = circleCount;
}
function setTween(target, nX, nY, duration) {
  createjs.Tween.get(target, {override:true})
  .to({x:nX, y:nY}, duration, createjs.Ease.bounceOut)  // ;
  .call(tweenComplete);
}
function tweenComplete(eventObject) {
  activeCount--;
}
function tick() {
  if (activeCount) {
    stage.update();
  }
}

このトゥイーンアニメーションを何度か試しながらよく見てみると、気になるところがある。初め(initialize())のランダムな位置から中央へのトゥイーンで、つぎのようにランダムな座標(nX)の幅を極端に拡げてみるとはっきりする。トゥイーンする最後のリングが、目的の中心までたどり着かずに力つきてしまう図3⁠。

function initialize() {
  // ...[中略]...
  for (var i = 0; i < circleCount; i++) {
    // var nX = Math.random() * nWidth;
    var nX = Math.random() * nWidth * 50;
    // ...[中略]...
  }
  // ...[中略]...
}
図3 力つきた最後のリング
図3 力つきた最後のリング

帰るまでが遠足

前掲コード2で最後のリングが目的地にたどりつけないのは、処理の詰めに問題がある。家に帰るまでが遠足なのだ。すべてのリングのトゥイーンを終えて、残り個数がなくなったら画面は書替えないという考え方はよい。しかし0になったとき、最後にたどり着いたリングは描画すべきだ。それをしていないために、最後のリングが卒業写真に欠席した子のようになってしまった。

function tick() {
  if (activeCount) {   // 0になったら処理しない
    stage.update();
  }
}

最後にゴールする子まできちんと見届けようとするなら、つぎのような手を加えればよいだろう。残り個数が0になったときは、1度画面は描き替えてカウンタの変数(activeCount)を減らしている。そして、変数がマイナスになったら、もはや再描画はしない。

function tick() {
  // if (activeCount) {
  if (activeCount > 0) {
    stage.update();
  } else if (activeCount > -1) {
    stage.update();
    activeCount--;
  }
}

ただこの処理は、後からつけ足した感が否めない。それに、画面の描き替えは省けても、Ticker.tickイベントは続く。イベントリスナーを除いてしまえば、イベントそのものが止められる。つぎのように、スクリプトももう少しすっきりする。

function startTween(eventObject) {
  for (var i = 0; i < circleCount; i++) {
    var circle = circles[i];
    setTween(circle, stage.mouseX, stage.mouseY, (0.5 + i * delay) * 1500);
  }
  activeCount = circleCount;
  createjs.Ticker.addEventListener("tick", tick);   // 追加
}
function tick() {
  if (activeCount > 0) {
    stage.update();
  } else {  // if (activeCount > -1) {
    stage.update();
    // activeCount--;
    createjs.Ticker.removeEventListener("tick", tick);
  }
}

さらに見直せば、まだ手を入れる余地はあるだろう。だが、このお題については、style要素につぎのようなCanvasの背景色を加えて結びとしたい図4⁠。上述とおりにイベントリスナーの削除を加えたJavaScriptコード全体は、つぎにコード3として掲げる。あわせて、jsdo.itにコードに公開したコードも添える。次回は、また新たなお題に取り組もう。

<style type="text/css">
canvas {
  background-color: #333333;
}
</style>
図4 Canvasに暗いグレーの背景色を加えた
図4 Canvasに暗いグレーの背景色を加えた
コード3 リングがトゥイーンを終えたらイベントリスナーは除く
var stage;
var circles = [];
var circleCount = 20;
var activeCount;
var delay = 1 / circleCount;
function initialize() {
  var canvasElement = document.getElementById("myCanvas");
  stage = new createjs.Stage(canvasElement);
  var nWidth = canvasElement.width;
  var nHeight = canvasElement.height;
  for (var i = 0; i < circleCount; i++) {
    // var nX = Math.random() * nWidth * 50;
    var nX = Math.random() * nWidth;
    var nY = Math.random() * nHeight;
    var circle = createCircle(10, "#113355", (i + 1) * 3);
    setAppearance(circle, nX, nY, 1 - i * 0.025, "lighter");
    setTween(circle, nWidth / 2, nHeight / 2, (0.5 + i * delay) * 1500);
    circles.push(circle);
    stage.addChild(circle);
  }
  activeCount = circleCount;
  stage.addEventListener("stagemouseup", startTween);
  createjs.Ticker.addEventListener("tick", tick);
}
function createCircle(stroke, color, radius) {
  var circle = new createjs.Shape();
  var myGraphics = circle.graphics;
  myGraphics.setStrokeStyle(stroke);
  myGraphics.beginStroke(color);
  myGraphics.drawCircle(0, 0, radius);
  return circle;
}
function setAppearance(instance, nX, nY, nAlpha, composite) {
  instance.x = nX;
  instance.y = nY;
  instance.alpha = nAlpha;
  instance.compositeOperation = composite;
}
function startTween(eventObject) {
  for (var i = 0; i < circleCount; i++) {
    var circle = circles[i];
    setTween(circle, stage.mouseX, stage.mouseY, (0.5 + i * delay) * 1500);
  }
  activeCount = circleCount;
  createjs.Ticker.addEventListener("tick", tick);
}
function setTween(target, nX, nY, duration) {
  createjs.Tween.get(target, {override:true})
  .to({x:nX, y:nY}, duration, createjs.Ease.bounceOut)
  .call(tweenComplete);
}
function tweenComplete(eventObject) {
  activeCount--;
}
function tick() {
  if (activeCount > 0) {
    stage.update();
  } else {
    stage.update();
    createjs.Ticker.removeEventListener("tick", tick);
  }
}

おすすめ記事

記事・ニュース一覧