メニュー

Canvasで桜を降らせてみました

canvas_sakura_mb

はじめまして、WEBプログラマーの佐野と申します。

少し前ですが、NTTソルマーレ株式会社様の海外向けゲームアプリ「Moe! Ninja Girls」の公式サイト制作をさせていただきました!

※サーバーが海外にあるため、読み込みに時間がかかります。
※日本での配信はしておりません。

その中で、HTML5のCanvasを使って桜を降らせるアニメーションを作成しましたので、
完成までの手順をSTEPに分けて共有させていただきたいと思います。

こちらが今回作成したサンプルの完成形です。

【2017/02/13追記】

ブログの最後に、今回のサンプル使ってWEBチームで1つ作品を作ってみました。
サンプルと違い、かなり豪華なものになってますので、ぜひ最後まで見ていただければと思います。

See the Pen egdMjW by otwo (@otwo) on CodePen.

Step1:画像を表示して上から下に動かす

まずは画像の表示と画像を上から下に動かすところまでやってみました。

See the Pen NdNwEp by otwo (@otwo) on CodePen.


ソースコードはこちら!

<!DOCTYPE HTML>
<html>
<body>

<script>
var canvas = document.getElementById("cvs");
var ctx = canvas.getContext("2d");
var imgCnt = 25;  // 描画する画像の数
var aryImg = [];  // 画像の情報を格納
var cvsw = 800;   // canvasタグに指定したwidth
var cvsh = 500;   // canvasタグに指定したheight
var imgBaseSizeW = 15;    // 画像の基本サイズ横幅
var imgBaseSizeH = 18.5;  // 画像の基本サイズ立幅

// 画像の読み込み
var img = new Image();
img.src = "./images/sakura.png";
img.onload = flow_start;

// 画像のパラメーターを設定
function setImagas(){
  for(var i = 0;i < imgCnt;i++){
    aryImg.push({
      "posx": Math.random()*cvsw,     // 初期表示位置x
      "posy": Math.random()*cvsh,     // 初期表示位置y
      "sizew": imgBaseSizeW,          // 画像の横幅
      "sizeh": imgBaseSizeH,          // 画像の縦幅
    });
  }
}

// 描画、パラメーターの更新
var idx = 0;
function flow(){
  ctx.clearRect(0,0,cvsw,cvsh);
  for(idx = 0;idx < imgCnt;idx++){
    aryImg[idx].posy += 1;
    ctx.drawImage(img, aryImg[idx].posx, aryImg[idx].posy, aryImg[idx].sizew , aryImg[idx].sizeh);
    // 範囲外に描画された画像を上に戻す
    if(aryImg[idx].posy >= cvsh){
      aryImg[idx].posy = -aryImg[idx].sizeh;
    }
  }
}

function flow_start(){
  setImagas();
  setInterval(flow,10);
}
</script>
</body>
</html>

すごい機械的に桜が流れていますね。
しかしStep1はこれでクリアです!

メソッドの説明

次に進む前にCanvasで使用したメソッドの説明をさせてもらいます。

clearRect()メソッド

clearRectは指定した範囲の描画面の内容をクリアするメソッドになります。
引数は
clearRect(基準x座標 , 基準y座標 , 横幅 , 立幅)
を渡しています。

Canvasでアニメーションを表現する場合、jQueryなどのDOM操作と違い、
描画 → クリア → 描画 → クリア → 描画 ・・・
を連続で繰り返すことでアニメーションしている様に見せています。
なので、描画を始める前に必ず直前の描画をクリアする必要があるので、
記憶の片隅にでも覚えておくと良いかと思います!

drawImage()メソッド

drawImageは画像オブジェクトを描画するメソッドになります。
こちらのメソッドには、いくつか引数のパターンがありますが今回は
drawImage(画像オブジェクト , 表示x座標 , 表示y座標)
の形式で使用しています。

Step2:画像毎にサイズ、動く速度を変える

次に自然な感じを出すために、画像1つ1つに違うパラメーターを設定してみました!
今回は以下の2つを変えてみます。
・画像のサイズ
・動く速度

See the Pen Grjxyz by otwo (@otwo) on CodePen.

<!DOCTYPE HTML>
<html>
<body>

<script>
var canvas = document.getElementById("cvs");
var ctx = canvas.getContext("2d");
var imgCnt = 25;          // 描画する画像の数
var aryImg = [];          // 画像の情報を格納
var cvsw = 800;           // canvasタグに指定したwidth
var cvsh = 500;           // canvasタグに指定したheight
var imgBaseSizeW = 15;    // 画像の基本サイズ横幅
var imgBaseSizeH = 18.5;  // 画像の基本サイズ立幅
var aspectMax = 1.5;      // アスペクト比計算時の最大値
var aspectMin = 0.5;      // アスペクト比計算時の最小値
var speedMax = 2;         // 落下速度の最大値
var speedMin = 0.5;       // 落下速度の最小値

// 画像の読み込み
var img = new Image();
img.src = "./images/sakura.png";
img.onload = flow_start;

// 画像のパラメーターを設定
function setImagas(){
  var aspect = 0;
  for(var i = 0;i < imgCnt;i++){
    // 画像サイズに掛けるアスペクト比を0.5~1.5倍でランダムで生成
    aspect = Math.random()*(aspectMax-aspectMin)+aspectMin;
    aryImg.push({
      "posx": Math.random()*cvsw,   // 初期表示位置x
      "posy": Math.random()*cvsh,   // 初期表示位置y
      "sizew": imgBaseSizeW*aspect, // 画像の横幅
      "sizeh": imgBaseSizeH*aspect, // 画像の縦幅
      "speedy": Math.random()*(speedMax-speedMin)+speedMin, // 画像が落ちていく速度
    });
  }
}

// 描画、パラメーターの更新
var idx = 0;
function flow(){
  ctx.clearRect(0,0,cvsw,cvsh);
  for(idx = 0;idx < imgCnt;idx++){
    aryImg[idx].posy += aryImg[idx].speedy;
    ctx.drawImage(img, aryImg[idx].posx, aryImg[idx].posy, aryImg[idx].sizew , aryImg[idx].sizeh);
    // 範囲外に描画された画像を上に戻す
    if(aryImg[idx].posy >= cvsh){
      aryImg[idx].posy = -aryImg[idx].sizeh;
    }
  }
}

function flow_start(){
  setImagas();
  setInterval(flow,10);
}
</script>
</body>
</html>

Step1からあまり手を入れていませんが、個々にバラつきをつけることで印象がだいぶ変わって見えました。
今回は桜ですが、雪など小さいものを降らせる場合には、これだけでもそれらしく見えるかもしれません。

今回の主な変更点はこちらになります。(26~34行)

// 画像サイズに掛けるアスペクト比を0.5~1.5倍でランダムで生成
aspect = Math.random()*(aspectMax-aspectMin)+aspectMin;
aryImg.push({
  "posx": Math.random()*cvsw,   // 初期表示位置x
  "posy": Math.random()*cvsh,   // 初期表示位置y
  "sizew": imgBaseSizeW*aspect, // 画像の縦幅
  "sizeh": imgBaseSizeH*aspect, // 画像の横幅
  "speedy": Math.random()*(speedMax-speedMin)+speedMin, // 画像が落ちていく速度
});

特に説明は要らないかとも思いますが・・・さらっとだけ。
画像サイズ[sizew , sizeh]に掛ける倍率をランダムに生成して乗算している事と、
speedyを新しく追加して、flow関数内でposyのパラメーターへの加算に使用しています。

そして、最後にもう1工夫してみたいと思います!

Step3:画像の角度を変える

やはり花びらを降らせるわけなので、ひらひらさせたいと思いますよね!
と言う事で最終ステップは画像の角度を変えてみたいと思います。

See the Pen egdMjW by otwo (@otwo) on CodePen.


最終のソースコードです!

<!DOCTYPE HTML>
<html>
<body>

<script>
var canvas = document.getElementById("cvs");
var ctx = canvas.getContext("2d");
var imgCnt = 25;          // 描画する画像の数
var aryImg = [];          // 画像の情報を格納
var cvsw = 800;           // canvasタグに指定したwidth
var cvsh = 500;           // canvasタグに指定したheight
var imgBaseSizeW = 15;    // 画像の基本サイズ横幅
var imgBaseSizeH = 18.5;  // 画像の基本サイズ立幅
var aspectMax = 1.5;      // アスペクト比計算時の最大値
var aspectMin = 0.5;      // アスペクト比計算時の最小値
var speedMax = 2;         // 落下速度の最大値
var speedMin = 0.5;       // 落下速度の最小値
var angleAdd = 4;         // 画像角度への加算値

// 画像の読み込み
var img = new Image();
img.src = "./images/sakura.png";
img.onload = flow_start;

// 画像のパラメーターを設定
function setImagas(){
  var aspect = 0;
  for(var i = 0;i < imgCnt;i++){
    // 画像サイズに掛けるアスペクト比を0.5~1.5倍でランダムで生成
    aspect = Math.random()*(aspectMax-aspectMin)+aspectMin;
    aryImg.push({
      "posx": Math.random()*cvsw,   // 初期表示位置x
      "posy": Math.random()*cvsh,   // 初期表示位置y
      "sizew": imgBaseSizeW*aspect, // 画像の横幅
      "sizeh": imgBaseSizeH*aspect, // 画像の縦幅
      "speedy": Math.random()*(speedMax-speedMin)+speedMin, // 画像が落ちていく速度
      "angle": Math.random()*360,   // 角度
    });
  }
}

// 描画、パラメーターの更新
var idx = 0;
var cos = 0;
var sin = 0;
var rad = Math.PI / 180;
function flow(){
  ctx.clearRect(0,0,cvsw,cvsh);
  for(idx = 0;idx < imgCnt;idx++){
    aryImg[idx].posy += aryImg[idx].speedy;
    aryImg[idx].angle += Math.random()*angleAdd;
    cos = Math.cos(aryImg[idx].angle * rad);
    sin = Math.sin(aryImg[idx].angle * rad);
    ctx.setTransform(cos, sin, sin, cos, aryImg[idx].posx, aryImg[idx].posy);
    ctx.drawImage(img, 0, 0 , aryImg[idx].sizew , aryImg[idx].sizeh);
    ctx.setTransform(1, 0, 0, 1, 0, 0);
    // 範囲外に描画された画像を上に戻す
    if(aryImg[idx].posy >= cvsh){
      aryImg[idx].posy = -aryImg[idx].sizeh;
    }
  }
}

function flow_start(){
  setImagas();
  setInterval(flow,10);
}
</script>
</body>
</html>

どうでしょう!一気にそれらしものができたんじゃないでしょうか!!

メソッドの説明

変更点の説明前に、新しく出てきたメソッドの説明です。

setTransform()メソッド

setTransformは描画面を変形するメソッドです。
ざっくりとしたイメージは
描画面の伸縮、角度、座標を変更しているイメージかと思います。
詳しくは下記の様に細かく解説されているサイトがたくさんありますので、興味がある方は検索してみてください。

主な変更点と説明

今回の主な変更点は描画処理周辺になります。
Step2までは画像の表示位置をdrawImageメソッドで指定していましたが、
以下の様に、setTransformメソッドでの指定に変更をしています。(52~56行目)

cos = Math.cos(aryImg[idx].angle * rad);
sin = Math.sin(aryImg[idx].angle * rad);
ctx.setTransform(cos, sin, sin, cos, aryImg[idx].posx, aryImg[idx].posy);
ctx.drawImage(img, 0, 0 , aryImg[idx].sizew , aryImg[idx].sizeh);
ctx.setTransform(1, 0, 0, 1, 0, 0);

簡単にですが処理の流れは、こんな感じになります。
1:cosとsinには傾斜率を入れています。
2:setTransformで指定した方に描画面を変形しています。
3:drawImageで画像を描画します。
4:次の描画に影響が出ない様に、描画面の変形をリセットしています。

最後に

今回はあまり複雑な処理は入れずに、極力簡潔に組んでみました。
各パラメーターを変更したり加えたりして、動きにもっとバリエーションを増やすなどカスタマイズすると、もっともっとそれらしくなるかと思います!
今回の様な動きの参考サイトはたくさんありますが、その中でこの記事が何かのお役に立てたなら幸いに思います!
それでは、またの機会に!!

追記

WEBチームで「夜の花見」をテーマに作成してみましたので載せておきます!
画像はテーマに沿ったものを作成してもらいましたが、プログラムは今回のサンプルをベースに、風に吹かれている様な動き等を追加しています。
やはりデザインが入ると一気に印象が変わりますね!

See the Pen WpPmxx by otwo (@otwo) on CodePen.

この記事をシェアする

  • twitter
  • facebook
  • Google+
  • B!はてブ
  • pocket
トップへ戻る

CONTACT

ゲーム開発、Webサイト制作に関するご相談等ございましたら、お気軽にお問い合わせください。

ゲーム開発に関する
お問い合わせはこちら

Webサイト制作に関する
お問い合わせはこちら