【JavaScript & Canvas】 投射と跳ね返り
前回 : 等速度運動。
等速度運動やったなら、斜方投射もやっておこうと物理の計算を復習。
作成物
マウスオーバーで円 (= ボール) を作り出すのは前回と同じ。
右方向を0°として、0° ~ 180°の方向にランダムなボールを生成して投射します。
X方向の範囲外部分はループ。
Y方向下側が地面になっていて、衝突するとバウンドします。
技術要素
投射
物理の知識に詳しい記事はいっぱいあるので省略。
コンストラクタで初速を設定し、それを X 方向・Y方向に分割して速度を算出し、更に位置も算出します。
class Point { constructor(x, y, ts) { // 初期位置および初期タイムスタンプ this.first_x = x; this.first_y = y; this.first_ts = ts; // 初速 const frac = Math.random(); this.v0 = (1 - frac) * 0.3 + 0.7; // 0.3 ~ 1.0 // 投射方向 this.direction = Math.random() * Math.PI; // 0 ~ 180° // 加速度 // NOTE: 重力 = 9.8 を使うと速すぎるので適当な小さい値を設定。 this.a = 0.001; (後略) } update(ts) { // 初期時刻からの差分 const ts_diff = ts - this.first_ts; const ts_diff2 = ts_diff * ts_diff; // 2乗 // 速度 // NOTE: x は等速度、y は等加速度で落下。 this.vx = this.v0 * Math.cos(this.direction); this.vy = this.v0 * Math.sin(this.direction) - this.a * ts_diff; // 位置を算出 const dx = this.v0 * Math.cos(this.direction) * ts_diff; const dy = this.v0 * Math.sin(this.direction) * ts_diff - 0.5 * this.a * ts_diff2; // 初期位置に加算 // NOTE: canvas 座標系は y 下方向が正方向 this.x = this.first_x + dx; this.y = this.first_y - dy; ( 後略 ) } }
ループ & 反射
X方向はループ処理にしました。
範囲を超えたら、その分だけ加算 or 減算します。
// x方向 ... 壁にぶつかったらループ while (this.x < 0) this.x += WIDTH; while (this.x > WIDTH) this.x -= WIDTH;
Y方向は反射。
衝突時の反射角度、衝突時速度を求め、衝突した位置を初期位置にして描画することで実現しています。
衝突時速度に反発係数を乗算して、徐々に速度が 0 になるようになっています。
// y方向 ... 反射角度および衝突時距離を算出し、衝突位置より描画を再開 if (this.y > HEIGHT) { // 衝突時の速度から反射角度を算出 this.direction = Math.abs(Math.PI / 2.0 - Math.atan(this.vx, this.vy)); // 衝突時速度 this.v0 = Math.sqrt(this.vx * this.vx + this.vy * this.vy) * REFLECTION; // 反発係数 // 激突位置から再開 this.first_ts = ts this.first_y = HEIGHT; this.first_x = this.x; // 速度が小さくなったら無効化 if (this.v0 < REFLECTION_VMIN) this.enabled = false; }
Canvas の操作とかは前回と一緒なので省略。
コード全体
<div id="canvas_base"></div> <script> // Canvas 紐づけ要素 ID const BASE_ID = "canvas_base"; // 描画エリアサイズ const WIDTH = 500; const HEIGHT = 500; // 反発係数 const REFLECTION = 0.8; // 反射時に速度が一定以下になったら除外 const REFLECTION_VMIN = 0.2; //////////////////// // // 描画する点 // 物理法則に従って落下させる // class Point { constructor(x, y, ts) { // 初期位置および初期タイムスタンプ this.first_x = x; this.first_y = y; this.first_ts = ts; // 有効フラグ // 無効になったら上位モジュールより削除 this.enabled = true; // 表示色 this.color = 'rgb(' + Math.floor(Math.random() * 256) + ',' + Math.floor(Math.random() * 256) + ',' + Math.floor(Math.random() * 256) + ')' // 大きさ const frac = Math.random(); this.size = frac * 20 + 10; // 10 ~ 30 // 初速 // 大きさに反比例 this.v0 = (1 - frac) * 0.3 + 0.7; // 0.3 ~ 1.0 // 投射方向 this.direction = Math.random() * Math.PI; // 0 ~ 180° // 加速度 // NOTE: 重力 = 9.8 を使うと速すぎるので適当な小さい値を設定。 this.a = 0.001; } // 現在位置を取得 update(ts) { // 初期時刻からの差分 const ts_diff = ts - this.first_ts; const ts_diff2 = ts_diff * ts_diff; // 2乗 // 速度 // NOTE: x は等速度、y は等加速度で落下。 this.vx = this.v0 * Math.cos(this.direction); this.vy = this.v0 * Math.sin(this.direction) - this.a * ts_diff; // 位置を算出 const dx = this.v0 * Math.cos(this.direction) * ts_diff; const dy = this.v0 * Math.sin(this.direction) * ts_diff - 0.5 * this.a * ts_diff2; // 初期位置に加算 // NOTE: canvas 座標系は y 下方向が正方向 this.x = this.first_x + dx; this.y = this.first_y - dy; // x方向 ... 壁にぶつかったらループ while (this.x < 0) this.x += WIDTH; while (this.x > WIDTH) this.x -= WIDTH; // y方向 ... 反射角度および衝突時距離を算出し、衝突位置より描画を再開 if (this.y > HEIGHT) { // 衝突時の速度から反射角度を算出 this.direction = Math.abs(Math.PI / 2.0 - Math.atan(this.vx, this.vy)); // 衝突時速度 this.v0 = Math.sqrt(this.vx * this.vx + this.vy * this.vy) * REFLECTION; // 反発係数 // 激突位置から再開 this.first_ts = ts this.first_y = HEIGHT; this.first_x = this.x; // 速度が小さくなったら無効化 if (this.v0 < REFLECTION_VMIN) this.enabled = false; } } } //////////////////// // // Canvas 操作クラス // class CanvasOp { constructor(width, height, base_id = null) { this.width = width; this.height = height; // 描画用 Canvas およびダブルバッファリング用 Canvas を作成 this.canvas = document.createElement("canvas"); this.canvas_buff = document.createElement("canvas"); this.canvas_buff.hidden = true; // 情報表示用 this.text = document.createElement("div"); // サイズ設定 this.canvas.width = width; this.canvas.height = height; this.canvas_buff.width = width; this.canvas_buff.height = height; // canvas を親要素に紐づけ if (base_id == null) { // 未指定なら document.body に紐づける document.body.appendChild(this.canvas); document.body.appendChild(this.canvas_buff); document.body.appendChild(this.text); } else { const base = document.getElementById(base_id); base.appendChild(this.canvas); base.appendChild(this.canvas_buff); base.appendChild(this.text); } // 操作用 Context if (this.canvas.getContext && this.canvas_buff.getContext) { this.context = this.canvas.getContext("2d"); this.context_buff = this.canvas_buff.getContext("2d"); } else { this.context = null; this.context_buff = null; console.error("Error: Canvas not supported."); } // イベント追加 // this.canvas.addEventListener('click', this.onclick.bind(this), false); // 「マウス上にあれば常に」版 this.canvas.addEventListener('mousemove', this.onclick.bind(this), false); // アニメーション用カウント this.count = 0; // クリック位置 this.click_x = null; this.click_y = null; // 点群 this.points = [] } // クリック時イベント onclick(event) { const rect = event.target.getBoundingClientRect(); this.click_x = event.clientX - rect.left; this.click_y = event.clientY - rect.top; } // 更新処理 update(timestamp) { // context 空なら停止 if (this.context == null) { return; } // クリックされていたら点を追加 if (this.click_x != null && this.click_y != null) { this.points.push(new Point(this.click_x, this.click_y, timestamp)) this.click_x = null; this.click_y = null; } ////// /// buff に描画 /// // Clear this.context_buff.clearRect(0, 0, this.width, this.height); // 外枠 this.context_buff.strokeRect(0, 0, this.width, this.height); // animation this.points.forEach(element => { element.update(timestamp); this.context_buff.beginPath(); this.context_buff.arc(element.x, element.y, element.size, 0, 2 * Math.PI, false); this.context_buff.fillStyle = element.color; this.context_buff.fill(); }); // 範囲外に出たものを除外 this.points = this.points.filter((value, index, array) => value.enabled); // 現在のボール数 this.text.innerHTML = "Ball count : " + this.points.length; ////// /// Canvas に転送 /// const data = this.context_buff.getImageData(0, 0, this.width, this.height); this.context.putImageData(data, 0, 0); // Update window.requestAnimationFrame((timestamp) => this.update(timestamp)); } } //////////////////// // // onload // window.onload = function() { let canvas = new CanvasOp(WIDTH, HEIGHT, BASE_ID); canvas.update(0); } </script>
アイキャッチ用
前回と一緒っぽいけど……。