Rust and WebAssembly Game of Life - Adding Interactive -
Rust and WebAssembly book の 4.6. Debugging に引き続き、 4.7. Adding Interactive に進みます。
マウスインタラクティブ
アニメーションをマウスで操作できると楽しいですよね。
今回はアニメーションの「再生」「停止」ボタンを追加しましょう、というチュートリアルでした。これ自体は特に難所もなく実装出来ました。
これだけでは面白く無いので、何か追加したいと思います。ちなみにチュートリアルでは Exercises として「1フレーム間の世代数指定」「リセットボタン」「Ctrl+ClickでGliderを、Shift+ClickでPulserを追加(Glider/Pulserは Game of Life の特定の形状を指すものですね)」が用意されていましたが、今回は「マウスボタンを押下した状態でドラッグすると、ドラッグした場所をAliveにする」ことにします。
Rust lib.rs 側の追記
impl Cell に set_alive (とset_dead) 関数を追加します。toggle はチュートリアルで作成した関数です。また、JavaScriptから見えるトレイトに set_alive / set_dead を呼び出す関数も定義しておきます。
impl Cell {
fn set_alive(&mut self) {
*self = Cell::Alive;
}
fn set_dead(&mut self) {
*self = Cell::Dead;
}
}
#[wasm_bindgen]
impl Universe {
pub fn set_cell_alive(&mut self, row: u32, column: u32) {
let idx = self.get_index(row, column);
self.cells[idx].set_alive();
}
pub fn set_cell_dead(&mut self, row: u32, column: u32) {
let idx = self.get_index(row, column);
self.cells[idx].set_dead();
}
}
JavaScript index.js 側の追記
Canvas のイベントリスナに mousedown / mouseup / mousemove があるので、mousedown:フラグセット / mousemove:通った場所をAliveにセット / mousedown:フラグクリア という感じで実装しました。 mousemove 内の関数はチュートリアルの click イベントリスナの流用です。
let isDragging = false;
canvas.addEventListener("mousedown", event => {
isDragging = true;
});
canvas.addEventListener("mouseup", event => {
isDragging = false;
});
canvas.addEventListener("mousemove", event => {
if (isDragging === true) {
const boundingRect = canvas.getBoundingClientRect();
const scaleX = canvas.width / boundingRect.width;
const scaleY = canvas.height / boundingRect.height;
const canvasLeft = (event.clientX - boundingRect.left) * scaleX;
const canvasTop = (event.clientY - boundingRect.top) * scaleY;
const row = Math.min(Math.floor(canvasTop / (CELL_SIZE + 1)), height - 1);
const col = Math.min(Math.floor(canvasLeft / (CELL_SIZE + 1)), width - 1);
universe.set_cell_alive(row, col);
drawGrid();
drawCells();
}
});
また、 const pause() 内も以下のように変更しました。アニメーション停止して、マウスでcanvasをクリックすると描画が1世代進む(pauseした瞬間は内部データは更新済、描画が未実行という状態になる模様)ので、pauseした瞬間も draw することとしました。
const pause = () => {
playPauseButton.textContent = "▶";
cancelAnimationFrame(animationId);
animationId = null;
drawGrid(); // 追加
drawCells(); // 追加
};
マウスでグリグリ
実装し、マウスでグリグリしている様子を下図に掲載します。マウスドラッグで経由したセルがAliveになっているのがわかります。それっぽいものが出来ました。
課題
お気づきの方もいらっしゃるかもしれませんが、今の実装ではところどころ怪しい動きをしています。今回は修正しませんが。。。
1. 素早くマウスドラッグすると、途中でラインが切れます。
これはドラッグ速度と比較してイベントリスナーの周期が長過ぎることに起因しています。ちゃんとするなら、イベントリスナ実行ごとに2点間にラインを引く、といったことをする必要がありますね。
2. ドラッグが終わり、マウスボタンから手を離した箇所がAliveだと、離した瞬間に Dead になります。これはイベントリスナーのclickがまだ有効となっていることに起因すると思われます。clickはマウスがDown/Upされて初めて認識されるものだとすれば、Up時にセルのAlive/Deadが反転する関数が呼び出されています。ドラッグが実装されているので、clickは不要になりますね。
おわりに
今回も大きなつまづきポイントがありませんでした。そんなに難しいことをやっていないということと、ちょっと Rust and WebAssembly に慣れてきた、ということはあるかもしれません。
最後まで読んでいただき、ありがとうございました。