HTML Canvas要素とJavaScriptを使うと、手軽にクリエイティブコーディングをはじめられます。
前回の記事『パーリンノイズを使いこなせ』に引き続き、2018年7月25日に開催されたイベント「Frontend de KANPAI! #4」の登壇内容を記事にしました。
本記事ではHTML CanvasとJavaScriptの理解につながることを目標に、パーティクル表現の作成テクニックを解説します。サンプルのソースコードはすべてGitHubにて公開していますので、あわせて参照ください。
▲ 完成版サンプル
※本記事はライブラリを利用せず、HTML CanvasとJavaScriptのみで説明しています。
ステップ① パーティクル表現
シンプルなグラフィックから手順を理解していきましょう。パーティクルの定義として、座標と速度、寿命を用意します。オブジェクトにプロパティを定義するだけです。
const MAX_LIFE = 80; // 寿命の最大値
const particles = []; // 配列でパーティクルを管理
// パーティクルを発生させます
function emit() {
// オブジェクトの作成
const particle = {
// stageW, stageH は画面の幅と高さ
x: stageW * 0.5, // パーティクルの発生場所(X)
y: (stageH * 4) / 5, // パーティクルの発生場所(Y)
vy: 0, // 速度
life: MAX_LIFE, // 寿命
};
// 配列に保存
particles.push(particle);
}
関数setTimeout()
によって、時間経過でupdate()
関数を呼び出します。関数のなかでは、パーティクルに対して重力を加えたり、摩擦を計算します。いつか消えるように寿命も減らしておくことも必要です。
// パーティクルを更新します
function update() {
// パーティクルの計算を行う
for (let i = 0; i < particles.length; i++) {
// オブジェクトの作成
const particle = particles[i];
// 重力
particle.vy -= 1;
// 摩擦
particle.vy *= 0.92;
// 速度を位置に適用
particle.y += particle.vy;
// 寿命を減らす
particle.life -= 1;
// 寿命の判定
if (particle.life <= 0) {
// 配列からも削除
particles.splice(i, 1);
i -= 1;
}
}
}
描画は次のようにHTML Canvasに描きます。配列particles
に格納したパーティクルの情報を使います。
// 描画します
function draw() {
// 画面をリセット
context.clearRect(0, 0, stageW, stageH);
for (let i = 0; i < particles.length; i++) {
const particle = particles[i];
context.beginPath();
// 白い色を設定
context.fillStyle = "white";
// 円を描く
context.arc(particle.x, particle.y, 100, 0, Math.PI * 2, false);
// 形状に沿って塗る
context.fill();
context.closePath();
}
}
ステップ② 寿命でスケール変化
パーティクルの寿命を使って変化を持たせます。発生時のパーティクルは大きいサイズとし、寿命を迎えて死滅するときには小さいサイズにします。
寿命に対する生存期間の割合は計算によって求まるので、その値をスケールに割り当てます。
// パーティクルのサイズを寿命に比例にする
const scale = particle.life / MAX_LIFE;
particle.scale = scale;
particle.life -= 1; // 寿命を減らす
if (particle.life <= 0) {
// 寿命の判定
// 削除
}
ステップ③ 数を増やす
時間経過でパーティクルの発生頻度をあげます。1秒間に60回呼び出すtick()
関数において、パーティクル発生のemit()
関数を呼び出すようにします。発生位置に乱数を持たせて、横幅いっぱいにパーティクルを発生するようにしておきましょう。
tick();
/** フレーム更新のタイミングです。 */
function tick() {
setTimeout(tick, 16);
// パーティクルを発生
emit();
// パーティクルを更新
update();
// 画面を更新する
draw();
}
// パーティクルを発生させます
function emit() {
// オブジェクトの作成
const particle = {
x: stageW * Math.random(), // パーティクルの発生場所(X)
y: (stageH * 3) / 4, // パーティクルの発生場所(Y)
vy: 30 * (Math.random() - 0.5), // 速度
life: MAX_LIFE, // 寿命
};
particles.push(particle);
}
ステップ④ 形状を増やし点滅させる
形状を増やすために、パーティクル発生時に形状の種類を示すtype
プロパティを設定します。
// オブジェクトの作成
const particle = {
// 種類を定義
type = Math.floor(Math.random() * 2).toString();
// 他いろいろ・・・
};
type
プロパティに値によって描画の形状を切り替えます。このとき、透明度は80%〜100%の値になるように乱数を使って設定します。時間経過で乱数によって透明度が変化するため、点滅しているような表現になります。
context.beginPath();
context.arc(particle.x, particle.y, /* 省略 */);
// 点滅ロジック 0.8〜1.0の間で乱数を得る
const alpha = Math.random() * 0.2 + 0.8;
switch (particle.type) {
case "0": // 塗りつぶした円「●」を描画
context.fillStyle = `hsla(0, 0%, 50%, ${alpha})`;
context.fill();
break;
case "1": // 線で円「○」を描画
context.strokeStyle = `hsla(0, 0%, 50%, ${alpha})`;
context.lineWidth = 5;
context.stroke();
break;
}
context.closePath();
ステップ⑤ 乱数を使いこなせ
乱数の制御テクニックを紹介します。乱数を使ってパーティクルの発生位置を調整すると、表現にバリエーションを増やせます。JavaScriptにはMath.random()
という一様分布の乱数生成命令があります。この乱数を加減乗除するとさまざまなバリエーションが得られます。
中央に偏らせる
const x = (Math.random() + Math.random()) / 2;
中央に強く偏らせる
const valueA = (Math.random() + Math.random()) / 2;
const valueB = (Math.random() + Math.random()) / 2;
const x = (valueA + valueB) / 2;
端に偏らせる
const base = Math.random() * Math.random() * Math.random();
const inverse = 1.0 - base;
const x = Math.random() < 0.5 ? base : inverse;
ここで紹介したランダムの計算方法は一部です。詳しくは記事『JavaScript開発に役立つ重要なランダムの数式まとめ』にまとめているので、参照ください。
ステップ⑥ 最適化手法としてのオブジェクトプール
パーティクル表現は、大量のオブジェクトの生成・破棄を繰り返します。JavaScriptにはメモリ管理の機構としてガベージコレクション(略してGCといいます)が存在します。どこからも参照されておらず不要になったオブジェクト(ゴミ=ガベージ)を自動的に回収することで、メモリ使用量を抑えます。
一見便利そうなガベージコレクションは、実は重たい処理です。ガベージコレクションが発生すると、数ミリ秒の負荷が発生します。数ミリ秒というのは、表現の世界ではシビアな時間です。ウェブコンテンツは多くの端末で60fps(1秒間に60回の画面更新)で動作しますが、その場合は1フレームあたり16ミリ秒しか時間がありません。16ミリ秒で1フレームの描画更新処理を終わらせなければならないのに、数ミリ秒もガベージコレクションに時間が取られてしまいます。
もし描画更新処理が16ミリ秒をこえてしまえば、一瞬止まったようなプチフリーズが発生します。そこで対策となるのがオブジェクトプールです。
JavaScriptで参照が外れると、ガベージコレクションの対象となります。メモリ使用量が多いと、強烈なガベージコレクションを引き起こす可能性があります。
オブジェクトプールはガベージコレクションの対象にならないように、あえて参照を保持する方法です。インスタンスはプールと呼ばれる配列に保持されるので(参照を保ち続けるので)、ガベージコレクションの対象にはなりません。
次のコードはオブジェクトプールの簡易的な実装例です。
const objectPool = []; // オブジェクトプール
function toPool(particle) {
// 使用済み粒子はプールに移動
objectPool.unshift(particle);
}
function fromPool() {
if (objectPool.length === 0) {
// プールが空なら新規生成
return new Particle();
} else {
// プールにストックがあれば取り出す
return objectPool.pop();
}
}
詳しくは次の記事『ゲームにおけるガベージコレクションとの付き合い方 - Qiita』が参考になるでしょう。内容はかつてのFlashの話題ですが、JavaScriptでも同じことがいえます。
まとめ
今回の記事はパーティクル作り方を説明しました。簡単な物理演算とJavaScriptの配列管理がキモです。記事「CreateJS でパーティクルシステムの開発に挑戦しよう」では初級的な内容から解説しているので、あわせて参照ください。
補足
時間経過の処理にはrequestAnimationFrame()
を使うのが一般的ですが、本記事ではsetTimeout()
で説明しています。当初はrequestAnimationFrame()
を利用していましたが、昨今、フレームレートの異なるデバイス環境が増えてきたことをうけ、デバイス依存の少ないsetTimeout()
に変更しました。
次の記事『Canvasだけじゃない!requestAnimationFrameを使ったアニメーション表現』では対策として「リフレッシュレートに依存しない処理」を紹介しています。
※この記事が公開されたのは6年前ですが、5か月前の7月に内容をメンテナンスしています。