animation load

CreateJSを使って雪を降らせてみる

東京は先日40年ぶりの大雪、家から出られなかったのでCreateJSを使って当サイトトップと当ブログに雪を降らせるエフェクトを作ってみました。

snow

デモページ

基本的な作りはとても単純で、正円にボカシを入れた画像をBitmapクラスに使い、複数上から降らせています。

このエフェクトのキモはなによりも “雪っぽさ”、少し揺れながらゆっくり落ちてくるアルゴリズムをどのように作るかというところにあります。

今回の場合、雪は大きなSの字を書きながら落ちていくようにプログラムしています。
このSの字の動きのソースは以下となります。

snow.angle += snow.vangle;
snow.y += snow.vy;
snow.x = snow.base_x + snow.vx * Math.sin(snow.angle);

基本的には各雪のつぶに対して上記の処理をアニメーションのフレームごとに実行しています。

y軸の移動に関しては運動量vyを加算しているだけです。
x軸の揺れの動きは、Math.sin()を使ったサイン波を用いています。

サイン波の説明は言葉よりプログラムで見てもらうのが早いと思います。
以下のソースを実行してみてください。

for(var i = 0; i < 360; i++){
  console.log(Math.sin(i * Math.PI / 180));
}

コンソールに表示される数字が、おおよそ0から1へ増え、1から-1へ減り、-1から0に向かって増えていると思います。
(小数点第二位辺りまでの値で見るとわかりやすいかと思います。)

sin関数の引数は角度をラジアンに変換する処理です。
このプログラムで言いたいことは、0から360までの角度をsin関数に与えると0から1、1から-1へと値が波のように変化をする、ということです。

図にするとこんな感じ。

sinwave

縦軸はconsoleに表示される計算結果の数字、横軸は変数iの値です。
sin関数に与えられる引数が増えるに従い、数字が波のように変化しているのがお分かりいただけると思います。

これを使い、雪が大きくS時に触れながら落ちていく処理を実現しています。

では再度、雪の動きのソースを見てみましょう。

snow.angle += snow.vangle;
snow.y += snow.vy;
snow.x = snow.base_x + snow.vx * Math.sin(snow.angle);

snow.xに対しての処理でいくつかの変数を使っていますが、処理内容の概要は以下です。

snow.x = x座標の基準位置 + 振れ幅の最大値 * Math.sin(角度);

「x座標の基準位置」とは、雪ごとの降り出す最初のx座標です。
このx座標を基準にし、x座標の増減を繰り返します。

「振れ幅の最大値」とは、S字の並みの頂点です。30なら、x座標の基準位置からプラスマイナス30pxまでの波を描くこととなります。
この値によって揺れる幅の大きさが変わります。


最後に、以下がJavaScriptの全体のソースです。
雪の降るスピードや振れ幅などは雪の大きさをベースにしています。
雪が大きければ早く落ち、大きく振れるようにすることで、エフェクトに奥行き感が出るようになっています。

(function(){

var CJS = createjs;
var canvas;
var stage;
var snowImg;
var snows = [];
var animationCount = 0;
var isSnowCreated = false;

/*
 * common
 */
function preload(){
    var preload = new CJS.LoadQueue(false);
    preload.loadFile({id:"snow", src:"/images/snow.png"}, false);
    preload.load();
    preload.on("fileload", function(obj) {
        snowImg = obj.result;
    });
    preload.on("complete", init);
}

function init(){
    canvas = $("#demo")[0];
    stage = new CJS.Stage(canvas);
    CJS.Ticker.setFPS(60);
    CJS.Ticker.addEventListener("tick", tick);

    snowInit();
}


function tick(){
    if(isSnowCreated) snowUpdate();
    stage.update();
}



/*
 * snow effect
 */
function snowInit(){
    var max = Math.floor(stage.canvas.width / 40);
    var snows = [];
    var createFrameNum = 4;
    var createCount = 0;

    for(var i = 0, l = max; i < l; i++) {
        snowCreate();
    }
    isSnowCreated = true;
}

function snowCreate(){
    var bmp = new CJS.Bitmap(snowImg)
    var size = Math.floor(stage.canvas.width / 1000 + Math.random() * 20);
    var scale = size / snowImg.width;

    bmp.width = size;
    bmp.height = size;
    bmp.scaleX = scale;
    bmp.scaleY = scale;
    bmp.x = Math.random() * stage.canvas.width;
    bmp.y = 0 - size - Math.random() * 100;

    // 揺れながら降るアニメーションに必要な変数
    bmp.base_x = bmp.x;
    bmp.angle = 0;
    bmp.vangle = (Math.random() - Math.random()) / size / 8;

    // 着地フラグ
    bmp.isLanding = false;

    // 降るスピード
    bmp.vy = size * 0.05;
    // 横に揺れる移動料
    bmp.vx = size * 10;

    snows.push(bmp);
    stage.addChild(bmp);
}

function snowUpdate(){
    animationCount++;
    // 雪作る
    if(animationCount % 2 == 0) {
        snowCreate();
    }
    // 雪の配列で回す
    for(var i = 0; i < snows.length; i++) {
        var snow = snows[i];

        // 溶けるアニメーション
        if(snow.isLanding) {
            snow.alpha -= 0.0015;
            // remove
            if(snow.alpha <= 0) {
                snows.splice(i,1);
                stage.removeChild(snow);
                i--;
            }
            continue;
        } else {
            // 降るアニメーション
            snow.angle += snow.vangle;
            snow.y += snow.vy;
            snow.x = snow.base_x + snow.vx * Math.sin(snow.angle);
        }

        // hitTest
        if(snow.y >= stage.canvas.height - snow.width) {
            snow.isLanding = true;
        }
    }
};
preload();
})();



TAG