前回、JavaScript transform関数で空間上で回転した平面を描画することができるようになったので、これを利用して既存のライブラリを使わずにcanvas上に紙吹雪を描画する方法を考えます。Canvas Confettiと同じようにある地点からクラッカーのように紙吹雪を飛ばして舞い落ちるような描画をさせることにします。
Contents
最高高度と到達時間から初速と重力加速度を算出する
クラッカーのように紙吹雪を飛ばすときに問題になるのが、初速と重力加速度です。実際には飛ばしたときに、どこまで飛ぶか、その位置まで飛ぶにはどれくらいの時間がかかるかです。
重力加速度は9.8m/s2ですが、ここでは①真上に飛ばした場合、最高点の座標と②最高点に達するまでにかかる時間のふたつを指定したらそのときに必要な初速と重力加速度を取得できるようにしておきたいです。
初速v0のときt秒後の速度vと位置sの公式は以下のようになります。
v = v0 – g * t
s = v0 * t – 1 / 2 * g * t * t
最高点に達するのがT秒後とすれば
0 = v0 – g * T ⇔ v0 = g * T
となります。t = T と v0 = g * T を s = の式に代入すると
s = g * T * T – 1 / 2 * g * T * T
となり、最高点の座標がgとTで表わすことができます。s = 1 / 2 * g * T * T です。
もし最高点の座標がSとして与えられているのであれば、g と v0 は S と T で表わすことができます。
S = 1 / 2 * g * T * T なので g = 2 * S / T / T です。また v0 = g * T なので v0 = 2 * S / T です。
これらの単位は初速は「メートル/秒」、加速度は「メートル/秒/秒」です。知りたいのは1更新あたりの値です。1秒間に60回更新されるので、1更新あたりでみた場合の重力加速度は 2 * S / T / T / 3600 であり、初速は 2 * S / T / 60 です。
Webページ上に紙吹雪を表示する
実際に紙吹雪を描画してみることにします。
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 |
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>紙吹雪のテスト</title> <meta name = "viewport" content = "width=device-width, initial-scale = 1.0"> <style> #shot { margin-top: 20px; width: 200px; height: 50px; display: block; } </style> </head> <body> <canvas id = "canvas"></canvas> <button id = "shot">発射</button> <script src= "./index.js"></script> </body> </html> |
グローバル変数と定数
グローバル変数と定数を示します。
index.js
1 2 3 4 5 6 7 8 9 10 11 |
// canvasと紙吹雪のサイズ const CANVAS_WIDTH = 360; const CANVAS_HEIGHT = 360; const BLOCK_SIZE = 16; // canvas要素とコンテキスト const $canvas = document.getElementById('canvas'); const ctx = $canvas.getContext('2d'); // 後述するPieceオブジェクトを格納する配列 let pieces = []; |
Mat3クラスは前回とまったく同じものを使用します。
Pieceクラスの定義
Pieceクラスを定義します。まずコンストラクタを示します。
引数から紙吹雪の初期座標や初速、加速度を設定しますが、初速と加速度は乱数をつかってばらつきを持たせます。
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 |
class Piece { constructor(){ this.X = CANVAS_WIDTH / 2; // 初期座標 this.Y = CANVAS_HEIGHT; // 1更新あたりのY方向の加速度 const maxHeight = CANVAS_HEIGHT; const time = 1; this.AY = 2 * maxHeight / time / time / 3600; // 加速度 // 初速 let v0 = -2 * maxHeight / time / 60; const magnification = Math.random() * 0.1 + 0.95; // 0.95 ~ 1.05 v0 *= magnification; // X方向Y方向それぞれの初速を計算して設定する const angle = Math.PI / 2 + (Math.random() * Math.PI / 6 - Math.PI / 12); this.VY = v0 * Math.sin(angle); this.VX = v0 * Math.cos(angle); // 初期回転量 this.RX = Math.random() * Math.PI * 2; this.RY = Math.random() * Math.PI * 2; this.RZ = Math.random() * Math.PI * 2; // 回転速度 this.VRX = 0.02 + Math.random() * 0.02; this.VRY = 0.02 + Math.random() * 0.02; this.VRZ = 0.02 + Math.random() * 0.02; let colors = ['#f00', '#0f0', '#00f', '#ff0', '#0ff', '#f0f']; const r = Math.floor(Math.random() * colors.length); this.Color = colors[r]; // 空気抵抗によって一定速度で落下する const maxSpeed = 1.5; this.SlowDown = false; this.SlowDownSpeed = maxSpeed + Math.random() * 0.5; this.IsDead = false; } } |
加速度を速度に加算して紙吹雪の座標と回転角を求めます。また最大落下速度が指定されている場合は空気抵抗によって最大落下速度以上は速くならないようにします。また空気抵抗をうけているときは左右にヒラヒラと移動させながら落下させます。またY座標がCANVAS_HEIGHTよりも下になったら死亡フラグをセットします。
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 |
class Piece { Update(){ this.VY += this.AY; // this.SlowDownSpeedを超えたら加速しない if(!this.SlowDown && this.VY >= this.SlowDownSpeed){ this.SlowDown = true; this.IVX = this.VX; this.CR = Math.random() * 0.2; this.R = 0; } // this.SlowDownSpeedを超えたら左右にヒラヒラ落下させる if(this.SlowDown){ this.VY = this.SlowDownSpeed; this.R += this.CR; this.VX = this.IVX * Math.cos(this.R); } this.X += this.VX; this.Y += this.VY; this.RX += this.VRX; this.RY += this.VRY; this.RZ += this.VRZ; if(this.Y > CANVAS_HEIGHT) this.IsDead = true; } } |
描画処理をしめします。回転量から回転行列を取得してtransform関数に渡す引数を求めます。そのあと描画処理をおこないます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
class Piece { Draw(){ const mat0 = new Mat3(1, 0, 0, 0, 1, 0, 0, 0, 1); const mat1 = new Mat3(1, 0, 0, 0, Math.cos(this.RX), -Math.sin(this.RX), 0, Math.sin(this.RX), Math.cos(this.RX)); const mat2 = new Mat3(Math.cos(this.RY), 0, Math.sin(this.RY), 0, 1, 0, -Math.sin(this.RY), 0, Math.cos(this.RY)); const mat3 = new Mat3(Math.cos(this.RZ), -Math.sin(this.RZ), 0, Math.sin(this.RZ), Math.cos(this.RZ), 0, 0, 0, 1); let mat = mat0; mat = Mat3.Multi(mat, mat1); mat = Mat3.Multi(mat, mat2); mat = Mat3.Multi(mat, mat3); const arr = mat.GetArray(); ctx.save(); ctx.transform(arr[0][0], arr[1][0], arr[0][1], arr[1][1], this.X, this.Y); ctx.fillStyle = this.Color; ctx.fillRect(-BLOCK_SIZE/2, -BLOCK_SIZE/2, BLOCK_SIZE, BLOCK_SIZE); ctx.restore(); } } |
ページが読み込まれたときの処理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
window.onload = () => { $canvas.width = CANVAS_WIDTH; $canvas.height = CANVAS_HEIGHT; document.getElementById('shot').addEventListener('click', () => shot()); update(); } function update(){ ctx.fillStyle = '#000'; ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); pieces.forEach(piece => piece.Update()); pieces = pieces.filter(piece => !piece.IsDead); pieces.forEach(piece => piece.Draw()); requestAnimationFrame(update); } function shot(){ for(let i = 0; i < 32; i++) pieces.push(new Piece()); } |