WebAssembly でセルオートマトン

　WebAssemblyの練習に、セル・オートマトンの有名な問題ライフゲーム(Game of Life)を実装してみました。


## ライフゲームのルール
　[Wikipediaより](https://ja.wikipedia.org/wiki/%E3%83%A9%E3%82%A4%E3%83%95%E3%82%B2%E3%83%BC%E3%83%A0)、典型的と思われるルールを採用しました。

>  誕生
 　死んでいるセルに隣接する生きたセルがちょうど3つあれば、次の世代が誕生する。
   生存
　生きているセルに隣接する生きたセルが2つか3つならば、次の世代でも生存する。
過疎
　生きているセルに隣接する生きたセルが1つ以下ならば、過疎により死滅する。
過密
　生きているセルに隣接する生きたセルが4つ以上ならば、過密により死滅する。

## 結果

 まず出力結果から。ここで、nはキャンバス１辺に含まれるセルの数。初期条件は、市松模様からスタートしています。デモは https://cellular-automaton-webassembly.herokuapp.com/ にあげてあります。

> ### n = 20 
[![cell20](https://raw.githubusercontent.com/snst-lab/cellular-automaton-webassembly/master/doc/img/cell20.gif)]()

> ### n = 50
[![cell50](https://raw.githubusercontent.com/snst-lab/cellular-automaton-webassembly/master/doc/img/cell50.gif)]()

> ### n = 100
[![cell100](https://raw.githubusercontent.com/snst-lab/cellular-automaton-webassembly/master/doc/img/cell100.gif)]()



## Cargo.toml
主に３つのcrateを読ませます。
> - wasm-bindgen - JavaScript連携
> - web-sys - HtmlElementの操作に使用
> - lazy_static - セルの状態等を格納するベクトルをグローバル変数化し、クロージャー内で使用するために使用


```toml
[package]
name = "wasm"
version = "0.1.0"
authors = ["snst.lab <snst.lab@gmail.com>"]
edition = "2018"

[lib]
crate-type = ["cdylib"]

[dependencies]
lazy_static = "0.2.1"

[dependencies.wasm-bindgen]
version = "0.2.45"

[dependencies.web-sys]
version = "0.3.4"
features = [
  'Document',
  'Window',
  'Element',
  'Node',
  'HtmlElement',
  'DocumentFragment',
  'CssStyleDeclaration',
  'Event',
  'EventTarget',
  'MouseEvent',
]
```
<br>

## Rust側

とりあえず主要箇所のみ抜粋。ソースは[GitHub](https://github.com/snst-lab/cellular-automaton-webassembly)にあげています。

#### ヘッダー部分
　ポイントとして、`Rc`と`RefCell`を`request_animation_frame`の呼び出しに、`RwLock`を`lazy_static`に使用します。

```rust
extern crate wasm_bindgen;

use std::rc::Rc;
use std::cell::RefCell;
use web_sys::{HtmlElement, DocumentFragment};
use wasm_bindgen::{prelude::*, JsCast};

#[macro_use]
extern crate lazy_static;
use std::sync::RwLock;
```

#### グローバル変数の定義

　`Vec<HtmlElement>`を`RwLock`に渡すと、以下のエラーとなります。

> error[E0277]: `*mut u8` cannot be sent between threads safely
> help: within `web::web_sys::HtmlElement`, the trait `std::marker::Send` is not implemented for `*mut u8`

回避策として、`Vec<String>`を`RwLock`に渡して各セル要素のクラス名を保持しています。

```rust
lazy_static! {
    static ref CELLS: RwLock<Vec<String>> = RwLock::new(Vec::new()); //セル要素のクラス名を保持するベクトル
    static ref LIFE: RwLock<Vec<u16>> = RwLock::new(Vec::new());　//セルの状態を保持するベクトル
    static ref LIFE_TEMP: RwLock<Vec<u16>> = RwLock::new(Vec::new()); //セルの状態を一時保管するベクトル
    static ref RUNNING: RwLock<bool> = RwLock::new(false);  //アニメーションの稼働状態を保持
}
```

#### 構造体の定義

　wasm-bindgenの作法で書いていきます。ちなみに、`Document::query_selector()`や`Window::request_animation_frame()`は`web-sys`で用意されている関数を[ラップした関数](https://github.com/snst-lab/cellular-automaton-webassembly/blob/master/src/public/wasm/src/web.rs)です。

```rust

#[wasm_bindgen]
pub struct CellularAutomaton{
    canvas: HtmlElement,  //描画範囲のHTML要素。HtmlElementはCopy Treatを実装していないため、pubは外す
    pub n: isize,  //描画範囲１辺に含まれるセルの数
    pub N: isize,  //全セル数
    pub size_of_cell: f64,  //セルの幅(px)
}

#[wasm_bindgen]
impl CellularAutomaton{
    /**
     コンストラクタで各プロパティを初期化。N, size_of_cellは計算で求めるため、とりあえずの初期値
    */
    #[wasm_bindgen(constructor)]
    pub fn new() -> CellularAutomaton {
        CellularAutomaton { 
            canvas: Document::query_selector(".canvas"),
            n : 20,
            N : 400,
            size_of_cell:10.0
         }
    }
    /**
      N, size_of_cellの計算、各メソッドの呼び出し
    */
    pub fn start(&mut self) -> Result<(), JsValue>{
        self.N = self.n.pow(2);
        self.size_of_cell = self.canvas.client_width() as f64 /self.n as f64;
        CellularAutomaton::draw_canvas(self).expect("failed to draw canvas");
        CellularAutomaton::initialize(self).expect("failed to initialize");
        Ok(())
    }
    /**
      Canvasの描画。DocumentFragmentを使って一気に追加。
    */
    fn draw_canvas(&mut self) -> Result<(), JsValue> {
        self.canvas.style().set_property("height", &(self.canvas.client_width().to_string()+"px"))?;
        let fragment : DocumentFragment = DocumentFragment::new().unwrap();
        /**
          各グローバル変数を書き込み用に呼び出し。
        */
        let mut life = LIFE.write().unwrap();
        let mut life_temp = LIFE_TEMP.write().unwrap();
        let mut cells = CELLS.write().unwrap();
        
        for i in 0..(self.N as usize){
            let cell:HtmlElement = Document::create_element("div");
            let class_name:String = "cell".to_owned() + &i.to_string();
            cell.set_class_name(&("cell ".to_owned()+&class_name));
            cell.style().set_property("width", &(self.size_of_cell.to_string() + "px"))?;
            cell.style().set_property("height", &(self.size_of_cell.to_string() + "px"))?;
            fragment.append_child(&cell)?;
            /**
              まず全セルを0で初期化。
            */
            (*life).push(0);
            (*life_temp).push(0);
            (*cells).push(".".to_owned() + &class_name);
        }
        self.canvas.append_child(&fragment);
        Ok(())
    }
    /**
      初期状態として市松模様を与え、アニメーションスタート
    */
    fn initialize(&self) -> Result<(), JsValue>{
        let mut life = LIFE.write().unwrap();
        let mut life_temp = LIFE_TEMP.write().unwrap();
        let cells = CELLS.write().unwrap();
        
        for i in 0..(self.N as usize){
            if ((i/(self.n as usize))%2==0 && i % 2 == 0) || ((i/(self.n as usize))%2==1 && i % 2 == 1){
                (*life)[i] = 1;
                (*life_temp)[i] = 1;
                let cell:HtmlElement = Document::query_selector(&(*cells)[i as usize]);
                cell.style().set_property("background-color", "deeppink").expect("failed to set property");
            }
        }
        
        let mut running = RUNNING.write().unwrap();
        (*running) = true;
        CellularAutomaton::run(self.n,self.N);
        Ok(())
    }

    /**
      request_animation_frameで15フレーム毎（1秒に約4回）に世代遷移のメソッドevaluateを呼び出す。
　　　ブール値runningは中断や再開をコントロールするためのフラグとして使用。
    */
    fn run(n:isize, N:isize) -> Result<(), JsValue>{
        let f = Rc::new(RefCell::new(None));
        let g = f.clone();
        
        let mut frame = 0;
        *g.borrow_mut() = Some(Closure::wrap(Box::new(move || {
            let runnning = RUNNING.read().unwrap();
            if *runnning {
                if frame % 15 == 0 {CellularAutomaton::evaluate(n,N).expect("failed to evaluate");}
                frame += 1;
                Window::request_animation_frame(f.borrow().as_ref().unwrap());
            }
        }) as Box<FnMut()>));
        Window::request_animation_frame(g.borrow().as_ref().unwrap());
        Ok(())
    }
    /**
      世代遷移のメソッド
    */
    fn evaluate(n:isize, N:isize) -> Result<(), JsValue>{
        /**
          cellsを読み取り専用で、lifeとlife_tempは書き込み用に呼び出す。
        */
        let cells = CELLS.read().unwrap();
        let mut life = LIFE.write().unwrap();
        let mut life_temp = LIFE_TEMP.write().unwrap();
        /**
         　セルiの周囲の生きたセルの数から、上記ルールに従って次世代のセルの生死を判定
        */
        for i in 0..N {
			let top_right: isize = i - n + 1;
			let top: isize = i - n;
			let top_left: isize = i - n - 1;
			let left: isize = i - 1;
			let bottom_left: isize = i + n - 1;
			let bottom: isize = i + n;
			let bottom_right: isize = i + n + 1;
			let right: isize = i + 1;
            /**
            　around  = セルiの周囲の生きたセルの数
            */
            let around : u16 =
				(if top_right < 0 { 0 } else { (*life)[top_right as usize] } ) +
				(if top < 0 { 0 } else { (*life)[top as usize] } ) +
				(if top_left < 0 { 0 } else { (*life)[top_left as usize] } ) +
				(if left < 0 { 0 } else { (*life)[left as usize] } ) +
				(if bottom_left >= N  { 0 } else { (*life)[bottom_left as usize] } ) +
				(if bottom >= N  { 0 } else { (*life)[bottom as usize] } ) +
				(if bottom_right >= N { 0 } else { (*life)[bottom_right as usize] } ) +
				(if right >= N  { 0 } else { (*life)[right as usize] } );

            (*life_temp)[i as usize] = (*life)[i as usize]; 

			if (*life)[i as usize] == 0 && around == 3 {
                let cell:HtmlElement = Document::query_selector(&(*cells)[i as usize]);
                cell.style().set_property("background-color", "deeppink").expect("failed to set property");
				(*life_temp)[i as usize] = 1;

			} else if (*life)[i as usize] == 1 && (around == 2 || around == 3) {
				continue;

			} else if (*life)[i as usize] == 1 && (around <= 1 || around >= 4) {
                let cell:HtmlElement = Document::query_selector(&(*cells)[i as usize]);
                cell.style().set_property("background-color", "lightgray").expect("failed to set property");
				(*life_temp)[i as usize] = 0;
			}
        }
        /**
         　上記の判定が全て終わったら、life_tempの状態をlifeにコピーする。
        */
        for i in 0..(N as usize){
		    (*life)[i] = (*life_temp)[i]; 
        }
        Ok(())
    }
}

```


---

<br>

## JavaScript側

WebAssemblyは非同期呼び出しする必要があるため、async即時関数でラップします。
init関数で初期化してから上記CellularAutomaton構造体を呼び出します。

※`wasm.js`、`wasm_bg.wasm`のパスは環境によって変えて下さい。

```ts
import { CellularAutomaton as CA , default as init } from '../wasm/pkg/wasm.js';

(async() =>{
    await init('./src/wasm/pkg/wasm_bg.wasm');
    const ca: CA = new CA();
    ca.start();

})().catch(() => 'Failed to load wasm.');

```


## 参考にさせていただいたサイト

https://rustwasm.github.io/wasm-bindgen/examples/request-animation-frame.html
https://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.HtmlElement.html
https://qiita.com/nacika_ins/items/cf3782bd371da79def74
https://ja.wikipedia.org/wiki/%E3%83%A9%E3%82%A4%E3%83%95%E3%82%B2%E3%83%BC%E3%83%A0