TomatoChups(とまとちゃっぷす)のBlog

小さな常識を考え直すブログ 日常のこととたまにテックとビール

Canvasで '雪' を描いてみた

はじめに

HTML5に導入されたcanvas。名前は聞いたことがあるけど実際何ができるの?いつ使うの?といった技術者から見た使い所的な部分は今回は置いておいて、暇な時にちょっと触ってみてプログラムでブラウザにお絵かきしたい!みたいな楽しそうなことをしたい人向けに書いた記事です。そして、私も実際1週間前くらいに初めて触っただけです。

HTML5 canvas (多分非公式だけど十分な)リファレンス

canvas とは

一言で言うと、ブラウザにお絵かきをするためのもので、javascriptを使って線だったり円だったりをプログラムで描写するといったものです。 繰り返しのアニメーションを普段フロント側でアニメーション等のプログラムを書いている方なら、setTimeoutやsetIntervalを使っていると思いますが、もっとぬるぬる動く何かを作りたい時にcanvasが最適と言うわけです。

とりあえず楽しくお絵かきができるものだと言う理解で大丈夫だと思います。今回はコード量も少ないので、javascriptを始めて基本の文法を理解した人や、気分転換に別のことをやりたいなと思っている人に最適な内容だと思います。

対応ブラウザについて

Chrome, Firefox, Safari, Operaであれば問題なく使えます。IEの場合はIE8以下が非対応らしいのですが、なんとかする方法があるらしいのと、Qiitaにプログラムを見にくる人でIE8以下を使っている人は多分ほとんどいないと思うので、みなさん問題なく利用できると思います。

必要な環境

・対応ブラウザ ・テキストエディタ

今回のコードは codepen snow こちらのコードをほぼ引用した形になるので、直接理解できると言う方はこちらを参考にしてください。ここからは、日本語のコメントを付け加え、初めての人にもわかるように説明していきます。それでは始めましょう。

コーディング

HTML5

canvasを使う場合javascriptで描画する絵を作成するので、HTMLファイルはかなりシンプルです。

index.html

<body>
  <canvas id="canvas"></canvas>
  <h2>Snow</h2>
  <script src="main.js"></script>
</body>

CSS

今回はブラウザいっぱいに雪を描画するので、CSSも非常にシンプル。

main.css

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
  }
html, body {
    width: 100%;
    height: 100%;
  }
#canvas {
    position: absolute;
    width: 100%;
    height: 100%;
    background: #000;
  }
h2 {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    color: #E0E0E0;
    /*cursor: pointer;*/
  }

canvasだけをwidth: 100%; height: 100%;で広げても反映されないので、ちゃんとhtml,bodywidth: 100%; height: 100%;で広げてくださいね。

HTMLとCSSファイルの内容はこれで全てですので、ここから編集することはありません。非常にシンプル。

JavaScript (canvasにお絵描き)

ここからやっとお絵かきタイムですね。最初に必要なものを宣言していきましょう。

[必要なものの宣言]

main.js

const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const animFrame = window.requestAnimationFrame ||
                  window.mozRequestAnimationFrame ||
                  window.webkitRequestAnimationFrame ||
                  window.msRequestAnimationFrame;
const cancFrame = window.cancelAnimationFrame ||
                  window.mozCancelAnimationFrame ||
                  window.webkitCancelAnimationFrame ||
                  window.msCancelAnimationFrame;

// 雪の粒を保存する配列
const snowflakes = [];

// animFrame()の戻り値を保存する変数 cancFrame()に入れると描画が止まる
let handle;

// canvasのwidthとheightを保存する変数
let w = ctx.canvas.width = window.innerWidth;
let h = ctx.canvas.height = window.innerHeight;

最初の4っの定数は今後canvasを使うたびに出てくると思うので、しっかり覚えておきましょう。 const ctx = canvas.getContext('2d');とは、getContext()メソッドcanvasに描画するためのAPIにアクセスするオブジェクトを返します。つまり、この定数ctxにいろんなメソッドを使って絵を描いていくということです。今回は3Dで描くのではなく2Dなので引数は'2d'となります。

animFramecancFrameに関しては、requestAnimFrameが描画を始めるためのメソッド、cancelAnimationFrameが描画を止めるためのメソッドと覚えておきましょう。

[関数random()の定義]

今回の雪の実装のために使う簡単で便利な関数を作ります。

  main.js

  function random(min, max) {
    let rand = Math.floor((min + (max - min + 1) * Math.random()));
    return rand;
  }

このrandom()関数はmin以上max以下の間でランダムな数値を返します。 次の、雪を作る関数でたくさん使います。

[雪を作る関数の定義]

  main.js

  // 雪を作るSnow()コンストラクタの定義
  function Snow() {
    // 雪のx座標を決める
    this.x = random(0, w);
 // 雪のy座標を決める
    this.y = random(-h, 0);
 // 雪の半径を決める(雪の粒の大きさ)
    this.radius = random(0.5, 3.0);
 // 雪が水平(x軸)にどれくらいのスピードで移動するかを決める
    this.wind = random(-0.5, 3.0);
 // 雪が垂直(y軸)にどれくらいのスピードで移動する(落ちる)かを決める
    this.speed = random(1, 3);
  }

  // 生成したSnow()コンストラクタにdrawメソッドを設定する
  // 雪を描画するメソッド
  Snow.prototype.draw = function() {
    ctx.beginPath();
    ctx.arc(this.x, this.y, this.radius, 0, 2*Math.PI);
    ctx.fillStyle = "#fff";
    ctx.fill();
    ctx.closePath();
  }
    
  // 生成したSnow()コンストラクタにupdateメソッドを設定する
  // 雪を次に描画する座標にupdateする(雪を動かす)
  Snow.prototype.update = function() {
 // 雪を水平(x軸)に動かす
    this.x += this.wind;
    // 雪を垂直(y軸)に動かす
    this.y += this.speed;
 // x軸y軸でどちらも動いているので、結果的に斜めに落ちるように動く

    if (this.y > h) {
      this.x = random(0, w);
      this.y = 0;
    }
  }

function Snow()では、生成したSnowインスタンスにthisを使って初期値を設定しています。具体的には、雪が最初に出現する座標と、雪の粒の大きさ、そしてそれがどういったスピードで落下していくかということを設定しています。

draw()メソッドの定義では、Snowインスタンスcanvasに描画するためのコードを記述しています。ここでcanvasの基礎知識が少し必要になります。簡単に説明すると、beginPath()で書き始めることを宣言し、arc(this.x, this.y, this.radius, 0, 2*Math.PI)で先ほどインスタンス生成した初期値を使って雪を描画します。fillStyleは色の設定で、今回は雪なので白(#fff)fill()で先ほど設定した#fffの色で雪を塗りつぶします。そして、最後にclosePath()で書き終わりということになります。今回は丸を描いているので実際closePath()がなくても問題なく動作しますが、描画の終わりをわかりやすくするためにつけておいてください。詳しく知りたい方は :point_right_tone2:closePath()について こちらに詳しく載っています。

[描画する雪の個数を決める関数]

最初に定義した配列snowflakesに、雪の粒を引数の数値分だけ入れていきます。

  main.js

  function createSnow(count) {
    for (let i = 0; i < count; i++) {
      snowflakes[i] = new Snow();
    }
  }

[雪を描画するための関数と雪を移動させるための関数]

先ほどSnowコンストラクタに定義したdrawメソッドupdateメソッドcreateSnow(count)で作成した雪ひとつひとつに実行するために作ります。

  main.js

  function draw() {
    // これがないと直線続きでレーザー光線みたいになってしまう
    ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);

    for (let i = 0; i < snowflakes.length; i++) {
      // ここで使っているdraw()はSnow.prototype.drawで定義したもの
      snowflakes[i].draw();
    }
  }

  function update() {
    for (let i = 0; i < snowflakes.length; i++) {
      // ここで使っているupdate()はSnow.prototype.updateで定義したもの
      snowflakes[i].update();
    }
  }

clearRect()は、指定した範囲のcanvasの中身を透明にするもので、この記述をなくしてしまうと、update()で移動させた雪と移動させる前の雪が繋がってしまい、最終的に直線を描くことになってしまうので注意が必要です。

ここまでくれば、描画に関する関数はほぼ書き終わりました。あとはdraw()とupdate()をループさせる関数を作成します。

[連続的に雪を描画するloop関数]

  main.js

  function loop() {
    draw();
    update();
    handle = animFrame(loop);
  }

animFrame(loop)でloop()関数自身をループさせています。handle変数に代入することで、後からループを止めたい場合などに、cancFrame(loop)でループを止めることができます。

[関数の呼び出し]

最後にcreateSnow()関数とloop関数を呼び出せば完成です。

  main.js

  createSnow(300);
  loop();

僕は雪の粒の数が300くらいが好きなので300ですが、色々試してみてください、粉雪とか、牡丹雪とか。

 まとめ

今回は雪を表現しましたが、これをベースに少しいじるだけでいろいろなアニメーションに応用が効くと思うので、興味のある方がいればいろんな数値を変更してどんどん試してみてください。

canvasの知識的には一度リファレンスを読むと理解が深まると思うので、もう一度載せておきます。 HTML5 canvas (多分非公式だけど十分な)リファレンス

最後にコメント抜きのmain.jsのコードを載せておきます。

main.js

(function(){
  'use strict';

  const canvas = document.getElementById("canvas");
  const ctx = canvas.getContext("2d");
  const animFrame = window.requestAnimationFrame ||
                  window.mozRequestAnimationFrame ||
                  window.webkitRequestAnimationFrame ||
                  window.msRequestAnimationFrame;
  const cancFrame = window.cancelAnimationFrame ||
                  window.mozcancelAnimationFrame ||
                  window.webkitcancelAnimationFrame ||
                  window.mscancelAnimationFrame;
  const snowflakes = [];

  let handle;
  let w = ctx.canvas.width = window.innerWidth;
  let h = ctx.canvas.height = window.innerHeight;
  
  function random(min, max) {
    let rand = Math.floor((min + (max - min + 1) * Math.random()));
    return rand;
  }
 
  function Snow() {
    this.x = random(0, w);
    this.y = random(-h, 0);
    this.radius = random(0.5, 3.0);
    this.speed = random(1, 3);
    this.wind = random(-0.5, 3.0);
  }
  
  Snow.prototype.draw = function() {
    ctx.beginPath();
    ctx.arc(this.x, this.y, this.radius, 0, 2*Math.PI);
    ctx.fillStyle = "#fff";
    ctx.fill();
    ctx.closePath();
  }

  Snow.prototype.update = function() {
    this.x += this.wind;
    this.y += this.speed;

    if (this.y > h) {
      this.x = random(0, w);
      this.y = 0;
    }
  }

  function createSnow(count) {
    for (let i = 0; i < count; i++) {
      snowflakes[i] = new Snow();
    }
  }

  function draw() {
    ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
    for (let i = 0; i < snowflakes.length; i++) {
      snowflakes[i].draw();
    }
  }

  function update() {
    for (let i = 0; i < snowflakes.length; i++) {
      snowflakes[i].update();
    }
  }

  function loop() {
    draw();
    update();
    handle = animFrame(loop);
  }

  createSnow(300);
  loop();

})();

今後もcanvasで天気を表現したものを載せていこうかなと思っているのでぜひ参考にしてみてください〜