Commit bd5ddc47 authored by mara's avatar mara
Browse files

initial commit

parents
install:
- appveyor-retry appveyor DownloadFile https://win.rustup.rs/ -FileName rustup-init.exe
- if not defined RUSTFLAGS rustup-init.exe -y --default-host x86_64-pc-windows-msvc --default-toolchain nightly
- set PATH=%PATH%;C:\Users\appveyor\.cargo\bin
- rustc -V
- cargo -V
build: false
test_script:
- cargo test --locked
/target
**/*.rs.bk
Cargo.lock
/bin
/pkg
wasm-pack.log
language: rust
sudo: false
matrix:
include:
# tests pass
- rust: nightly
script:
- cargo test --locked
- rustup component add rustfmt-preview
- cargo fmt --all -- --check
env: RUST_BACKTRACE=1
[package]
name = "wasm-game-of-life"
version = "0.1.0"
authors = ["Medusacle <cyphergothic@protonmail.com>"]
[lib]
crate-type = ["cdylib"]
[features]
default-features = ["console_error_panic_hook", "wee_alloc"]
[dependencies]
cfg-if = "0.1.2"
wasm-bindgen = "0.2"
js-sys = "0.2"
# The `console_error_panic_hook` crate provides better debugging of panics by
# logging them with `console.error`. This is great for development, but requires
# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for
# code size when deploying.
console_error_panic_hook = { version = "0.1.1", optional = true }
# `wee_alloc` is a tiny allocator for wasm that is only ~1K in code size
# compared to the default allocator's ~10K. It is slower than the default
# allocator, however.
wee_alloc = { version = "0.4.1", optional = true }
[dependencies.web-sys]
#path = "../../crates/web-sys"
version = "0.3"
# https://rustwasm.github.io/wasm-bindgen/api/web_sys/
features = [
'CanvasRenderingContext2d',
'CssStyleDeclaration',
'Document',
'DomRect',
'Element',
'EventTarget',
'HtmlCanvasElement',
'HtmlElement',
'MouseEvent',
'Node',
'Window',
#'console',
]
[profile.release]
lto = true
Copyright (c) 2018 Medusacle <cyphergothic@protonmail.com>
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
# Conway's Game of Life
Implementation of Conway's game of life using Rust and WebAssembly, based on
the [tutorial](https://rustwasm.github.io/book/introduction.html).
The main difference with the tutorial is that all the parts have been
implemented in Rust, including canvas rendering and event handlers. The only
javascript left is:
```js
import { main } from "wasm-game-of-life";
const eventloop = main(
document.getElementById("game-of-life-canvas"),
document.getElementById("play-pause"),
document.getElementById("randomize"),
document.getElementById("omnicide"));
```
The returned `eventloop` handle holds on to closures &c.
This is my own experiment to see if a web game can be completely implemented in
Rust.
## Dependencies
This currently requires the Rust toolchain, `npm` and `wasm-pack`
(see [setup instructions](https://rustwasm.github.io/book/game-of-life/setup.html)).
(As the javascript is trivial, going through `npm` seems cruel and
unnecessary. It could be removed, I suppose, at the cost of needing to write
more bootstrap javascript)
## Running and buildling
To build everything and start a webserver on `0.0.0.0:8080`, run:
```
./run.sh
```
There's also `./dist.sh` that will build a distribution directory in `www/dist`,
for deploying to a separate web server.
#!/bin/bash
set -e
wasm-pack build --no-typescript
#--debug
cd www
if [ ! -d node_modules ]; then
npm install
cd node_modules
ln -s ../../pkg wasm-game-of-life
fi
cd ..
#!/bin/bash
set -e
./build.sh
cd www
npm run build
#!/bin/bash
set -e
./build.sh
cd www
npm run start
extern crate cfg_if;
extern crate wasm_bindgen;
extern crate web_sys;
mod random;
mod renderer;
mod renderloop;
mod universe;
mod utils;
use std::cell::RefCell;
use std::cmp;
use std::rc::Rc;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
// use web_sys::console;
use renderer::{Renderer, CELL_SIZE};
use renderloop::RenderLoop;
use universe::Universe;
#[wasm_bindgen]
/** Interface handle to event loop, from javascript, also provides for memory management
* by keeping references to structures.
*/
pub struct RenderLoopHandle {
/** handle to inner render loop state structure */
render_loop: Rc<RefCell<RenderLoop>>,
/** holds on to the closures until they can be dropped */
closures: Vec<Box<Drop>>,
}
#[wasm_bindgen]
pub fn main(
canvas: web_sys::HtmlCanvasElement,
playpause_button: web_sys::HtmlElement,
randomize_button: web_sys::EventTarget,
omnicide_button: web_sys::EventTarget,
) -> Result<RenderLoopHandle, JsValue> {
let window = web_sys::window().expect("no global `window` exists");
let universe: Rc<RefCell<Universe>> = Rc::new(RefCell::new(Universe::new()));
let renderer: Rc<RefCell<Renderer>> = Rc::new(RefCell::new(Renderer::new(
canvas.clone(),
universe.clone(),
)?));
// Holds on to the closures until they can be dropped
let mut closures: Vec<Box<Drop>> = Vec::new();
// Mouse click handler on canvas
{
let closure: Closure<Fn(_)> = {
let canvas = canvas.clone();
let universe = universe.clone();
let renderer = renderer.clone();
Closure::wrap(Box::new(move |event: web_sys::MouseEvent| {
{
let mut universe = universe.borrow_mut();
let bounding_rect =
(canvas.as_ref() as &web_sys::Element).get_bounding_client_rect();
let scaleX = canvas.width() as f64 / bounding_rect.width();
let scaleY = canvas.height() as f64 / bounding_rect.height();
let canvasLeft = (event.client_x() as f64 - bounding_rect.x()) * scaleX;
let canvasTop = (event.client_y() as f64 - bounding_rect.y()) * scaleY;
let cellsz = CELL_SIZE as f64;
let row = cmp::min((canvasTop / (cellsz + 1.0)) as u32, universe.height() - 1);
let col = cmp::min((canvasLeft / (cellsz + 1.0)) as u32, universe.width() - 1);
universe.toggle_cell(row, col);
}
renderer.borrow().draw();
}))
};
(canvas.as_ref() as &web_sys::EventTarget)
.add_event_listener_with_callback("click", closure.as_ref().unchecked_ref())?;
closures.push(Box::new(closure));
}
// Handle "randomize" button
{
let universe = universe.clone();
let renderer = renderer.clone();
let closure: Closure<Fn(_)> = Closure::wrap(Box::new(move |event: web_sys::MouseEvent| {
randomize_title();
universe.borrow_mut().randomize();
renderer.borrow().draw();
}));
randomize_button
.add_event_listener_with_callback("click", closure.as_ref().unchecked_ref())?;
closures.push(Box::new(closure));
}
// Handle "omnicide" button
{
let universe = universe.clone();
let renderer = renderer.clone();
let closure: Closure<Fn(_)> = Closure::wrap(Box::new(move |event: web_sys::MouseEvent| {
universe.borrow_mut().kill_all();
renderer.borrow().draw();
}));
omnicide_button
.add_event_listener_with_callback("click", closure.as_ref().unchecked_ref())?;
closures.push(Box::new(closure));
}
// Render loop handling
let render_loop: Rc<RefCell<RenderLoop>> = Rc::new(RefCell::new(RenderLoop::new(
window.clone(),
playpause_button.clone(),
universe.clone(),
renderer,
)));
render_loop.borrow_mut().closure = Some({
let render_loop = render_loop.clone();
Closure::wrap(Box::new(move |time: f64| {
render_loop.borrow_mut().render_loop(time);
}))
});
// Play/pause button
{
let closure: Closure<Fn()> = {
let render_loop = render_loop.clone();
Closure::wrap(Box::new(move || {
render_loop.borrow_mut().play_pause();
}))
};
(playpause_button.as_ref() as &web_sys::EventTarget)
.add_event_listener_with_callback("click", closure.as_ref().unchecked_ref())?;
closures.push(Box::new(closure));
}
randomize_title();
render_loop.borrow_mut().play();
Ok(RenderLoopHandle {
render_loop,
closures,
})
}
/* TODO: mass toggling using mouseup, mousemove, mousedown */
/*** MISC ***/
const TITLE_HEAD: &[&str] = &[
"alpha", "beta", "gamma", "delta", "omega", "aleph", "nabla", "eschaton",
];
#[wasm_bindgen]
pub fn randomize_title() -> Result<(), JsValue> {
let window = web_sys::window().expect("no global `window` exists");
let document = window.document().expect("should have a document on window");
document.set_title(&format!(
"{}-{:04x}-{:04x}",
random::choice(TITLE_HEAD),
random::u32(65536),
random::u32(65536)
));
Ok(())
}
/** non-cryptographic "fun" randomness functions */
pub fn choice<T>(arr: &[T]) -> &T {
&arr[(js_sys::Math::random() * arr.len() as f64) as usize]
}
pub fn u32(max: u32) -> u32 {
(js_sys::Math::random() * (max as f64)) as u32
}
pub fn bool() -> bool {
js_sys::Math::random() >= 0.5
}
/** RENDERING */
use std::cell::RefCell;
use std::rc::Rc;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use universe::{UCell, Universe};
pub struct Renderer {
canvas: web_sys::HtmlCanvasElement,
ctx: web_sys::CanvasRenderingContext2d,
universe: Rc<RefCell<Universe>>,
}
pub const CELL_SIZE: u32 = 5; // px
const GRID_COLOR: &str = "#202020";
const DEAD_COLOR: &str = "#000000";
const ALIVE_COLOR: &str = "#8000FF";
impl Renderer {
pub fn new(
canvas: web_sys::HtmlCanvasElement,
universe: Rc<RefCell<Universe>>,
) -> Result<Renderer, JsValue> {
{
let universe = universe.borrow();
canvas.set_height((CELL_SIZE + 1) * universe.height() + 1);
canvas.set_width((CELL_SIZE + 1) * universe.width() + 1);
}
let ctx = canvas
.get_context("2d")?
.expect("Canvas must have a 2d context")
.dyn_into::<web_sys::CanvasRenderingContext2d>()?;
Ok(Renderer {
canvas: canvas,
universe: universe,
ctx: ctx,
})
}
fn draw_grid(&self) {
let ctx = &self.ctx;
let universe = self.universe.borrow();
let width = universe.width();
let height = universe.height();
let cellsz = CELL_SIZE as f64;
ctx.begin_path();
ctx.set_stroke_style(&JsValue::from(GRID_COLOR));
// Vertical lines.
for i in 0..=width {
ctx.move_to(i as f64 * (cellsz + 1.0) + 1.0, 0.0);
ctx.line_to(
i as f64 * (cellsz + 1.0) + 1.0,
(cellsz + 1.0) * height as f64 + 1.0,
);
}
// Horizontal lines.
for j in 0..=height {
ctx.move_to(0.0, j as f64 * (cellsz + 1.0) + 1.0);
ctx.line_to(
(cellsz + 1.0) * width as f64 + 1.0,
j as f64 * (cellsz + 1.0) + 1.0,
);
}
ctx.stroke();
}
fn draw_cells(&self) {
let ctx = &self.ctx;
let universe = self.universe.borrow();
let cellsz = CELL_SIZE as f64;
ctx.begin_path();
for (status, style) in &[
(UCell::Dead, JsValue::from(DEAD_COLOR)),
(UCell::Alive, JsValue::from(ALIVE_COLOR)),
] {
ctx.set_fill_style(style);
for row in 0..universe.height() {
for col in 0..universe.width() {
if universe.get_cell(row, col) == *status {
ctx.fill_rect(
col as f64 * (cellsz + 1.0) + 1.0,
row as f64 * (cellsz + 1.0) + 1.0,
cellsz,
cellsz,
);
}
}
}
}
ctx.stroke();
}
pub fn draw(&self) {
self.draw_grid();
self.draw_cells();
}
}
/*** EVENT HANDLING ***/
use std::cell::RefCell;
use std::rc::Rc;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use renderer::Renderer;
use universe::Universe;
pub struct RenderLoop {
window: web_sys::Window,
playpause_button: web_sys::HtmlElement,
universe: Rc<RefCell<Universe>>,
renderer: Rc<RefCell<Renderer>>,
animation_id: Option<i32>,
pub closure: Option<Closure<Fn(f64)>>,
}
impl RenderLoop {
pub fn new(
window: web_sys::Window,
playpause_button: web_sys::HtmlElement,
universe: Rc<RefCell<Universe>>,
renderer: Rc<RefCell<Renderer>>,
) -> RenderLoop {
RenderLoop {
window: window,
playpause_button: playpause_button,
universe: universe,
renderer: renderer,
animation_id: None,
closure: None,
}
}
pub fn render_loop(&mut self, _time: f64) {
self.universe.borrow_mut().tick();
self.renderer.borrow().draw();
self.animation_id = if let Some(ref closure) = self.closure {
Some(
self.window
.request_animation_frame(closure.as_ref().unchecked_ref())
.expect("cannot set animation frame"),
)
} else {
None
}
}
pub fn is_paused(&self) -> bool {
self.animation_id.is_none()
}
pub fn play(&mut self) {
(self.playpause_button.as_ref() as &web_sys::Node).set_text_content(Some("⏸"));
self.render_loop(0.0);
}
pub fn pause(&mut self) {
(self.playpause_button.as_ref() as &web_sys::Node).set_text_content(Some("▶"));
if let Some(id) = self.animation_id {
self.window.cancel_animation_frame(id);
self.animation_id = None;
}
}
pub fn play_pause(&mut self) {
if self.is_paused() {
self.play();
} else {
self.pause();
}
}
}
use random;
#[repr(u8)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum UCell {
Dead = 0,
Alive = 1,
}
pub struct Universe {
width: u32,
height: u32,
cells: Vec<UCell>,
}
impl UCell {
fn toggle(&mut self) {
*self = match *self {
UCell::Dead => UCell::Alive,
UCell::Alive => UCell::Dead,
};
}
}
impl Universe {
pub fn new() -> Universe {
let width = 64;
let height = 64;
let cells = (0..width * height)
.map(|i| {
if i % 2 == 0 || i % 7 == 0 {
UCell::Alive
} else {
UCell::Dead
}
}).collect();
Universe {
width,
height,
cells,
}
}
fn get_index(&self, row: u32, column: u32) -> usize {
(row * self.width + column) as usize
}
pub fn get_cell(&self, row: u32, column: u32) -> UCell {
let idx = self.get_index(row, column);
self.cells[idx]
}
fn live_neighbor_count(&self, row: u32, column: u32) -> u8 {
let mut count = 0;
for delta_row in [self.height - 1, 0, 1].iter().cloned() {
for delta_col in [self.width - 1, 0, 1].iter().cloned() {
if delta_row == 0 && delta_col == 0 {
continue;
}
let neighbor_row = (row + delta_row) % self.height;
let neighbor_col = (column + delta_col) % self.width;
let idx = self.get_index(neighbor_row, neighbor_col);
count += self.cells[idx] as u8;
}
}
count
}
pub fn tick(&mut self) {
let mut next = self.cells.clone();