k41531

Rust 🦀 と Wasm のインタラクティブな実装

本記事は、筆者がRustとWasmを学ぶために以下のドキュメントを読み進めた過程を記すものです。今回はボタンやインプットフィールドなどのインタラクティブな部分の実装を行います。
https://rustwasm.github.io/docs/book/game-of-life/interactivity.html

一時停止と再生ボタン

一時停止と再生はJSのみで実装ができる。基本的にただのJSなので詳細は省略します。
cancelAnimationFrameとrequestAnimationFrameを使う。

<button id="play-pause"></button>
let animationId = null;

const renderLoop = () => {
  drawGrid();
  drawCells();

  universe.tick();

  animationId = requestAnimationFrame(renderLoop);
};

const isPaused = () => {
  return animationId === null;
};

const play = () => {
  playPauseButton.textContent = "⏸";
  renderLoop();
};

const pause = () => {
  playPauseButton.textContent = "▶️";
  cancelAnimationFrame(animationId);
  animationId = null;
};

const playPauseButton = document.getElementById("play-pause");

playPauseButton.addEventListener("click", event => {
  if (isPaused()) {
    play();
  } else {
    pause();
  }
});

play();

クリックでセルの状態を変える

サンプルではCell構造体を使っているが、ここではFixedBitSetを使ったサンプルコードを記載します。
not演算子をつければいいだけなので比較的簡単に実装できました。

pub fn toggle_cell(&mut self, row: u32, column: u32) {
    let idx = self.get_index(row, column);
    self.cells.set(idx,!self.cells[idx]);
}

次のコードはCanvas上の座標を、行列に変換するためのものです。ここでも特別な実装は特に行っていません。

canvas.addEventListener("click", event => {
  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.toggle_cell(row, col);

  drawGrid();
  drawCells();
});

練習問題

アニメーションの速度を変更できる様にする。

<input name="speed" id="speed" type="number"> </input>
const speedField = document.getElementById("speed");
    speedField.addEventListener("change", event => {
    speed = parseInt(event.target.value);
});

Promiseを使って、JSでsleepを実装する方法を見つけたのでそれを使用しました。
awaitを使うため、renderLoop関数をasyncにしました。

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

...

const renderLoop = async () => {
    drawGrid();
    drawCells();
    universe.tick();
    await sleep(speed);
    animationId = requestAnimationFrame(renderLoop);
};

ランダムに初期化するボタンと全てのセルを死んだ状態にするリセットボタンを作る。

<button id="reset">RESET</button>
<button id="random">RANDOM</button>
const resetButton = document.getElementById("reset");
resetButton.addEventListener("click", event => {
  universe.reset_cells();
});

const randomButton = document.getElementById("random");
randomButton.addEventListener("click", event => {
  universe.set_random_cells();
});
pub fn reset_cells(&mut self) {
    let size = (self.width * self.height) as usize;
    self.cells = FixedBitSet::with_capacity(size);
    for i in 0..size {
        self.cells.set(i, false);
    }
}

pub fn set_random_cells(&mut self) {
    let size = (self.width * self.height) as usize;
    self.cells = FixedBitSet::with_capacity(size);
    for i in 0..size {
        self.cells.set(i, js_sys::Math::random() < 0.5);
    }
}

Ctrl(cmd) + クリックでグライダーを生成する。

pub fn create_glider(&mut self, row: u32, column: u32) {
    let glider = vec![0, 0, 1, 1, 0, 1, 0, 1, 1];
    let height = 3;
    let width = 3;
    for glider_row in 0..height {
        for glider_col in 0..width {
            let idx = self.get_index(row+glider_row, column+glider_col);
            let glider_idx = (glider_row*height + glider_col) as usize;
            self.cells.set(idx, glider[glider_idx] != 0);
        }
    }
}
if(event.ctrlKey || event.metaKey){
    universe.create_glider(row, col); 
}