冬っぽい雪が降るだけのアニメーションをjavascript
で作った。
デモ
ずっと動いたままになるので、バッテリー残量注意。
ソースもgithub
に: ikapper/anim-fuyu-yuki
使ったもの
canvas
を直に操作するのは辛いので、fabric
を使用した。公式サイトはこちら: Fabric.js Javascript Canvas Library
リソースはwebpack
でバンドルしている。
していること
開いたときに表示されているものが全てだが、一応書いておく。
fabric
でstatic canvas
を生成- 生成した
canvas
に背景色となる色を設定 - 家を2つ生成
- 雪を生成
- 雪を下へアニメーション
fabric.StaticCanvasの生成
fabric.StaticCanvas
の生成時はadd
メソッドでレンダリングが行われないようにしておく:
fabcanvas = new fabric.StaticCanvas('canvas', {
renderOnAddRemove: false
});
レンダリングしたいときは、fabcanvas.renderAll()
を実行する。
レンダリングを行うタイミングは、初期化後と雪を下方向に動かした後の2つ。後者は何度もコールする。
また、背景色も設定する。fabcanvas.setBackgroundColor('#e2e2e2');
とするだけ。
家の生成
家は自作したsvgイメージを読み込むことにした。
import HouseWinter from "./assets/house_winter.svg";
// fabricのStaticCanvasのインスタンス。初期化時に代入される。
let fabcanvas;
const roofColors = ['#ffe1e1', '#ff9595', '#ffca95', '#ca95ff'];
function init_house(scaling) {
// 家を表現
const canvasH = fabcanvas.getHeight();
const canvasW = fabcanvas.getWidth();
fabric.loadSVGFromURL(HouseWinter, (oImg) => {
const house = new fabric.Group(oImg);
house.set({ scaleX: scaling, scaleY: scaling });
house.set({
top: canvasH - house.height * scaling,
left: (canvasW - house.width * scaling) * Math.random()
});
house.item(3).set({ 'stroke': roofColors[Math.floor(roofColors.length * Math.random())] });
fabcanvas.insertAt(house, 0);
});
}
上方から順に説明していく。
屋根色をランダムにするために、何色か用意してリストで保持している。
引数のscaling
は拡大縮小の倍率で、生成するsvg
の大きさで調整が必要。今回は0.5
と0.3
で生成している。
関数の実行時には、最初にcanvas
の幅と高さを取得している。定数で持っていてもいいのだが、ウィンドウのリサイズにも対応するようにしているため、各関数で取得するようにしている。家のレンダリングは最初の1回だけなのであまり関係はない。
fabric.loadSVGFromURL()
は画像パスとコールバックを指定してsvgをロードできる関数。oImg
は(確か)fabric.Image
のインスタンスのリストだったので、fabric.Group
で1つのhouse
にまとめている。
そして、house
の大きさをセットして、canvas
上の位置を決めている。
house.item(3)
は屋根部分で、かつstroke
で着色しているので、stroke
を上書きしている。
fabric.StaticCanvas.add()
はレイヤ的に考えると最上位に追加されるようだ。アニメーションされる雪が奥側に行ってしまうと少し不自然になるので、奥側にhouse
を配置するためにfabric.StaticCanvas.insertAt()
を使って挿入位置を指定している。実際を考えたら奥に行く雪もあるが考慮してない。
スケール倍率を指定してこの関数を呼び出せば、canvas
に家を描画できる。重なったときの違和感はご愛嬌。
雪の生成
雪は円で表現している。大きさの異なる円をcanvas
上に散らせておけばそれっぽく見えるのは不思議な感じもする。
const snows = [];
const SNOW_COUNT_MAX = 150; // 粒
const snowColors = ['#f8f8ff', '#f9fafb', '#fffaff', '#fbfbf9', '#fafafa'];
function init_yuki() {
const canvasW = fabcanvas.getWidth();
const canvasH = fabcanvas.getHeight();
for (let i = 0; i < SNOW_COUNT_MAX; i++) {
const snow = new fabric.Circle({
radius: 10 * Math.random() + 3,
fill: snowColors[Math.floor(snowColors.length * Math.random())],
left: canvasW * Math.random(),
top: canvasH * Math.random(),
});
snow.velY = 15 * Math.random() + 10;
fabcanvas.add(snow);
snows.push(snow);
}
}
関数外から。
snows
は、アニメーション用に関数外で保持する。fabcanvas
から取得もできるが、家との区別が面倒なのでこの形にした。
次に雪の最大個数をSNOW_COUNT_MAX
で定義しておく。数を大きくしすぎるとかくつくことがある。
また家の屋根と同じく、雪の色も5通り用意しておく。
関数の外側はこれで終わり、今度は中身に移る。
家の方で述べたとおり、まず最初にcanvas
の幅・高さを取得する。
次にループで雪となる円を生成していく。
fabric.Circle
をnew
すればいい。半径・塗りつぶし色・位置の順で指定している。top
とleft
はcanvas
の左上からの位置。
その後、アニメーションで使うvelY
を設定する。下方向へ動く速度になる。単位はpixel/sec
。範囲は[10,25)
とした。
最後に、fabcanvas
とsnows
に追加する。
アニメーション
雪を下方向へ動かすアニメーションを行いたい。
fabric.util.requestAnimFrame()
を利用する。window.requestAnimationFrame()
の改良版らしい。
使い方は同じ。
let timestamp = 0;
const reqAnimFrame = () => {
fabric.util.requestAnimFrame((time) => {
const timedelta = time - timestamp; // ms
timestamp = time;
const canvasH = fabcanvas.getHeight();
const canvasW = fabcanvas.getWidth();
for (let snow of snows) {
let newy = snow.get('top') + snow.velY * timedelta / 1000;
if (newy > canvasH + 20) {
newy = -20;
snow.set('left', canvasW * Math.random());
}
snow.set('top', newy);
}
fabcanvas.renderAll();
reqAnimFrame();
}, fabcanvas);
};
reqAnimFrame();
正しい使い方をしている自信があまりない。とりあえず普通に動いているのでいいだろう。
前回実行時の時間を保持するために外側にtimestamp
を持つ。単位はミリ秒。
最近無名関数でも自身を呼び出して再帰できることを知ったが、ここではそれは行ってないので、関数を定義してその名前で再帰している。
ページのロード完了時からの経過時間を返してくれていると思うので、timestamp
で前回実行時間を保持し、渡されてきたtime
との差を比較し、経過時間timedelta
を知る。そのあとにtimestamp
を更新する。(改善点有り、後述)
またこれまで通りcanvas
の幅・高さを取得。
ループでsnows
の中身のfabric.Circle
インスタンス達の位置を更新する。
速度velY
に基づいて次の高さを決める。その高さがcanvas
の高さを超えたら、今度は天辺から描画し直す。雪の大きさの分だけ上にはみ出させて違和感を軽減している。
また天辺からの時は、横方向の座標を再度ランダムに決定する。
全ての雪(円)について更新が終わったらcanvas
に再描画を促す。
そして最後に再帰。
再帰するまでの処理で時間がかかりすぎると動作が重くなってしまう。
改善点
とりあえず思いつくのは2点ある。修正するかもしれない改善点。
動作が重くなりがち
もう少し動作を軽くしたい。たったこれだけなのに割と重く感じる。 原因はやはり雪となる円のせいだろう。円を1つずつ描くのはやはり効率が悪い気がする。
雪のグループの画を何枚か用意して重ねれば簡単に増やせるとは思うが、ランダム性は薄くなってしまう気がする。そもそもそんな簡単に軽くなるかも分からないが…
アニメーションの起点とブラウザの仕様
requestAnimFrame()
でtimestamp
の初期値はnull
にしておくべきだった。そうしないと、最初のreqAnimFrame()
のtimedelta
の値が正確で無くなってしまうためだ。
さらに、ブラウザの省エネ設計により、他タブを開くなどでcanvas
が隠れてしまうと、requestAnimationFrame()
の更新頻度がかなり下がり、結果timedelta
が大きくなり、雪が画面外の下方へ行ったと判定されて、全て上方へ行ってしまうという問題もある。
これは、timedelta
が大きすぎたら適当な値(1000/60
など)に調整することでいい感じにごまかすことはできると思う。
もしくはsetTimeout
,setInterval
などを使う方法もある。あれらはおそらく他タブを開いても実行される?と思う(未確認)。
おわり
もう少し色々配置したいがsvg
を作るのが大変だ。もっと作るのに慣れるべきだ。見た目も改善できるし。
もっと眺めてて面白いものを作りたいものだ。
以上です。
Javascript本の広告