Canvas Confettiというライブラリをつかってwebページ上に紙吹雪を描画しました。ちょっとした事情があってCanvas Confettiを使わずに紙吹雪のようなものを描画する必要に迫られたため、別の方法で紙吹雪のようなものを描画する方法を考えます。
今回はCanvasRenderingContext2D: transform関数を使います。この関数はcanvasのフォームを変更するのに行列を使用します。これによってcanvasを伸縮、回転、移動、傾斜することができます。引数は6つあり、以下の行列のa~fに対応しています。
一方、紙吹雪は四角形の平面で回転しています。3次元の回転体はX軸、Y軸、Z軸を中心にした3つの回転にわけて考えます。
Contents
行列と回転処理
行列を使うと回転処理をおこなう行列は以下のようになっています。
画面は2次元であるなので回転処理をしたあとこれを平面 z = 0 に平行投影した図形を 画面に表示させればよいことになります。そのためには行列の積を計算して①~④を引数a~dにすればよいです。eとfは平行移動のためのものなので別に考えます。
行列を計算するクラスの定義
3×3行列を計算する処理をするのでMat3クラスを定義します。Multi関数は2つの行列の積を計算するためのものです。なんか変な計算ですが、これが行列の積の計算方法です。
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 |
class Mat3 { constructor(v00, v01, v02, v10, v11, v12, v20, v21, v22){ this.Values = [ [v00, v01, v02], [v10, v11, v12], [v20, v21, v22], ]; } GetArray(){ return this.Values; } static Multi(mat1, mat2){ const arr1 = mat1.GetArray(); const arr2 = mat2.GetArray(); const v00 = arr1[0][0] * arr2[0][0] + arr1[0][1] * arr2[1][0] + arr1[0][2] * arr2[2][0]; const v01 = arr1[0][0] * arr2[0][1] + arr1[0][1] * arr2[1][1] + arr1[0][2] * arr2[2][1]; const v02 = arr1[0][0] * arr2[0][2] + arr1[0][1] * arr2[1][2] + arr1[0][2] * arr2[2][2]; const v10 = arr1[1][0] * arr2[0][0] + arr1[1][1] * arr2[1][0] + arr1[1][2] * arr2[2][0]; const v11 = arr1[1][0] * arr2[0][1] + arr1[1][1] * arr2[1][1] + arr1[1][2] * arr2[2][1]; const v12 = arr1[1][0] * arr2[0][2] + arr1[1][1] * arr2[1][2] + arr1[1][2] * arr2[2][2]; const v20 = arr1[2][0] * arr2[0][0] + arr1[2][1] * arr2[1][0] + arr1[2][2] * arr2[2][0]; const v21 = arr1[2][0] * arr2[0][1] + arr1[2][1] * arr2[1][1] + arr1[2][2] * arr2[2][1]; const v22 = arr1[2][0] * arr2[0][2] + arr1[2][1] * arr2[1][2] + arr1[2][2] * arr2[2][2]; return new Mat3(v00, v01, v02, v10, v11, v12, v20, v21, v22); } } |
X軸を中心とした回転画像の描画
まずは、X軸を中心とした回転画像を描画することにします。
HTML部分は以下のようにします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>テスト</title> <meta name = "viewport" content = "width=device-width, initial-scale = 1.0"> </head> <body> <canvas id = "canvas"></canvas> <script src= "./index.js"></script> </body> </html> |
JavaScript部分は上記のMat3クラスの定義に加えて、
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 28 29 30 31 32 33 34 35 36 37 38 |
const CANVAS_WIDTH = 360; const CANVAS_HEIGHT = 360; const BLOCK_SIZE = 150; const $canvas = document.getElementById('canvas'); const ctx = $canvas.getContext('2d'); let angleX = 0; window.onload = () => { $canvas.width = CANVAS_WIDTH; $canvas.height = CANVAS_HEIGHT; update(); } function update(){ ctx.fillStyle = '#000'; ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); angleX += 0.02; const mat = new Mat3(1, 0, 0, 0, Math.cos(angleX), -Math.sin(angleX), 0, Math.sin(angleX), Math.cos(angleX)); const arr = mat.GetArray(); ctx.textBaseline = 'top'; ctx.save(); ctx.transform(arr[0][0], arr[1][0], arr[0][1], arr[1][1], 150, 150); ctx.fillStyle = '#00f'; ctx.fillRect(-BLOCK_SIZE/2, -BLOCK_SIZE/2, BLOCK_SIZE, BLOCK_SIZE); ctx.fillStyle = '#fff'; ctx.font = '150px "MS ゴシック"'; ctx.fillText('鳩', -BLOCK_SIZE/2, -BLOCK_SIZE/2); ctx.restore(); requestAnimationFrame(update); } |
これでX軸を中心に正方形が回転する描画ができるようになりました。
回転をシミュレーションする
これでは面白くないのでX軸、Y軸、Z軸を中心に回転させたりさせなかったり回転速度を変更できるようなものをつくります。
HTML部分
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
<!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> <canvas id = "canvas"></canvas> <div id = "x"><input type="checkbox" id = "checkX"><label for="x">X</label><input type="range" id = "rangeX" min="0" max="0.100" step="0.002"><span id = "valueX"></span></div> <div id = "y"><input type="checkbox" id = "checkY"><label for="y">Y</label><input type="range" id = "rangeY" min="0" max="0.100" step="0.002"><span id = "valueY"></span></div> <div id = "z"><input type="checkbox" id = "checkZ"><label for="z">Z</label><input type="range" id = "rangeZ" min="0" max="0.100" step="0.002"><span id = "valueZ"></span></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 |
#canvas { display:block; } #x, #y, #z { margin-top: 20px; } #rangeX, #rangeY, #rangeZ { margin-left: 20px; width: 250px; vertical-align: middle; } #valueX, #valueY, #valueZ { margin-left: 20px; } |
Mat3クラス
index.js
Mat3クラスは変更ありません。
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 |
class Mat3 { constructor(v00, v01, v02, v10, v11, v12, v20, v21, v22){ this.Values = [ [v00, v01, v02], [v10, v11, v12], [v20, v21, v22], ]; } GetArray(){ return this.Values; } static Multi(mat1, mat2){ const arr1 = mat1.GetArray(); const arr2 = mat2.GetArray(); const v00 = arr1[0][0] * arr2[0][0] + arr1[0][1] * arr2[1][0] + arr1[0][2] * arr2[2][0]; const v01 = arr1[0][0] * arr2[0][1] + arr1[0][1] * arr2[1][1] + arr1[0][2] * arr2[2][1]; const v02 = arr1[0][0] * arr2[0][2] + arr1[0][1] * arr2[1][2] + arr1[0][2] * arr2[2][2]; const v10 = arr1[1][0] * arr2[0][0] + arr1[1][1] * arr2[1][0] + arr1[1][2] * arr2[2][0]; const v11 = arr1[1][0] * arr2[0][1] + arr1[1][1] * arr2[1][1] + arr1[1][2] * arr2[2][1]; const v12 = arr1[1][0] * arr2[0][2] + arr1[1][1] * arr2[1][2] + arr1[1][2] * arr2[2][2]; const v20 = arr1[2][0] * arr2[0][0] + arr1[2][1] * arr2[1][0] + arr1[2][2] * arr2[2][0]; const v21 = arr1[2][0] * arr2[0][1] + arr1[2][1] * arr2[1][1] + arr1[2][2] * arr2[2][1]; const v22 = arr1[2][0] * arr2[0][2] + arr1[2][1] * arr2[1][2] + arr1[2][2] * arr2[2][2]; return new Mat3(v00, v01, v02, v10, v11, v12, v20, v21, v22); } } |
グローバル変数と定数
グローバル変数と定数を示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
// canvasとブロックのサイズ const CANVAS_WIDTH = 360; const CANVAS_HEIGHT = 360; const BLOCK_SIZE = 150; // canvas要素とコンテキスト const $canvas = document.getElementById('canvas'); const ctx = $canvas.getContext('2d'); // チェックボックスにチェックはついているか? let checkX = false; let checkY = false; let checkZ = false; // XYZ軸の回転速度 let valueX = 0; let valueY = 0; let valueZ = 0; // XYZ軸の回転量 let angleX = 0; let angleY = 0; let angleZ = 0; |
ページが読み込まれたときの処理
ページが読み込まれたら初期化の処理をしたあと更新処理を開始します。
1 2 3 4 5 6 7 |
window.onload = () => { $canvas.width = CANVAS_WIDTH; $canvas.height = CANVAS_HEIGHT; initRanges(); update(); } |
レンジスライダーで回転速度を変更できるようにします。またチェックが外れている場合はその軸では回転させません。initRanges関数はそのための処理をおこないます。
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 |
function initRanges(){ const $checkX = document.getElementById('checkX'); const $checkY = document.getElementById('checkY'); const $checkZ = document.getElementById('checkZ'); const $rangeX = document.getElementById('rangeX'); const $rangeY = document.getElementById('rangeY'); const $rangeZ = document.getElementById('rangeZ'); const $valueX = document.getElementById('valueX'); const $valueY = document.getElementById('valueY'); const $valueZ = document.getElementById('valueZ'); // レンジスライダーの右側に設定されている値を表示する // チェックがはずれている場合は設定値は0とする function showValue(){ valueX = checkX ? Number($rangeX.value) : 0; $valueX.innerHTML = valueX; valueY = checkY ? Number($rangeY.value) : 0; $valueY.innerHTML = valueY; valueZ = checkZ ? Number($rangeZ.value) : 0; $valueZ.innerHTML = valueZ; } // チェックボックスの状態が変更されたらレンジスライダーを有効・無効化し設定値を変更・表示する $checkX.onchange = () => { checkX = $checkX.checked; $rangeX.disabled = !checkX; showValue(); } $checkY.onchange = () => { checkY = $checkY.checked; $rangeY.disabled = !checkY; showValue(); } $checkZ.onchange = () => { checkZ = $checkZ.checked; $rangeZ.disabled = !checkZ; showValue(); } // レンジスライダーが動いたら設定値を変更・表示する $rangeX.oninput = () => showValue(); $rangeY.oninput = () => showValue(); $rangeZ.oninput = () => showValue(); // 初期状態 $checkX.checked = false; $checkY.checked = false; $checkZ.checked = false; $rangeX.value = 0; $rangeY.value = 0; $rangeZ.value = 0; $rangeX.disabled = true; $rangeY.disabled = true; $rangeZ.disabled = true; } |
更新処理
更新処理を示します。
valueXだけangleXを増加させます。0の場合は回転させないということなのでangleXには0を代入します。そのあと回転行列を計算してtransform関数にわたす引数を取得します。そのあと描画処理をおこないます。
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 |
function update(){ ctx.fillStyle = '#000'; ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); if(valueX != 0) angleX += valueX; else angleX = 0; if(valueY != 0) angleY += valueY; else angleY = 0; if(valueZ != 0) angleZ += valueZ; else angleZ = 0; const mat0 = new Mat3(1, 0, 0, 0, 1, 0, 0, 0, 1); const mat1 = new Mat3(1, 0, 0, 0, Math.cos(angleX), -Math.sin(angleX), 0, Math.sin(angleX), Math.cos(angleX)); const mat2 = new Mat3(Math.cos(angleY), 0, Math.sin(angleY), 0, 1, 0, -Math.sin(angleY), 0, Math.cos(angleY)); const mat3 = new Mat3(Math.cos(angleZ), -Math.sin(angleZ), 0, Math.sin(angleZ), Math.cos(angleZ), 0, 0, 0, 1); let mat = mat0; if(checkX) mat = Mat3.Multi(mat, mat1); if(checkY) mat = Mat3.Multi(mat, mat2); if(checkZ) mat = Mat3.Multi(mat, mat3); const arr = mat.GetArray(); ctx.textBaseline = 'top'; ctx.save(); ctx.transform(arr[0][0], arr[1][0], arr[0][1], arr[1][1], 150, 150); ctx.fillStyle = '#00f'; ctx.fillRect(-BLOCK_SIZE/2, -BLOCK_SIZE/2, BLOCK_SIZE, BLOCK_SIZE); ctx.fillStyle = '#fff'; ctx.font = '150px "MS ゴシック"'; ctx.fillText('鳩', -BLOCK_SIZE/2, -BLOCK_SIZE/2); ctx.restore(); requestAnimationFrame(update); } |