JavaScriptでcanvas上に雪を降らせる方法です。
DEMO
HTMLを書く
始めに以下のHTMLを書きます。
<canvas id="canvas"></canvas>
HTMLではcanvas要素のみを定義しています。
CSSを書く
次に以下のCSSを書きます。
html, body { margin: 0; overflow: hidden; } #canvas { background: #08233E; }
html
とbody
には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
が最も手前のレイヤーとなります。そして奥の階層へ行くにつれ雪片のサイズを小さく、スピードを落とすことで、パララックス効果による奥行感を生み出すことができるのです。
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上に雪を降らせる方法を終わります。今後は三角関数を使った複雑なアニメーションに挑戦してみたいです。