on master help sign in
examples: Add Conway's Game of Life example
04bfc33Nick Fitzgerald
Cargo.lock+14
@@ -108,6 +108,20 @@ dependencies = [
108 "web-sys 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)",
108 "web-sys 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)",
109]
109]
110
110
111+[[package]]
112+name = "dodrio-game-of-life"
113+version = "0.1.0"
114+dependencies = [
115+ "console_error_panic_hook 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
116+ "dodrio 0.1.0",
117+ "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
118+ "js-sys 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
119+ "wasm-bindgen 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)",
120+ "wasm-bindgen-futures 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
121+ "wasm-bindgen-test 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)",
122+ "web-sys 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
123+]
124+
111[[package]]
125[[package]]
112name = "dodrio-hello-world"
126name = "dodrio-hello-world"
113version = "0.1.0"
127version = "0.1.0"
Cargo.toml+1
@@ -49,6 +49,7 @@ opt-level = "s"
49members = [
49members = [
50 "./crates/js-api",
50 "./crates/js-api",
51 "./examples/counter",
51 "./examples/counter",
52+ "./examples/game-of-life",
52 "./examples/hello-world",
53 "./examples/hello-world",
53 "./examples/input-form",
54 "./examples/input-form",
54 "./examples/js-component",
55 "./examples/js-component",
examples/game-of-life/Cargo.tomlnew file+30
@@ -0,0 +1,30 @@
1+[package]
2+name = "dodrio-game-of-life"
3+version = "0.1.0"
4+authors = ["Nick Fitzgerald <fitzgen@gmail.com>"]
5+edition = "2018"
6+
7+[lib]
8+crate-type = ["cdylib"]
9+
10+[features]
11+
12+[dependencies]
13+dodrio = { path = "../.." }
14+wasm-bindgen = "0.2.38"
15+console_error_panic_hook = "0.1.6"
16+js-sys = "0.3.15"
17+wasm-bindgen-futures = "0.3.15"
18+futures = "0.1.25"
19+
20+[dependencies.web-sys]
21+version = "0.3.15"
22+features = [
23+ "Document",
24+ "HtmlElement",
25+ "Node",
26+ "Window",
27+]
28+
29+[dev-dependencies]
30+wasm-bindgen-test = "0.2.38"
examples/game-of-life/README.mdnew file+23
@@ -0,0 +1,23 @@
1+# Conway's Game of Life
2+
3+A port of the [Rust and WebAssembly tutorial's Game of Life
4+implementation](https://rustwasm.github.io/book/game-of-life/introduction.html)
5+to rendering to HTML with Dodrio instead of to a canvas via JavaScript.
6+
7+## Source
8+
9+See `src/lib.rs`.
10+
11+## Build
12+
13+```
14+wasm-pack build --target no-modules
15+```
16+
17+## Serve
18+
19+Use any HTTP server, for example:
20+
21+```
22+python -m SimpleHTTPServer
23+```
examples/game-of-life/index.htmlnew file+47
@@ -0,0 +1,47 @@
1+<!DOCTYPE html>
2+<html>
3+ <head>
4+ <meta http-equiv="Content-type" content="text/html; charset=utf-8"/>
5+ <title>Conway's Game of Life</title>
6+ <style>
7+ .cell {
8+ display: inline-block;
9+ width: 1vh;
10+ height: 1vh;
11+ border: 1px #cccccc solid;
12+ margin: -1px;
13+ padding: 0;
14+ }
15+
16+ .row {
17+ height: 1vh;
18+ margin: 0;
19+ padding: 0;
20+ }
21+
22+ .universe {
23+ }
24+
25+ body {
26+ width: 100vw;
27+ height: 100vh;
28+ display: flex;
29+ align-items: center;
30+ justify-content: center;
31+ padding: 0;
32+ margin: 0;
33+ }
34+
35+ html {
36+ padding: 0;
37+ margin: 0;
38+ }
39+ </style>
40+ </head>
41+ <body>
42+ <script src="pkg/dodrio_game_of_life.js"></script>
43+ <script>
44+ wasm_bindgen("pkg/dodrio_game_of_life_bg.wasm");
45+ </script>
46+ </body>
47+</html>
examples/game-of-life/src/lib.rsnew file+216
@@ -0,0 +1,216 @@
1+use dodrio::bumpalo::{self, Bump};
2+use dodrio::{Node, Render, Vdom};
3+use futures::prelude::*;
4+use std::cell::RefCell;
5+use std::rc::Rc;
6+use wasm_bindgen::{prelude::*, JsCast};
7+
8+#[repr(u8)]
9+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10+pub enum Cell {
11+ Dead = 0,
12+ Alive = 1,
13+}
14+
15+/// The universe containing all of the cells for our Game of Life.
16+struct Universe {
17+ width: u32,
18+ height: u32,
19+ cells: Vec<Cell>,
20+}
21+
22+impl Universe {
23+ /// Construct a new universe.
24+ pub fn new() -> Universe {
25+ let width = 64;
26+ let height = 64;
27+
28+ let cells = (0..width * height)
29+ .map(|i| {
30+ if i % 2 == 0 || i % 7 == 0 {
31+ Cell::Alive
32+ } else {
33+ Cell::Dead
34+ }
35+ })
36+ .collect();
37+
38+ Universe {
39+ width,
40+ height,
41+ cells,
42+ }
43+ }
44+
45+ /// Compute one tick of the universe.
46+ pub fn tick(&mut self) {
47+ let mut next = self.cells.clone();
48+
49+ for row in 0..self.height {
50+ for col in 0..self.width {
51+ let idx = self.get_index(row, col);
52+ let cell = self.cells[idx];
53+ let live_neighbors = self.live_neighbor_count(row, col);
54+
55+ let next_cell = match (cell, live_neighbors) {
56+ // Rule 1: Any live cell with fewer than two live neighbours
57+ // dies, as if caused by underpopulation.
58+ (Cell::Alive, x) if x < 2 => Cell::Dead,
59+ // Rule 2: Any live cell with two or three live neighbours
60+ // lives on to the next generation.
61+ (Cell::Alive, 2) | (Cell::Alive, 3) => Cell::Alive,
62+ // Rule 3: Any live cell with more than three live
63+ // neighbours dies, as if by overpopulation.
64+ (Cell::Alive, x) if x > 3 => Cell::Dead,
65+ // Rule 4: Any dead cell with exactly three live neighbours
66+ // becomes a live cell, as if by reproduction.
67+ (Cell::Dead, 3) => Cell::Alive,
68+ // All other cells remain in the same state.
69+ (otherwise, _) => otherwise,
70+ };
71+
72+ next[idx] = next_cell;
73+ }
74+ }
75+
76+ self.cells = next;
77+ }
78+
79+ fn get_index(&self, row: u32, column: u32) -> usize {
80+ (row * self.width + column) as usize
81+ }
82+
83+ fn live_neighbor_count(&self, row: u32, column: u32) -> u8 {
84+ let mut count = 0;
85+
86+ let north = if row == 0 { self.height - 1 } else { row - 1 };
87+
88+ let south = if row == self.height - 1 { 0 } else { row + 1 };
89+
90+ let west = if column == 0 {
91+ self.width - 1
92+ } else {
93+ column - 1
94+ };
95+
96+ let east = if column == self.width - 1 {
97+ 0
98+ } else {
99+ column + 1
100+ };
101+
102+ let nw = self.get_index(north, west);
103+ count += self.cells[nw] as u8;
104+
105+ let n = self.get_index(north, column);
106+ count += self.cells[n] as u8;
107+
108+ let ne = self.get_index(north, east);
109+ count += self.cells[ne] as u8;
110+
111+ let w = self.get_index(row, west);
112+ count += self.cells[w] as u8;
113+
114+ let e = self.get_index(row, east);
115+ count += self.cells[e] as u8;
116+
117+ let sw = self.get_index(south, west);
118+ count += self.cells[sw] as u8;
119+
120+ let s = self.get_index(south, column);
121+ count += self.cells[s] as u8;
122+
123+ let se = self.get_index(south, east);
124+ count += self.cells[se] as u8;
125+
126+ count
127+ }
128+}
129+
130+/// The rendering implementation for our Game of Life.
131+impl Render for Universe {
132+ fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
133+ where
134+ 'a: 'bump,
135+ {
136+ use dodrio::builder::*;
137+
138+ let mut rows = bumpalo::collections::Vec::with_capacity_in(self.height as usize, bump);
139+
140+ for row in self.cells.chunks(self.width as usize) {
141+ let mut cells = bumpalo::collections::Vec::with_capacity_in(self.width as usize, bump);
142+
143+ for cell in row {
144+ cells.push(
145+ span(bump)
146+ .attr("class", "cell")
147+ .attr(
148+ "style",
149+ match cell {
150+ Cell::Alive => "background-color: black",
151+ Cell::Dead => "background-color: white",
152+ },
153+ )
154+ .finish(),
155+ );
156+ }
157+
158+ rows.push(div(bump).attr("class", "row").children(cells).finish());
159+ }
160+
161+ div(bump).attr("class", "universe").children(rows).finish()
162+ }
163+}
164+
165+#[wasm_bindgen(start)]
166+pub fn run() {
167+ // Set up the panic hook for debugging when things go wrong.
168+ console_error_panic_hook::set_once();
169+
170+ // Grab the document's `<body>`.
171+ let window = web_sys::window().unwrap_throw();
172+ let document = window.document().unwrap_throw();
173+ let body = document.body().unwrap_throw();
174+
175+ // Create a new Game of Life `Universe` render component.
176+ let universe = Universe::new();
177+
178+ // Create a virtual DOM and mount it and the `Hello` render component to the
179+ // `<body>`.
180+ let vdom = Vdom::new(body.as_ref(), universe);
181+
182+ // Kick off a loop that keeps computing a tick of the universe and then
183+ // re-rendering on every animation frame.
184+ let rc: Rc<RefCell<Option<Closure<FnMut()>>>> = Rc::new(RefCell::new(None));
185+ let rc2 = rc.clone();
186+ let window2 = window.clone();
187+ let weak = vdom.weak();
188+ let f = Closure::wrap(Box::new(move || {
189+ weak.schedule_render();
190+
191+ wasm_bindgen_futures::spawn_local(
192+ weak.with_component(|root| {
193+ let universe = root.unwrap_mut::<Universe>();
194+ universe.tick();
195+ })
196+ .map_err(|_| wasm_bindgen::throw_str("impossible, we always keep the vdom alive")),
197+ );
198+
199+ window
200+ .request_animation_frame(
201+ rc.borrow()
202+ .as_ref()
203+ .unwrap_throw()
204+ .as_ref()
205+ .unchecked_ref::<js_sys::Function>(),
206+ )
207+ .unwrap_throw();
208+ }) as Box<FnMut()>);
209+ window2
210+ .request_animation_frame(f.as_ref().unchecked_ref::<js_sys::Function>())
211+ .unwrap_throw();
212+ *rc2.borrow_mut() = Some(f);
213+
214+ // Run the virtual DOM forever and don't unmount it.
215+ vdom.forget();
216+}