WebAssembly でセルオートマトン

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

ライフゲームのルール

 Wikipediaより、典型的と思われるルールを採用しました。
誕生
 死んでいるセルに隣接する生きたセルがちょうど3つあれば、次の世代が誕生する。
生存
 生きているセルに隣接する生きたセルが2つか3つならば、次の世代でも生存する。
過疎
 生きているセルに隣接する生きたセルが1つ以下ならば、過疎により死滅する。
過密
 生きているセルに隣接する生きたセルが4つ以上ならば、過密により死滅する。

結果

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

n = 20

cell20

n = 50

cell50

n = 100

cell100

Cargo.toml

主に3つのcrateを読ませます。
  • wasm-bindgen – JavaScript連携
  • web-sys – HtmlElementの操作に使用
  • lazy_static – セルの状態等を格納するベクトルをグローバル変数化し、クロージャー内で使用するために使用
[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',
 ] 

Rust側

とりあえず主要箇所のみ抜粋。ソースはGitHubにあげています。

ヘッダー部分

 ポイントとして、RcRefCellrequest_animation_frameの呼び出しに、RwLocklazy_staticに使用します。
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に渡して各セル要素のクラス名を保持しています。
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で用意されている関数をラップした関数です。
#[wasm_bindgen]
pub struct CellularAutomaton{
    canvas: HtmlElement,  //描画範囲のHTML要素。HtmlElementはCopy Treatを実装していないため、pubは外す
    pub n: isize,  //描画範囲1辺に含まれるセルの数
    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(())
    }
}

JavaScript側

WebAssemblyは非同期呼び出しする必要があるため、async即時関数でラップします。
init関数で初期化してから上記CellularAutomaton構造体を呼び出します。 ※wasm.jswasm_bg.wasmのパスは環境によって変えて下さい。
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

Leave a Reply

Your email address will not be published. Required fields are marked *