さぁ!検索しよう!

JavaScriptでcanvas上に雪を降らせる方法です。

DEMO

HTMLを書く

始めに以下のHTMLを書きます。

<canvas id="canvas"></canvas>

HTMLではcanvas要素のみを定義しています。

CSSを書く

次に以下のCSSを書きます。

html,
body {
  margin: 0;
  overflow: hidden;
}

#canvas {
  background: #08233E;
}

htmlbodyにはoverflow:hidden;を指定してスクロールバーを隠し、#canvasにはbackground: #08233E;を指定して夜空を表現します。

JavaScriptを書く 

最後にJavaScriptを以下の順で書きます。

変数・配列を定義

始めに以下の変数・配列・を定義します。

//requestAnimationFrameのベンダープレフィクス
const requestAnimatoinFrame = window.requestAnimationFrame || 
      window.mozRequestAnimationFrame || 
      window.webkitRequestAnimationFrame || 
      window.msRequestAnimationFrame || 
      window.oRequestAnimationFrame || 
      function(callback) {
          window.setTimeout(callback, 1000 / 60);
      };

//ウィンドウサイズ
let width = window.innerWidth;
let height = window.innerHeight;

//雪が降るスピード
//レイヤー毎にスピードを変える
let speed1 = null;
let speed2 = null;
let speed3 = null;

//雪片のサイズ
let size1 = Math.min(width, height) / 380;
let size2 = Math.min(width, height) / 200;
let size3 = Math.min(width, height) / 100;

//雪片の座標
let x = null;
let y = null;

//雪の集合を含むレイヤーとなる配列
const snows1 = [];//1層目のレイヤー(一番奥)
const snows2 = [];//2層目のレイヤー(真ん中)
const snows3 = [];//3層目のレイヤー(先頭)

雪はアニメーションで降らすためrequestAnimationFrameを使用しますが、クロスブラウザにするためrequestAnimationFrameにベンダープレフィクスを付与します。

スピードと座標の変数はインスタンス化の際に値を代入するためここではnullとしています。

snows1~snows3の配列は雪片の集合を含むレイヤーとなり、このレイヤー毎に雪片のサイズ・スピードを変えることでパララックス効果による奥行感を生み出すことができます。

getRandomInt関数を定義

minからmaxまでのランダムな値を返すgetRandomInt関数を定義します。

//min~maxのランダムな値を返す関数
function getRandomInt(min, max) {
    return (Math.random() * (max - min + 1)) + min;
}

この関数は雪片のサイズや座標を取得する際に頻繁に使われるユーティリティ関数です。

Canvasクラスを作成・インスタンス化

canvas要素に関するCanvasクラスを作成・インスタンス化します。

//canvasに関するクラス
class Canvas {

    constructor() {

        //#canvasの参照
        this.elem = document.getElementById('canvas');

        //canvasのコンテキストを取得
        this.ctx = this.elem.getContext('2d');

        //ウィンドウサイズをcanvasのサイズとする
        this.elem.width = width;
        this.elem.height = height;

        //ウィンドウがリサイズされるとresizeメソッドが実行される
        window.addEventListener('resize', () => this.resize());
    }

    //canvasをリサイズ
    resize() {

        //canvasのサイズを更新
        this.elem.width = width = window.innerWidth;
        this.elem.height = height = window.innerHeight;
    }
}

//canvasオブジェクトを生成
const canvas = new Canvas;

画面いっぱいに雪を降らせたいのでcanvas要素のサイズをウィンドウのサイズとします。

また、resizeメソッドでウィンドウのリサイズに合わせて自動的にcanvasのサイズが変更されるようにします。

Snowflakeクラスを作成

雪片に関するSnowflakeクラスを作成します。

//雪片に関するクラス
class Snowflake {

    constructor(x, y, size, speed){

        //雪片の座標
        this.x = x;
        this.y = y;

        //雪片のサイズ
        this.size = size;

        //雪片のスピード
        this.speed = speed;

        //雪片の振り幅
        this.swingWidth = 0.3;
    }

    //雪片を描画するメソッド
    draw(){
        const ctx = canvas.ctx;

        //パスを初期化
        ctx.beginPath();

        //円グラデーションを指定
        //http://www.htmq.com/canvas/createRadialGradient.shtml
        const gradient = ctx.createRadialGradient(this.x, this.y, 0, this.x, this.y, this.size);
        gradient.addColorStop(0, 'rgba(255, 255, 255, 0.8)');//中心
        gradient.addColorStop(0.5, 'rgba(255, 255, 255, 0.5)');
        gradient.addColorStop(1, 'rgba(255, 255, 255, 0.1)');//外側

        //塗りをグラデーションにする
        ctx.fillStyle = gradient;

        //座標(x, y)に半径sizeの円弧を描く
        ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);

        //円弧を塗りつぶす
        ctx.fill();

        //パスを閉じる
        ctx.closePath();
    }

    //雪片を動かすメソッド
    move() {

        //ラジアン
        const rad = this.y * Math.PI / 180;

        //雪片をふらふらと左右に揺らしながら降らす
        this.x -=  Math.sin(rad * this.speed) * this.swingWidth;///getRandomInt(0, 0.5)*Math.sin(rad) * this.swingWidth
        this.y += this.speed;

        //雪片が画面下に隠れたら
        if (this.y > height || this.x < 0) {

            //隠れた雪片を先頭に戻す
            this.x = getRandomInt(0, width);
            this.y = 0;
        }

        //雪片を描画する
        this.draw();
    }

    //雪片を再配置・リサイズするメソッド
    resize(size, x, y){

        //再配置
        this.x = x;
        this.y = y;

        //リサイズ
        this.size = size;
    }
}

このクラスは雪片の座標・サイズ・スピード・振り幅のプロパティを持ちます。

また、このクラスは雪片を描画するdrawメソッドと雪片を動かすmoveメソッド、雪片の座標とサイズを更新するresizeメソッドを持ちます。

drawメソッドが実行されると雪片が描画されます。雪片は円グラデーションで表現します。

moveメソッドが実行されると雪片が下へ移動します。雪片はただ下へ移動させるのではなく、左右に揺れながら下へ移動させます。これを実現するにはyにはspeedを加えていき、xにはsin値に振り幅swingWidthを掛けた値を加えていきます。radは1度ずつ増えますが、speedを掛けると4度ずつ進むため揺れが速くなります。

また、雪片が画面下に隠れたら隠れた雪片を先頭に戻し、最後にdrawメソッドを実行して雪片を描画します。

ウィンドウがリサイズされるとresizeメソッドが実行され、それに合わせて雪片の座標とサイズが自動的に調節されます。
ウィンドウのリサイズに応じて雪片の座標を調節する訳は、常に画面内に雪を収めたいのでウィンドウのリサイズ時に雪片が画面からはみ出ないようにするためです。

run関数を定義

全レイヤーの雪を降らせるrun関数を定義します。

//全レイヤーの雪を降らせる関数
function run() {

    //描画されていた内容を全て消去する
    canvas.ctx.clearRect(0, 0, width, height);

    //雪を動かす
    for (let i = 0; i < snows1.length; i++) {
        snows1[i].move();
    }
    for (let i = 0; i < snows2.length; i++) {
        snows2[i].move();
    }
    for (let i = 0; i < snows3.length; i++) {
        snows3[i].move();
    }

    //run関数を繰り返し実行
    requestAnimationFrame(run);
}

requestAnimationFrameメソッドでrun関数を繰り返し実行することで、moveメソッドが連続して実行されて雪を降らすことができます。

init関数を定義・実行

全体を初期化するinit関数を定義・実行します。

//全体を初期化する関数
function init() {

    //レイヤーとなる配列に雪片のオブジェクトをwidth / 8個追加して雪の集合を3つ作る
    for (let i = 0; i < width / 8; i++) {
        snows1.push(new Snowflake(x = getRandomInt(0, width), y = getRandomInt(0, height), size1, speed1 = getRandomInt(0, 0.4)));
    }
    for (let i = 0; i < width / 8; i++) {
        snows2.push(new Snowflake(x = getRandomInt(0, width), y = getRandomInt(0, height), size2, speed2 = getRandomInt(0.4, 1)));
    }
    for (let i = 0; i < width / 8; i++) {
        snows3.push(new Snowflake(x = getRandomInt(0, width), y = getRandomInt(0, height), size3, speed3 = getRandomInt(1, 2)));
    }

    //全レイヤーの雪を降らせる
    run();
}

//初期化
init();

配列snows1~snows3にそれぞれ雪片のオブジェクトをwidth / 8個追加して雪の集合を3つ作成し、それらの雪をrun関数を実行して降らせます。

雪の集合を3つ作成した理由は、パララックス効果で奥行き感を生み出すためです。雪の集合をレイヤーに例えると一番先に作成した雪の集合snows1が最も奥のレイヤーとなり、最後に作成した雪の集合snows3が最も手前のレイヤーとなります。そして奥の階層へ行くにつれ雪片のサイズを小さく、スピードを落とすことで、パララックス効果による奥行感を生み出すことができるのです。

snow-layer

resize関数を定義・実行

ウィンドウのリサイズに合わせて全ての雪片の座標とサイズを更新するresize関数を定義・実行します。resize関数はaddEventListenerメソッドによってウィンドウがリサイズされると実行されます。

//雪をリサイズする関数
function resize() {

    //ウィンドウサイズを更新
    width = window.innerWidth;
    height = window.innerHeight;

    //雪片のサイズを更新
    size1 = Math.min(width, height) / 380;
    size2 = Math.min(width, height) / 200;
    size3 = Math.min(width, height) / 100;

    //全ての雪片を再配置・リサイズ
    for (let i = 0; i < snows1.length; i++) {
        snows1[i].resize(size1, x = getRandomInt(0, width), y = getRandomInt(0, height));
    }
    for (let i = 0; i < snows2.length; i++) {
        snows2[i].resize(size2, x = getRandomInt(0, width), y = getRandomInt(0, height));
    }
    for (let i = 0; i < snows3.length; i++) {
        snows3[i].resize(size3, x = getRandomInt(0, width), y = getRandomInt(0, height));
    }
}

//ウィンドウがリサイズされるとresize関数が実行される
window.addEventListener('resize', () => resize());

完成したJavaScript

完成したJavaScriptのコードは以下の通りです。

(() => {
    //requestAnimationFrameのベンダープレフィクス
    const requestAnimatoinFrame =
        window.requestAnimationFrame ||
        window.mozRequestAnimationFrame ||
        window.webkitRequestAnimationFrame ||
        window.msRequestAnimationFrame ||
        window.oRequestAnimationFrame ||
        function(callback) {
            window.setTimeout(callback, 1000 / 60);
        };

    //ウィンドウサイズ
    let width = window.innerWidth;
    let height = window.innerHeight;

    //雪が降るスピード
    //レイヤー毎にスピードを変える
    let speed1 = null;
    let speed2 = null;
    let speed3 = null;

    //雪片のサイズ
    let size1 = Math.min(width, height) / 380;
    let size2 = Math.min(width, height) / 200;
    let size3 = Math.min(width, height) / 100;

    //雪片の座標
    let x = null;
    let y = null;

    //雪の集合を含むレイヤーとなる配列
    const snows1 = []; //1層目のレイヤー(一番奥)
    const snows2 = []; //2層目のレイヤー(真ん中)
    const snows3 = []; //3層目のレイヤー(先頭)

    //canvasに関するクラス
    class Canvas {
        constructor() {
            //#canvasの参照
            this.elem = document.getElementById("canvas");

            //canvasのコンテキストを取得
            this.ctx = this.elem.getContext("2d");

            //ウィンドウサイズをcanvasのサイズとする
            this.elem.width = width;
            this.elem.height = height;

            //ウィンドウがリサイズされるとresizeメソッドが実行される
            window.addEventListener("resize", () => this.resize());
        }

        //canvasをリサイズ
        resize() {
            //canvasのサイズを更新
            this.elem.width = width = window.innerWidth;
            this.elem.height = height = window.innerHeight;
        }
    }

    //canvasオブジェクトを生成
    const canvas = new Canvas();

    //雪片に関するクラス
    class Snowflake {
        constructor(x, y, size, speed) {
            //雪片の座標
            this.x = x;
            this.y = y;

            //雪片のサイズ
            this.size = size;

            //雪片のスピード
            this.speed = speed;

            //雪片の振り幅
            this.swingWidth = 0.3;
        }

        //雪片を描画するメソッド
        draw() {
            const ctx = canvas.ctx;

            //パスを初期化
            ctx.beginPath();

            //円グラデーションを指定
            //http://www.htmq.com/canvas/createRadialGradient.shtml
            const gradient = ctx.createRadialGradient(
                this.x,
                this.y,
                0,
                this.x,
                this.y,
                this.size
            );
            gradient.addColorStop(0, "rgba(255, 255, 255, 0.8)"); //中心
            gradient.addColorStop(0.5, "rgba(255, 255, 255, 0.5)");
            gradient.addColorStop(1, "rgba(255, 255, 255, 0.1)"); //外側

            //塗りをグラデーションにする
            ctx.fillStyle = gradient;

            //座標(x, y)に半径sizeの円弧を描く
            ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);

            //円弧を塗りつぶす
            ctx.fill();

            //パスを閉じる
            ctx.closePath();
        }

        //雪片を動かすメソッド
        move() {

            //ラジアン
            const rad = this.y * Math.PI / 180;

            //雪片をふらふらと左右に揺らしながら降らす
            this.x -= Math.sin(rad * this.speed) * this.swingWidth; ///getRandomInt(0, 0.5)*Math.sin(rad) * this.swingWidth
            this.y += this.speed;

            //雪片が画面下に隠れたら
            if (this.y > height || this.x < 0) {
                //隠れた雪片を先頭に戻す
                this.x = getRandomInt(0, width);
                this.y = 0;
            }

            //雪片を描画する
            this.draw();
        }

        //雪片を再配置・リサイズするメソッド
        resize(size, x, y) {
            //再配置
            this.x = x;
            this.y = y;

            //リサイズ
            this.size = size;
        }
    }

    //全体を初期化する関数
    function init() {
        //レイヤーとなる配列に雪片のオブジェクトをwidth / 8個追加して雪の集合を3つ作る
        for (let i = 0; i < width / 8; i++) {
            snows1.push(
                new Snowflake(
                    (x = getRandomInt(0, width)),
                    (y = getRandomInt(0, height)),
                    size1,
                    (speed1 = getRandomInt(0, 0.4))
                )
            );
        }
        for (let i = 0; i < width / 8; i++) {
            snows2.push(
                new Snowflake(
                    (x = getRandomInt(0, width)),
                    (y = getRandomInt(0, height)),
                    size2,
                    (speed2 = getRandomInt(0.4, 1))
                )
            );
        }
        for (let i = 0; i < width / 8; i++) {
            snows3.push(
                new Snowflake(
                    (x = getRandomInt(0, width)),
                    (y = getRandomInt(0, height)),
                    size3,
                    (speed3 = getRandomInt(1, 2))
                )
            );
        }

        //全レイヤーの雪を降らせる
        run();
    }

    //初期化
    init();

    //全レイヤーの雪を降らせる関数
    function run() {
        //描画されていた内容を全て消去する
        canvas.ctx.clearRect(0, 0, width, height);

        //雪を動かす
        for (let i = 0; i < snows1.length; i++) {
            snows1[i].move();
        }
        for (let i = 0; i < snows2.length; i++) {
            snows2[i].move();
        }
        for (let i = 0; i < snows3.length; i++) {
            snows3[i].move();
        }

        //run関数を繰り返し実行
        requestAnimationFrame(run);
    }

    //雪をリサイズする関数
    function resize() {
        //ウィンドウサイズを更新
        width = window.innerWidth;
        height = window.innerHeight;

        //雪片のサイズを更新
        size1 = Math.min(width, height) / 380;
        size2 = Math.min(width, height) / 200;
        size3 = Math.min(width, height) / 100;

        //全ての雪片を再配置・リサイズ
        for (let i = 0; i < snows1.length; i++) {
            snows1[i].resize(
                size1,
                (x = getRandomInt(0, width)),
                (y = getRandomInt(0, height))
            );
        }
        for (let i = 0; i < snows2.length; i++) {
            snows2[i].resize(
                size2,
                (x = getRandomInt(0, width)),
                (y = getRandomInt(0, height))
            );
        }
        for (let i = 0; i < snows3.length; i++) {
            snows3[i].resize(
                size3,
                (x = getRandomInt(0, width)),
                (y = getRandomInt(0, height))
            );
        }
    }

    //ウィンドウがリサイズされるとresize関数が実行される
    window.addEventListener("resize", () => resize());

    //min~maxのランダムな値を取得する関数
    function getRandomInt(min, max) {
        return Math.random() * (max - min + 1) + min;
    }
})();

終わりに

以上で、canvas上に雪を降らせる方法を終わります。今後は三角関数を使った複雑なアニメーションに挑戦してみたいです。

参考文献