LoginSignup
10
9

More than 3 years have passed since last update.

WebAssembly でセルオートマトン

Last updated at Posted at 2019-07-09

 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

10
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
9