前回は レーザービームのエフェクトをシミュレーション しましたが、今回は爆発のエフェクトをやってみることにします。
やることは単純で円を大きくして消滅させるだけです。ぼかしをつかって縁の色を変えたものを爆発発生地点の周囲に複数つくるといい感じになることがわかりました。各自でよさそうな値を見つけてくだされば・・・。
HTML部分
HTML部分を示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>爆発のシミュレーション</title> <meta name = "viewport" content = "width=device-width, initial-scale = 1.0"> <link rel = "stylesheet" href = "./style.css" type = "text/css" media = "all"> <link rel="stylesheet" href="./style-mobile.css" media="screen and (max-width:800px)"> </head> <body> <div id = "container"> <div class = "col-1"> <canvas id = "canvas"></canvas> </div> <div class = "col-2"> <p><button id = "explode">爆発</button></p> <p>爆発音:<input type="range" min = "0" max="1" step="0.01" value="0.3" id = "range-volume"></p> <select name="choice" id = "explosion-sound"> <option value="explode0" selected>爆発音 0</option> <option value="explode1">爆発音 1</option> <option value="explode2">爆発音 2</option> <option value="explode3">爆発音 3</option> </select> <div id = "scroll"> <p>R(中心部):<input type="range" min = "0" max="255" step="1" id = "range-r"></p> <p>G(中心部):<input type="range" min = "0" max="255" step="1" id = "range-g"></p> <p>B(中心部):<input type="range" min = "0" max="255" step="1" id = "range-b"></p> <p>ぼかし:<input type="range" min = "0" max="100" step="1" id = "range-shadow-blur"></p> <p>R(縁):<input type="range" min = "0" max="255" step="1" id = "range-edge-r"></p> <p>G(縁):<input type="range" min = "0" max="255" step="1" id = "range-edge-g"></p> <p>B(縁):<input type="range" min = "0" max="255" step="1" id = "range-edge-b"></p> <p>描画回数:<input type="range" min = "1" max="20" step="1" id = "range-draw-count"></p> <p>火球が成長する更新回数:<input type="range" min = "1" max="100" step="1" id = "range-growing-max-life"></p> <p>火球が消滅する更新回数:<input type="range" min = "1" max="100" step="1" id = "range-spark-life"></p> <p>火球生成の時間差:<input type="range" min = "0" max="100" step="1" id = "range-spark-delay"></p> <p>火球の生成個数:<input type="range" min = "1" max="100" step="1" id = "range-sparks-count"></p> <p>火球の生成座標をどれだけずらすか:<input type="range" min = "1" max="100" step="1" id = "range-explosion-radius"></p> </div> </div> <div class = "both"></div> </div> <script src= "./index.js"></script> </body> </html> |
レスポンシブウェブデザインにしています。ディスプレイの幅が広いとcol-2はcol-1の右に、狭いと下に表示されます。
style.css
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
#explode { width: 200px; height: 60px; text-align: center; } .plus { width: 50px; height: 25px; margin-left: 20px; text-align: center; } input[type="range"] { vertical-align: middle; margin-right: 10px; } .col-1, .col-2 { float: left; max-width: 360px; width: 100%; } #scroll { width: 100%; height: 300px; overflow: scroll; } .both { clear: both; } .col-2 { margin-left: 30px; } |
スマホのようにディスプレイの幅が狭い場合はcol-1とcol-2を縦に並べます。
style-mobile.css
1 2 3 4 |
.col-1, .col-2 { margin-left: 0px; float: none; } |
グローバル変数と定数
グローバル変数と定数を示します。
index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
// canvasとコンテキスト const $canvas = document.getElementById('canvas'); const ctx = $canvas.getContext('2d'); // canvasのサイズ const CANVAS_WIDTH = 360; const CANVAS_HEIGHT = 360; // 描画に関する設定 const settingExplode = { 'volume':0.3, // 効果音のボリューム 'r':255, // 火球(中心部)の色(rgb) 'g':255, 'b':255, 'edge-r':0, // 火球(縁)の色(rgb) 'edge-g':255, 'edge-b':255, 'shadow-blur':50, // 火球のぼかし 'draw-count':2, // 同じ火球を何回描画しつづけるか(ぼかし部分が強くなる) 'spark-life':24, // 火球の寿命 'growing-max-life':16, // 火球が大きくなりつづける寿命 'spark-delay':10, // 次の火球が出現するまでの時間(ミリ秒) 'sparks-count':10, // ひとつの爆発で出現する火球の数 'explosion-radius':16, // 爆発の中心点と火球が出現する位置の距離の最大値 } let sparks = []; // 火球(Sparkオブジェクト)を格納する配列 |
Sparkクラスの定義
火球状態を更新して描画するためにSparkクラスを定義します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
class Spark{ constructor(x, y){ this.X = x; this.Y = y; this.Radius = 1; this.UpdateCount = 0; this.IsDead = false; } Update(){ // 更新回数がgrowing-max-life以下なら火球を大きくする this.UpdateCount++; if(this.UpdateCount < settingExplode['growing-max-life']) this.Radius += 1; // 更新回数がspark-lifeを超えたら死亡フラグをセットする if(this.UpdateCount > settingExplode['spark-life']) this.IsDead = true; } Draw(){ if(this.IsDead) return; ctx.shadowBlur = settingExplode['shadow-blur']; ctx.shadowColor = '#0ff'; ctx.shadowColor = `rgb(${settingExplode['edge-r']},${settingExplode['edge-g']},${settingExplode['edge-b']})`; ctx.fillStyle = `rgb(${settingExplode['r']},${settingExplode['g']},${settingExplode['b']})`; ctx.beginPath(); ctx.arc(this.X, this.Y, this.Radius, 0, Math.PI * 2); for(let i = 0; i < settingExplode['draw-count']; i++) ctx.fill(); ctx.shadowBlur = 0; } } |
ページが読み込まれたときの処理
ページが読み込まれたときの処理を示します。
canvasのサイズを調整してイベントリスナを追加して更新処理を開始します。
1 2 3 4 5 6 7 8 |
window.onload = () => { $canvas.width = CANVAS_WIDTH; $canvas.height = CANVAS_HEIGHT; document.getElementById('explode').addEventListener('click', () => explode()); // explode関数は後述 initRanges(); // 後述 update(); // 後述 } |
レンジスライダーを操作したら設定の値を変更してその値を項目の近くに表示させます。
HTMLからレンジスライダーに関する情報を読み取り、値の表示と操作用のボタンの表示、イベントリスナの追加をおこないます。settingExplodeのキーとレンジスライダーのid、ボタンのidなどに規則性をもたせることで配列とループ文で簡単に終わらせます。
1 2 3 4 5 |
<p>爆発音:<input type="range" min = "0" max="1" step="0.01" value="0.3" id = "range-volume"></p> // 上のHTMLから // 下を生成する <p>爆発音:<span id="volume">0.3</span><br><input type="range" min="0" max="1" step="0.01" value="0.3" id="range-volume"><button id="minus-volume" class="plus">-</button><button id="plus-volume" class="plus">+</button></p> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
function initRanges(){ const arr = Object.keys(settingExplode); for(let i = 0; i < arr.length; i++){ const $range = document.getElementById('range-' + arr[i]); $range.outerHTML = `<span id = "${arr[i]}"></span><br>` + $range.outerHTML + `<button id = "minus-${arr[i]}" class = "plus">-</button><button id = "plus-${arr[i]}" class = "plus">+</button>`; } for(let i = 0; i < arr.length; i++){ const $range = document.getElementById('range-' + arr[i]); const $value = document.getElementById(arr[i]); const $plus = document.getElementById('plus-' + arr[i]); const $minus = document.getElementById('minus-' + arr[i]); $range.addEventListener('input', () => { settingExplode[arr[i]] = Number($range.value); $value.innerText = settingExplode[arr[i]]; }); $plus.addEventListener('click', () => { const step = Number($range.step); if(settingExplode[arr[i]] + step < $range.max) settingExplode[arr[i]] += step; else settingExplode[arr[i]] = Number($range.max); $range.value = settingExplode[arr[i]]; $value.innerText = round(settingExplode[arr[i]]); }); $minus.addEventListener('click', () => { const step = Number($range.step); if(settingExplode[arr[i]] - step > $range.min) settingExplode[arr[i]] -= step; else settingExplode[arr[i]] = Number($range.min); $range.value = settingExplode[arr[i]]; $value.innerText = round(settingExplode[arr[i]]); }); $range.value = settingExplode[arr[i]]; $value.innerText = settingExplode[arr[i]]; } function round(value){ return Math.round(value * 100) / 100; } } |
更新処理
更新の処理を示します。
canvas全体を黒で塗りつぶして火球を更新して描画します。
1 2 3 4 5 6 7 8 9 |
function update(){ ctx.fillStyle = '#000'; ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); sparks.forEach(spark => spark.Update()); // 更新 sparks = sparks.filter(spark => !spark.IsDead); // 死亡フラグがセットされているオブジェクトを除去 sparks.forEach(spark => spark.Draw()); // 描画 requestAnimationFrame(update); } |
爆発を発生させる
爆発を発生させる処理を示します。
最初に効果音を再生します。セレクトボックスからどの効果音が選択されているのかを調べてAudioオブジェクトを生成します。そのあとvolumeを設定して効果音を再生します。
爆発させるときは火球は何個生成するのか?そのさい時間はどれだけズラすのか?爆発の中心点からどれくらい離れたところに火球を生成するのか?が問題になります。下記コードではそれらの値をsettingExplodeオブジェクトから取得して処理をおこなっています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
async function explode(){ const soundName = document.getElementById('explosion-sound').value; const sound = new Audio('./sounds/' + soundName + '.mp3'); sound.volume = settingExplode['volume']; sound.play(); for(let i = 0; i < settingExplode['sparks-count']; i++){ const radius = settingExplode['explosion-radius']; const dx = Math.random() * radius * 2 - radius; const dy = Math.random() * radius * 2 - radius; sparks.push(new Spark(CANVAS_WIDTH / 2 + dx, CANVAS_HEIGHT / 2 + dy)); await sleep(settingExplode['spark-delay']) } } async function sleep(ms){ return new Promise(resolve => setTimeout(() => resolve(''), ms)); } |