JavaScriptでレーザービームのようなエフェクトを作ります。レーザービームをただの直線ではなく周囲をボンヤリと明るくさせます。
前回、JavaScriptでレーザービームのようなエフェクトを作りましたが、描画の回数が多くなると処理に時間がかかるという問題がありました。
描画回数を大きな値にして発射ボタンを連打すると極端に処理速度が遅くなります。
今回は同じレーザービームを何度も生成するのではなく、一度生成されたものを再利用することで処理にかかる時間を短縮します。またレーザービームの太さやshadowBlurの値、描画回数を変更することでどのような描画がおこなわれるのかをシミュレーションできるようなものをつくります。
これだと描画回数を大きな値にして発射ボタンを連打しても処理速度はそんなに遅くはなりません。
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 |
<!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"> </head> <body> <div id = "container"> <div class = "col"> <canvas id = "canvas"></canvas> </div> <div class = "col"> <div id = "update-count"></div> <p><button id = "shot">発射</button></p> <p>shadowBlurの値:<input type="range" min = "1" max="100" step="1" value="1" id = "range-shadow-blur"><span id = "shadow-blur"></span></p> <p>レーザーの太さ:<input type="range" min = "1" max="10" step="1" value="1" id = "range-laser-width"><span id = "laser-width"></span></p> <p>描画回数:<input type="range" min = "1" max="100" step="1" value="1" id = "range-draw-count"><span id = "draw-count"></span></p> <p>発射音:<input type="range" min = "0" max="1" step="0.01" value="0.3" id = "range-volume"><span id = "volume"></span></p> <p><input type="checkbox" id = "head-mark"><label for="head-mark">レーザーの先端にマークをつける:</label></p> </div> <div class = "both"></div> </div> <script src= "./index.js"></script> </body> </html> |
style.css
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
.col { float: left; max-width: 400px; width: 100%; } .both { clear: both; } #shot { width: 200px; height: 60px; text-align: center; } input[type="range"] { vertical-align: middle; margin-right: 10px; } |
グローバル変数と定数
グローバル変数と定数を示します。
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 |
const $canvas = document.getElementById('canvas'); const ctx = $canvas.getContext('2d'); const playerImage = new Image(); // プレイヤー(レーザービームの発射元)のイメージ // canvasのサイズ const CANVAS_WIDTH = 360; const CANVAS_HEIGHT = 360; const PLAYER_SIZE = 48; // プレイヤーのサイズ // レーザービームの長さ、色、速度 const LASER_LENGTH = 100; const LINE_COLOR = '#0ff'; const LASER_SPEED = 8; let shadowBlur = 32; // 設定するshadowBlurの値 let laserWidth = 2; // レーザーの幅 let drawCount = 8; // ぼかしの描画を繰り返す回数 let volume = 0.3; // 効果音のボリューム let updateCount = 0; // 更新回数 let playerAngle = 0; // プレイヤーの向き let lasers = []; // Laserオブジェクトを格納する配列 |
Laserクラスの定義
レーザーを移動し描画するためにLaserクラスを定義します。
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 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
class Laser{ constructor(sx, sy, rad, drawCount){ // レーザーの先端の座標(ゲームに使う場合はここに当たり判定をおく) this.HeadX = sx; this.HeadY = sy; // レーザーの発射方向 this.Rad = rad; // XY方向の移動速度 this.VX = LASER_SPEED * Math.cos(rad); this.VY = LASER_SPEED * Math.sin(rad); // 発射されたあと移動した距離 this.MovingDistance = 0; this.Image = new Image(); this.shadowBlur = shadowBlur; // レーザービームを描画しそのイメージを保存する // そのためにまずcanvasを生成し、コンテキストを取得する const $tempCanvas = document.createElement('canvas'); const tempCtx = $tempCanvas.getContext('2d'); // canvasの大きさ(レーザー本体とぼかし部分がはいる大きさにする) $tempCanvas.width = LASER_LENGTH + this.shadowBlur * 2; $tempCanvas.height = this.shadowBlur * 2; // 線の幅、色とぼかしの色と範囲を設定 tempCtx.lineWidth = laserWidth; tempCtx.strokeStyle = LINE_COLOR; tempCtx.shadowColor = LINE_COLOR; tempCtx.shadowBlur = this.shadowBlur; // canvasにレーザーを描画する for(let i = 0; i < drawCount; i++){ tempCtx.beginPath(); tempCtx.moveTo(this.shadowBlur, this.shadowBlur); tempCtx.lineTo(LASER_LENGTH + this.shadowBlur, this.shadowBlur); tempCtx.stroke(); } tempCtx.shadowBlur = 0; // canvasに描画されたイメージをthis.Imageに保存する const dataUrl = $tempCanvas.toDataURL(); this.Image.src = dataUrl; } Update(){ this.HeadX += this.VX; this.HeadY += this.VY; this.MovingDistance += Math.sqrt(Math.pow(this.VX, 2) + Math.pow(this.VY, 2)); } Draw(){ // レーザーの発射地点より後ろにある部分は描画しない // レーザーの移動距離が長さに満たない場合は、満たない後ろの部分は描画しない let short = 100 - this.MovingDistance; if(short < 0) short = 0; // レーザーの先端がある座標と発射方向、イメージの大きさから // 描画する適切な座標を求めて描画する // (this.HeadX, this.HeadY)にレーザー本体の先端部分がくるようにする ctx.save(); ctx.translate(this.HeadX, this.HeadY); ctx.rotate(this.Rad); ctx.translate(-this.HeadX, -this.HeadY); const x = this.HeadX - LASER_LENGTH - this.shadowBlur; const y = this.HeadY - this.shadowBlur; const w = this.Image.width; const h = this.Image.height; ctx.drawImage(this.Image, x+short, y, w-short, h); ctx.restore(); } } |
ページが読み込まれたときの処理
ページが読み込まれたときにおこなわれる処理を示します。
レンジスライダーに初期値をセットするとともに、これを操作したら設定されている値を変更できるようにします。そのあと更新処理を開始します。
1 2 3 4 5 6 7 8 9 10 11 |
window.onload = () => { $canvas.width = CANVAS_WIDTH; $canvas.height = CANVAS_HEIGHT; playerImage.src = './images/player.png'; document.getElementById('shot').addEventListener('click', () => shot()); // shot関数は後述 initRanges(); // 後述 update(); // 後述 } |
レンジスライダーに関する初期化をする処理を示します。
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 |
function initRanges(){ const $rangeShadowBlur = document.getElementById('range-shadow-blur'); const $shadowBlur = document.getElementById('shadow-blur'); $rangeShadowBlur.addEventListener('input', () => { shadowBlur = Number($rangeShadowBlur.value); $shadowBlur.innerText = shadowBlur; }); $rangeShadowBlur.value = shadowBlur; $shadowBlur.innerText = shadowBlur; const $rangeLaserWidth = document.getElementById('range-laser-width'); const $laserWidth = document.getElementById('laser-width'); $rangeLaserWidth.addEventListener('input', () => { laserWidth = Number($rangeLaserWidth.value); $laserWidth.innerText = laserWidth; }); $rangeLaserWidth.value = laserWidth; $laserWidth.innerText = laserWidth; const $rangeDrawCount = document.getElementById('range-draw-count'); const $drawCount = document.getElementById('draw-count'); $rangeDrawCount.addEventListener('input', () => { drawCount = Number($rangeDrawCount.value); $drawCount.innerText = drawCount; }); $rangeDrawCount.value = drawCount; $drawCount.innerText = drawCount; const $rangeVolume = document.getElementById('range-volume'); const $volume = document.getElementById('volume'); $rangeVolume.addEventListener('input', () => { volume = $rangeVolume.value; $volume.innerText = volume; }); $rangeVolume.value = volume; $volume.innerText = volume; } |
発射時の処理
発射ボタンがクリックされたときの処理を示します。
Laserオブジェクトを生成して配列に格納します。発射地点はcanvasの中心です。発射角度はグローバル変数 playerAngleからわかります。発射時には効果音も鳴らします。
1 2 3 4 5 6 |
function shot(){ lasers.push(new Laser(CANVAS_WIDTH / 2, CANVAS_HEIGHT / 2, playerAngle, drawCount)); const sound = new Audio('./sounds/shot.mp3'); sound.volume = volume; sound.play(); } |
更新処理と描画処理
更新処理と描画処理を示します。
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 |
function update(){ ctx.fillStyle = '#000'; ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); lasers.forEach(laser => laser.Update()); // canvasの範囲外に飛んでいったオブジェクトは配列から削除する lasers = lasers.filter(laser => laser.HeadX > -LASER_LENGTH && laser.HeadX < CANVAS_WIDTH + LASER_LENGTH); lasers = lasers.filter(laser => laser.HeadY > -LASER_LENGTH && laser.HeadY < CANVAS_HEIGHT + LASER_LENGTH); lasers.forEach(laser => laser.Draw()); // チェックボックスがチェックされていたらレーザーの先端部に●を描画する if(document.getElementById('head-mark').checked){ lasers.forEach(laser => { ctx.fillStyle = '#ff0'; ctx.beginPath(); ctx.arc(laser.HeadX, laser.HeadY, 10, 0, Math.PI * 2); ctx.fill(); }); } // プレイヤーの向きを変更し、プレイヤーを描画する playerAngle = updateCount * 0.01; drawPlayer(playerAngle); // 更新回数を表示する(デバッグ用) updateCount++; document.getElementById('update-count').innerText = '更新回数:' + updateCount; requestAnimationFrame(update); } |
canvasの中央にプレイヤーを描画する処理を示します。
1 2 3 4 5 6 7 8 9 10 11 |
function drawPlayer(rad){ ctx.save(); ctx.translate(CANVAS_WIDTH/ 2, CANVAS_HEIGHT/ 2); ctx.rotate(rad) ctx.translate(-CANVAS_WIDTH/ 2, -CANVAS_HEIGHT/ 2); const x = (CANVAS_WIDTH - PLAYER_SIZE) / 2; const y = (CANVAS_HEIGHT - PLAYER_SIZE) / 2; ctx.drawImage(playerImage, x, y, PLAYER_SIZE, PLAYER_SIZE); ctx.restore(); } |