サイトトップ

Director Flash 書籍 業務内容 プロフィール

HTML5テクニカルノート

EaselJS 0.8.0: カラフルに光る円のアニメーション

ID: FN1501001 Technique: HTML5 and JavaScript Library: EaselJS 0.8.0

さまざまな色と大きさの円を、それぞれの向きに動かします(サンプル001)。速さは大きさと比例させました。フィルタで縁をぼかし、重なった色を加算することにより、光のような表現にしています。jsdo.it「EaselJSでキラキラエフェクト」を参考に、CreateJSの最新版(EaselJS 0.8.0)に対応させるとともに、スクリプトの構成をわかりやすく書き改めました。また、簡単な最適化の手法も加えています。

サンプル001■EaselJS 0.8.0: Glowing circles with blur

01 ランダムな色と大きさの円をステージに置く

まずは、ランダムな色と大きさの円を、予め決めた数だけステージに置きましょう。アニメーションは後回しです。Shapeインスタンスをつくって返す関数(createCircle())は、つぎのようなかたちで定めます。引数には、xy座標と色、および半径を渡します。なお、以下に抜書きするスクリプトの行番号は、後にまとめたコード001にもとづきます。

createCircle(x座標, y座標, 色, 半径)
  1. function createCircle(x, y, color, radius) {
  2.   var myShape = new createjs.Shape();
  3.   var graphics = myShape.graphics;
  4.   graphics.beginFill(color);
  5.   graphics.drawCircle(0, 0, radius);
  6.   myShape.x = x;
  7.   myShape.y = y;
  8.   myShape.radius = radius;
  9.   return myShape;
  10. }

円のインスタンスをつくる関数(createCircle())は、つぎのように初期設定の関数(initialize())からfor文で、予め変数(count)に定めた数だけ呼出します(第1行目および第10行目〜)。つくる円の色(randomColor)とxy座標(xとy)、および半径(radius)は、ランダムに定めました(第11〜14行目)。

  1. var count = 30;
  2. var stage;
  3. var stageWidth;
  4. var stageHeight;
  5. function initialize(){
  6.   var canvas = document.getElementById("myCanvas");
  7.   stage = new createjs.Stage(canvas);
  8.   stageWidth = canvas.width;
  9.   stageHeight =canvas.height;
  10.   for(var i = 0; i < count; i++){
  11.     var randomColor = createjs.Graphics.getRGB(Math.floor(Math.random() * 0xFFFFFF));
  12.     var x = Math.random() * stageWidth;
  13.     var y = Math.random() * stageHeight;
  14.     var radius = Math.random() * (50 - 5) + 5;
  15.     var myShape = createCircle(x, y, randomColor, radius);
  16.     stage.addChild(myShape);
  17.   }
  18.   stage.update();
  19. }
  1. window.onload = initialize;

これてで、さまざまな色と大きさのShapeインスタンスがつくられて、ランダムな位置に置かれます。なお、Canvasの背景色は黒に定めました(図001)。ここまで書いたJavaScriptコードは、以下のコード001にまとめたとおりです。

図001■さまざまな色と大きさの円がランダムな位置に描かれた
図001

コード001■さまざまな色と大きさでつくった円のインスタンスをステージのランダムな位置に置く
  1. var count = 30;
  2. var stage;
  3. var stageWidth;
  4. var stageHeight;
  5. function initialize(){
  6.   var canvas = document.getElementById("myCanvas");
  7.   stage = new createjs.Stage(canvas);
  8.   stageWidth = canvas.width;
  9.   stageHeight =canvas.height;
  10.   for(var i = 0; i < count; i++){
  11.     var randomColor = createjs.Graphics.getRGB(Math.floor(Math.random() * 0xFFFFFF));
  12.     var x = Math.random() * stageWidth;
  13.     var y = Math.random() * stageHeight;
  14.     var radius = Math.random() * (50 - 5) + 5;
  15.     var myShape = createCircle(x, y, randomColor, radius);
  16.     stage.addChild(myShape);
  17.   }
  18.   stage.update();
  19. }
  20. function createCircle(x, y, color, radius) {
  21.   var myShape = new createjs.Shape();
  22.   var graphics = myShape.graphics;
  23.   graphics.beginFill(color);
  24.   graphics.drawCircle(0, 0, radius);
  25.   myShape.x = x;
  26.   myShape.y = y;
  27.   myShape.radius = radius;
  28.   return myShape;
  29. }
  30. window.onload = initialize;

02 色の効果とアニメーションを加える

では、つぎにアニメーションを加えます(後掲コード002)。円の動く速さは半径(radius)に比例させ、角度(angle)をランダムに定めます(第13および第17行目)。半径と角度からxy座標の成分を求める式はつぎのとおりです(「sinとcosは何する関数?」02「理解すれば覚えることは減る」)。

x座標 = 半径×cos(角度)
y座標 = 半径×sin(角度)

速さのxy成分値はPointオブジェクト(velocity)に納めて、円をつくる関数(createCircle())に引数として渡します(第17〜19行目)。そして、関数はその引数を、Shapeインスタンス(myShape)のプロパティ(velocity)に加えました(第26および第34行目)。そして、Ticker.tickイベントにリスナー関数(tick())を定めています。

  1. function initialize(){
  1.   for(var i = 0; i < count; i++){
  1.     var angle = Math.random() * Math.PI * 2;
  1.     var speed = radius * 0.02;
  2.     var velocity = new createjs.Point(speed * Math.cos(angle), speed * Math.sin(angle));
        // var myShape = createCircle(x, y, randomColor, radius);
  3.     var myShape = createCircle(x, y, randomColor, radius, velocity);
  1.   }
  2.   createjs.Ticker.timingMode = createjs.Ticker.RAF;
  3.   createjs.Ticker.addEventListener("tick", tick);
  1. }
    // function createCircle(x, y, color, radius) {
  2. function createCircle(x, y, color, radius, velocity) {
  1.   myShape.velocity = velocity;
  1. }

Ticker.tickイベントのリスナー関数(tick())は、Container.numChildrenプロパティでStageオブジェクトに加えた子インスタンスの数を調べ、forループでContainer.getChildAt()メソッドにより子インスタンスを順に取出します(第39〜41行目)。あとは、インスタンスに定めた速度のプロパティ(velocity)から、xy座標値を加えればよいでしょう(第44および第46〜47行目)。

ただし、インスタンスがステージの外に出たら、反対の端に戻しました。そのとき、円のインスタンスの基準点は中心に置いていますので、半径(radius)の大きさだけ位置を調整しています(第48〜59行目)。つまり、ステージの外側に完全に消えたら、反対の端の隠れた位置から表れるようにしました。

  1. function tick(){
  2.   var numChildren = stage.getNumChildren();
  3.   for(var i = 0; i < numChildren; i++){
  4.     var circle = stage.getChildAt(i);
  5.     var x = circle.x;
  6.     var y = circle.y;
  7.     var velocity = circle.velocity;
  8.     var radius = circle.radius;
  9.     x += velocity.x;
  10.     y += velocity.y;
  11.     if (x > stageWidth + radius){
  12.       x = -radius;
  13.     } else if (x < -radius){
  14.       x = stageWidth + radius;
  15.     }
  16.     if (y > stageHeight + radius){
  17.       y = -radius;
  18.     } else if (y < -radius){
  19.       y = stageHeight + radius;
  20.     }
  21.     circle.x = x;
  22.     circle.y = y;
  23.   }
  24.   stage.update();
  25. }

そして、重ねたオブジェクトのピクセルごとの描画の仕方を決めるのがDisplayObject.compositeOperationプロパティです(第10行目)。値を"lighter"に定めると、ピクセルのカラー値を加算しますので、描画するオブジェクトが重なるほど明るくなります(「時間差をつけたトゥイーン」の「DisplayObject.compositeOperationプロパティでイメージのピクセルを合成する」参照)。

  1. function initialize(){
  1.   stage.compositeOperation = "lighter";
  1. }

これで、インスタンスが半径と比例した速さで、ランダムな向きにアニメーションします。また、重なったピクセルのカラー値は加算されて、光って見えるようになりました(図002)。ここまでのJavaScriptコードをまとめたのが、コード002です。

図002■重なりの光る円がアニメーションする
図002

コード002■円のインスタンスに光る効果を与えてランダムな向きにアニメーションさせる
  1. var count = 30;
  2. var stage;
  3. var stageWidth;
  4. var stageHeight;
  5. function initialize(){
  6.   var canvas = document.getElementById("myCanvas");
  7.   stage = new createjs.Stage(canvas);
  8.   stageWidth = canvas.width;
  9.   stageHeight =canvas.height;
  10.   stage.compositeOperation = "lighter";
  11.   for(var i = 0; i < count; i++){
  12.     var randomColor = createjs.Graphics.getRGB(Math.floor(Math.random() * 0xFFFFFF));
  13.     var angle = Math.random() * Math.PI * 2;
  14.     var x = Math.random() * stageWidth;
  15.     var y = Math.random() * stageHeight;
  16.     var radius = Math.random() * (50 - 5) + 5;
  17.     var speed = radius * 0.02;
  18.     var velocity = new createjs.Point(speed * Math.cos(angle), speed * Math.sin(angle));
  19.     var myShape = createCircle(x, y, randomColor, radius, velocity);
  20.     stage.addChild(myShape);
  21.   }
  22.   createjs.Ticker.timingMode = createjs.Ticker.RAF;
  23.   createjs.Ticker.addEventListener("tick", tick);
  24.   stage.update();
  25. }
  26. function createCircle(x, y, color, radius, velocity) {
  27.   var myShape = new createjs.Shape();
  28.   var graphics = myShape.graphics;
  29.   graphics.beginFill(color);
  30.   graphics.drawCircle(0, 0, radius);
  31.   myShape.x = x;
  32.   myShape.y = y;
  33.   myShape.radius = radius;
  34.   myShape.velocity = velocity;
  35.   return myShape;
  36. }
  37. function tick(){
  38.   var numChildren = stage.getNumChildren();
  39.   for(var i = 0; i < numChildren; i++){
  40.     var circle = stage.getChildAt(i);
  41.     var x = circle.x;
  42.     var y = circle.y;
  43.     var velocity = circle.velocity;
  44.     var radius = circle.radius;
  45.     x += velocity.x;
  46.     y += velocity.y;
  47.     if (x > stageWidth + radius){
  48.       x = -radius;
  49.     } else if (x < -radius){
  50.       x = stageWidth + radius;
  51.     }
  52.     if (y > stageHeight + radius){
  53.       y = -radius;
  54.     } else if (y < -radius){
  55.       y = stageHeight + radius;
  56.     }
  57.     circle.x = x;
  58.     circle.y = y;
  59.   }
  60.   stage.update();
  61. }
  62. window.onload = initialize;

03 アニメーションするインスタンスにぼかしを加える

円のインスタンスには、BlurFilterオブジェクトでぼかしを加えます。一般に、オブジェクトにフィルタをかけるには、つぎの3つの操作を行います。

  1. フィルタのインスタンスをつくる。
  2. フィルタオブジェクトを配列に入れてDisplayObject.filtersプロパティに定める。
  3. フィルタをDisplayObject.cache()メソッドで描画する。

第1に、フィルタのインスタンスをつくります。BlurFilter()コンストラクタには3つの引数が渡せます。初めのふたつは,それぞれ水平と垂直のぼかし幅で、単位はピクセルです。3つ目は、ぼかしのきめ細かさを1から3の整数を目安として与えます(デフォルト値は1)。この整数は、内部的にぼかしを重ねる回数になるので、増やせば負荷が上がります。なお、水平および垂直ぼかしの幅は、BlurFilter.blurXBlurFilter.blurYプロパティで後から定めることもできます。

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

手順の第2は、フィルタオブジェクトを配列に入れて、DisplayObject.filtersプロパティに与えます。配列に加えるのは、フィルタを複数かけられるように考えられた仕組みです。

そして第3に、設定されたフィルタをDisplayObject.cache()メソッドで描画しなければなりません。内部的には、Canvasが新たにつくられ、そこでフィルタの演算と描画を行った後、ステージにその結果が加えられます。引数は矩形領域を示す4つの座標値で、その範囲にフィルタのかかったイメージが描かれます[*1]

DisplayObjectオブジェクト.cache(左, 上, 幅, 高さ)

インスタンスにBlurFilterのフィルタを加える関数(setBlur())は、つぎのように定めました。ぼかし幅は円の半径に比例させます。そして、初期設定の関数(initialize())で円のインスタンスをつくったすぐ後に呼出しました。でき上がったJavaScriptコードは、後にコード003としてまとめています。

setBlur(インスタンス, BlurFilterオブジェクト, ぼかし幅, Rectangleオブジェクト)

関数(setBlur())は、ぼかし幅を引数値(blur)、フィルタがかかる領域は円の半径(radius)から求め、引数のBlurFilterオブジェクト(blurFilter)とRectangleオブジェクト(rect)のプロパティに定めたうえで、インスタンス(myShape)をぼかします(第41〜48行目)。

  1. function initialize(){
  1.   var blurFilter = new createjs.BlurFilter(0, 0, 2);
  2.   var rect = new createjs.Rectangle();
  1.   for(var i = 0; i < count; i++){
  1.     var blur = 1 + radius / 2;
  1.     setBlur(myShape, blurFilter, blur, rect);
  1.   }
  1. function setBlur(myShape, blurFilter, blur, rect) {
  2.   var radius = myShape.radius;
  3.   blurFilter.blurX = blurFilter.blurY = blur;
  4.   myShape.filters = [blurFilter];
  5.   rect.x = rect.y = -radius;
  6.   rect.width = rect.height = radius * 2;
  7.   myShape.cache(rect.x, rect.y, rect.width, rect.height);
  8. }

これでお題はでき上がりました。色とりどりのさまざまな円が、ぼんやりとした光のように交錯します。初めに掲げたサンプル001のJavaScriptコードが以下のコード003です。

図003■光る円が大きさに応じてぼける
図003

コード003■アニメーションする円のオブジェクトを半径に比例させてぼかす
  1. var count = 30;
  2. var stage;
  3. var stageWidth;
  4. var stageHeight;
  5. function initialize(){
  6.   var canvas = document.getElementById("myCanvas");
  7.   var blurFilter = new createjs.BlurFilter(0, 0, 2);
  8.   var rect = new createjs.Rectangle();
  9.   stage = new createjs.Stage(canvas);
  10.   stageWidth = canvas.width;
  11.   stageHeight =canvas.height;
  12.   stage.compositeOperation = "lighter";
  13.   for(var i = 0; i < count; i++){
  14.     var randomColor = createjs.Graphics.getRGB(Math.floor(Math.random() * 0xFFFFFF));
  15.     var angle = Math.random() * Math.PI * 2;
  16.     var x = Math.random() * stageWidth;
  17.     var y = Math.random() * stageHeight;
  18.     var radius = Math.random() * (50 - 5) + 5;
  19.     var speed = radius * 0.02;
  20.     var velocity = new createjs.Point(speed * Math.cos(angle), speed * Math.sin(angle));
  21.     var blur = 1 + radius / 2;
  22.     var myShape = createCircle(x, y, randomColor, radius, velocity);
  23.     setBlur(myShape, blurFilter, blur, rect);
  24.     stage.addChild(myShape);
  25.   }
  26.   createjs.Ticker.timingMode = createjs.Ticker.RAF;
  27.   createjs.Ticker.addEventListener("tick", tick);
  28.   stage.update();
  29. }
  30. function createCircle(x, y, color, radius, velocity) {
  31.   var myShape = new createjs.Shape();
  32.   var graphics = myShape.graphics;
  33.   graphics.beginFill(color);
  34.   graphics.drawCircle(0, 0, radius);
  35.   myShape.x = x;
  36.   myShape.y = y;
  37.   myShape.radius = radius;
  38.   myShape.velocity = velocity;
  39.   return myShape;
  40. }
  41. function setBlur(myShape, blurFilter, blur, rect) {
  42.   var radius = myShape.radius;
  43.   blurFilter.blurX = blurFilter.blurY = blur;
  44.   myShape.filters = [blurFilter];
  45.   rect.x = rect.y = -radius;
  46.   rect.width = rect.height = radius * 2;
  47.   myShape.cache(rect.x, rect.y, rect.width, rect.height);
  48. }
  49. function tick(){
  50.   var numChildren = stage.getNumChildren();
  51.   for(var i = 0; i < numChildren; i++){
  52.     var circle = stage.getChildAt(i);
  53.     var x = circle.x;
  54.     var y = circle.y;
  55.     var velocity = circle.velocity;
  56.     var radius = circle.radius;
  57.     x += velocity.x;
  58.     y += velocity.y;
  59.     if (x > stageWidth + radius){
  60.       x = -radius;
  61.     } else if (x < -radius){
  62.       x = stageWidth+radius;
  63.     }
  64.     if (y > stageHeight + radius){
  65.       y = -radius;
  66.     } else if (y < -radius){
  67.       y = stageHeight + radius;
  68.     }
  69.     circle.x = x;
  70.     circle.y = y;
  71.   }
  72.   stage.update();
  73. }
  74. window.onload = initialize;

[*1] ぼかしフィルタをかけると、もとのイメージより領域が広がります。けれど、DisplayObject.cache()メソッドの引数に渡す矩形座標は、もとのイメージの値で構いません。フィルタをかけて変わる矩形座標は、内部的に調べられて値が調整されるからです。

フィルタによって変わる矩形座標は、Filter.getBounds()メソッドによりRectangleオブジェクトのかたちで得られます。DisplayObject.cache()メソッドの実装は、内部的にDisplayObject.updateCache()メソッドを用いており、その中からプライベートなメソッド_getFilterBounds()が呼出されます。そして、このメソッドは、インスタンスにかけられたすべてのフィルタについてFilter.getBounds()メソッドにより矩形座標を調べるのです。

なお、EaselJS 0.8.0から、Filter.getBounds()メソッドに引数(オプション)として、もとのイメージの矩形座標がRectangleオブジェクトで渡せるようになりました。


04 最適化のヒント

前掲コード003は、最適化にも少し気をつかいました。もっとも、オブジェクトの数も処理も少ないので、目に見えて変わるところはありません。大量の処理を行うときのヒントと考えてくださればよいでしょう。ふたつ採上げます。

ひとつ目は、つぎのオブジェクトを動かすための演算です。プロパティ値を、予めローカル変数に納めています(第53〜56行目)。1行1行のステートメントは短くなったものの、行数そのものは増えてしまいました。

  1. var x = circle.x;
  2. var y = circle.y;
  3. var velocity = circle.velocity;
  4. var radius = circle.radius;
  5. x += velocity.x;
  6. y += velocity.y;
  7. if (x > stageWidth + radius){
  8.   x = -radius;
  9. } else if (x < -radius){
  10.   x = stageWidth+radius;
  11. }
  12. if (y > stageHeight + radius){
  13.   y = -radius;
  14. } else if (y < -radius){
  15.   y = stageHeight + radius;
  16. }
  17. circle.x = x;
  18. circle.y = y;

つぎのようにローカル変数を使わなくても、アニメーションに変わりはありません。そして、行数は2/3になります。けれど、オブジェクトからプロパティを取出すより、ローカル変数の方が速く参照できます。とくに、多くのオブジェクトをループ処理で扱う場合には、何度か参照する値はローカル変数に入れた方がお得です。

circle.x += circle.velocity.x;
circle.y += circle.velocity.y;
if (circle.x > stageWidth + circle.radius){
  circle.x = -circle.radius;
} else if (circle.x < -circle.radius){
  circle.x = stageWidth+circle.radius;
}
if (circle.y > stageHeight + circle.radius){
  circle.y = -circle.radius;
} else if (circle.y < -circle.radius){
  circle.y = stageHeight + circle.radius;
}

ふたつ目は、インスタンスをぼかす関数(setBlur())に渡した引数です。前掲コード003では、BlurFilterとRectangleオブジェクトを引数に含め、それらは関数を呼出すforループの外でつくりました(前掲コード003第7〜8行目)。けれど、以下のように関数の中でオブジェクトをつくった方が、引数もふたつに減ってすっきりするように感じられます(第43および第46行目)。

けれど、オブジェクトを新たにつくるより、すでにあるものを使い回す方が、わずかに負荷が減ります。それより大切なのは、BlurFilterとRectangleオブジェクトはつくって一度使うだけの使い捨てだということです。これらのオブジェクトをたくさんつくり続けると、用済みのオブジェクトがメモリに溜まります。それらを自動的にかたづけるJavaScriptの手間が増えてしまうのです。さらに詳しくは、「オブジェクトの使い回しとアニメーション素材の変更」の「ガベージコレクションを減らす」をお読みください。

  1. function initialize(){
  // var blurFilter = new createjs.BlurFilter(0, 0, 2);
  // var rect = new createjs.Rectangle();
  1.   for(var i = 0; i < count; i++){
        // setBlur(myShape, blurFilter, blur, rect);
  1.     setBlur(myShape, blur);
  1.   }
  1. }
    // function setBlur(myShape, blurFilter, blur, rect) {
  1. function setBlur(myShape, blur) {
      // blurFilter.blurX = blurFilter.blurY = blur;
  1.   var blurFilter = new createjs.BlurFilter(blur, blur, 2);
      // rect.x = rect.y = -radius;
      // rect.width = rect.height = radius * 2;
  1.   var rect = new createjs.Rectangle(-radius, -radius, radius * 2, radius * 2);
  1. }

作成者: 野中文雄
更新日: 2015年1月8日 注[*1]を加え、それにともない本文とコード003を修正。
作成日: 2015年1月7日


Copyright © 2001-2015 Fumio Nonaka.  All rights reserved.