DEV Community

loading...

How to only Rust for web front_end

arnthelongbeard profile image arn-the-long-beard Updated on ・29 min read

Hello guys !

Arn here and I am a programmer, mainly front_end & full_stack React/Angular/Redux/Ngrx/Javascript/Typescript + Node I was and now surely Rust/seed-rs full_stack.

Today I write my first big tutorial ever and I 'll show you something amazing I have discovered last week. I am new to Rust and I love it, so if there are few experienced Rustaceans who come by and see my ugly code, then your welcome to tell me :D.

Anyway I discovered Wasm few months ago and I started to play with this tutorial there :

https://rustwasm.github.io/docs/book/introduction.html

This book explains the basics about how to use Rust to actually make some front end stuff and use the compiled code with Javascript. I was so curious about it especially because Rust and also because my computer get too slow when doing front end stuff on larger apps... RAM issue there when too many files in Angular.

I started to really work on the tutorial last week and then it went " Oh My God " in my head. Having the rust toolchain, the amazing smart compiler with you while doing front end work and the performance....

Just the feelings of using little bit of rust to make front end made me feel so satisfied and happy and so motivated.

So after playing with this tutorial, I was like trying to work with WebGL instead of Canvas rendering and then maybe somebody made something using WebGl. Then I started to think, maybe somebody made a Web Framework so we can use only Rust and no need for Javascript. Then I guess it would be so crazy. Having the most solid/robust/safe/efficient programming language and use it for front end.

And I saw Seed and then my head blew up!

The day after I cloned a quick starter app for Seed, I decided to adapt the Game of Life tutorial into Rust only code.

It took me few hours to do it. Since I am new to Rust and I am also a bit rusty ( so bad joke ) , I think that few hours is fairly good when new to a new Framework & language.

So I am gonna show you in this tutorial the differences between the original game of life I made and the seeded_game_of_life.
It is recommended to know some stuff for this tutorial

  • You have read about Rust
  • You have read about Javascript
  • You have read about Wasm
  • It would be very nice if you have followed once the original game of life tutorial

NB :

  • I show you a very brutal and straight forward conversion. There is awesome stuff in Seed that can be used and that improves the code a lot like Reference to element from the doc => ElRef<T> . I just skip it for now.

Summary

1 - Setup
2 - Take the Rust code from the original tutorial for Universe
3 - Let's build the core of the app from Js to Rust
4 - Let's try test & benchmark
5 - How to improve performances
6 - Conclusion

Let's go !

1 - Setup

You can skip this section if you clone the quick start found at 'https://github.com/seed-rs/seed-quickstart' but the below shows quickly steps to create a project from scratch.

cargo new seeded-game-of-life --lib

Our project is a library.

We are gonna add few things :

Let's update our Cargo.toml

Cargo.toml

[package]
version = "0.1.0"
name = "seeded-game-of-life"
repository = "https://github.com/seed-rs/seed-quickstart"
authors = ["Your Name <email@address.com>"]
description = "App Description"
categories = ["category"]
license = "MIT"
readme = "./README.md"
edition = "2018"

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

[dependencies]
seed = { git = "https://github.com/seed-rs/seed", rev = "0a538f0" }
[dependencies.web-sys]
version = "0.3"

Okay here we have 3 important points :

  • cdylib is for compiling to .wasm for what I understand. If you try to compile without it you are gonna get errors

  • seed = { git = "https://github.com/seed-rs/seed", rev = "0a538f0" }

This is our main dependency because it is the Web Framework :D

  • [dependencies.web-sys] version = "0.3"

web-sys exposes most of the Web browser Api to Rust. Depending of what you need you might activate some features.

We need to run few tasks to watch and compile and also serve files.

cargo install cargo-make

Then make this file MakeFile.toml

We can also use it to make release build. There is no need to understand what there is inside :P for now.

MakeFile.toml

[env]
PORT = "8000"

[config]
skip_core_tasks = true

# ---- BASIC ----

[tasks.watch]
description = "Watch files and recompile the project on change"
run_task = [
    { name = "build" },
]
watch = true

[tasks.serve]
description = "Start server"
install_crate = { crate_name = "microserver", binary = "microserver", test_arg = "-h" }
command = "microserver"
args = ["--port", "${PORT}"]

[tasks.verify]
description = "Format, lint with Clippy and run tests"
dependencies = ["fmt", "clippy", "test_h_firefox"]

# ---- BUILD ----

[tasks.build]
description = "Build with wasm-pack"
install_crate = { crate_name = "wasm-pack", binary = "wasm-pack", test_arg = "-V" }
command = "wasm-pack"
args = ["build", "--target", "web", "--out-name", "package", "--dev"]

[tasks.build_release]
description = "Build with wasm-pack in release mode"
install_crate = { crate_name = "wasm-pack", binary = "wasm-pack", test_arg = "-V" }
command = "wasm-pack"
args = ["build", "--target", "web", "--out-name", "package"]

# ---- LINT ----

[tasks.clippy]
description = "Lint with Clippy"
install_crate = { rustup_component_name = "clippy", binary = "cargo-clippy", test_arg = "--help" }
command = "cargo"
args = ["clippy", "--all-features", "--", "--deny", "warnings", "--deny", "clippy::pedantic", "--deny", "clippy::nursery"]

[tasks.fmt]
description = "Format with rustfmt"
install_crate = { rustup_component_name = "rustfmt", binary = "rustfmt", test_arg = "-V" }
command = "cargo"
args = ["fmt"]


# ---- TEST ----

[tasks.test_h]
description = "Run headless tests. Ex: 'cargo make test_h firefox'. Test envs: [chrome, firefox, safari]"
extend = "test"
args = ["test", "--headless", "--${@}"]

[tasks.test_h_firefox]
description = "Run headless tests with Firefox."
extend = "test"
args = ["test", "--headless", "--firefox"]

[tasks.test]
description = "Run tests. Ex: 'cargo make test firefox'. Test envs: [chrome, firefox, safari]"
install_crate = { crate_name = "wasm-pack", binary = "wasm-pack", test_arg = "-V" }
command = "wasm-pack"
args = ["test", "--${@}"]

Add the index.html file

Our app is a Single Page Application. We are gonna feed it with Wasm and also some generated js.

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <title>Seeded Game of life</title>
</head>

<body>
    <section id="app"></section>
    <script type="module">
        import init from '/pkg/package.js';
        init('/pkg/package_bg.wasm');
    </script>
</body>
</html>

We have an import to a generated js file that contains communication between JS and WASM, because under the hood we have sometimes calls to some standard js function used to talk with the web browser.

This should change in the future.

Then we have the package_bg.wasm that contains compiled binaries from our lovely Rust :)

Now let's add an example code in our lib.rs and see if it works.

lib.rs

use seed::{prelude::*, *};

// `init` describes what should happen when your app started.
fn init(_: Url, _: &mut impl Orders<Msg>) -> Model {
    Model::default()
}

// `Model` describes our app state.
type Model = i32;

// `Msg` describes the different events you can modify state with.
enum Msg {
    Increment,
}

// `update` describes how to handle each `Msg`.
fn update(msg: Msg, model: &mut Model, _: &mut impl Orders<Msg>) {
    match msg {
        Msg::Increment => *model += 1,
    }
}

// `view` describes what to display.
fn view(model: &Model) -> Node<Msg> {
    div![
        "This is a counter: ",
        C!["counter"],
        button![
            model,
            ev(Ev::Click, |_| Msg::Increment),
        ],
    ]
}

#[wasm_bindgen(start)]
pub fn start() {
    // Mount the `app` to the element with the `id` "app".
    App::start("app", init, update, view);
}

In your .gitignore

/target
Cargo.lock
/pkg  

At least on linux, I need to do this otherwise the change are detected in /pkg and cargo keeps rebuilding every time I have compiled :P Maybe a tiny bug to fix there :D

We have now :

seeded-game-of-life/
├── .gitignore
├── Cargo.toml
├── index.html
├── MakeFile.toml
└── src
    ├── lib.rs

Then

cargo make watch in one terminal -> compile on changes

cargo make serve in an other one -> serve the file

http://localhost:8000

You should have a nice counter.

There will be no more setup to do except for test & release build and usage of web-sys

If we compare to the original setup for game of life we do not need www folder

  • no package json
  • no webpack
  • no javascript code
  • no node_modules

Basically, with 4 files & 2 dependencies ( sys-web + seed) we have the base to make web developement.

This is a huge improvement !!!! We can easily focus and be productive :).
Last week_end I showed this to a friend of mine new to web development. He has been struggling to use React, because there are so many dependencies and knowledge to get on webpack, package.json and so on. The same applies to Angular or Vue. Even if with time all of them get easier to use and configure, they still can be pretty challenging to people not familiar with Javascript and coming from more low level programming. There are so many files everywhere :P
And so many concepts and levels of abstraction to understand.

In the first hour with Rust/Seed my friend did update the code you will see and use ElRef<T> instead of the dirty DOM call I am doing. It was quite impressive because :

  • He never touched Rust before
  • He never touched Elm before but he understand just from the counter example the Pattern
  • He is not a web developer
  • He is not a Js dude but a Python dude.

So now we have a very light setup to start and have fun with :D

2 - Take the Rust code from the original tutorial

My original lib.rs from https://github.com/arn-the-long-beard/wasm-game-of-life/blob/master/src/lib.rs


mod utils;

use rand_core::{OsRng, RngCore};
use std::fmt;
use wasm_bindgen::prelude::*;
use web_sys::console;
#[wasm_bindgen]
extern crate web_sys;
// A macro to provide `println!(..)`-style syntax for `console.log` logging.
macro_rules! log {
    ( $( $t:tt )* ) => {
        web_sys::console::log_1(&format!( $( $t )* ).into());
    }
}
// When the `wee_alloc` feature is enabled, use `wee_alloc` as the global
// allocator.
#[cfg(feature = "wee_alloc")]
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;

#[wasm_bindgen]
#[repr(u8)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Cell {
    Dead = 0,
    Alive = 1,
}
impl Cell {
    fn toggle(&mut self) {
        *self = match *self {
            Cell::Dead => Cell::Alive,
            Cell::Alive => Cell::Dead,
        };
    }
}
#[wasm_bindgen]
pub struct Universe {
    width: u32,
    height: u32,
    cells: Vec<Cell>,
}

impl Universe {
    fn get_index(&self, row: u32, column: u32) -> usize {
        (row * self.width + column) as usize
    }
    fn live_neighbor_count(&self, row: u32, column: u32) -> u8 {
        let mut count = 0;

        let north = if row == 0 { self.height - 1 } else { row - 1 };

        let south = if row == self.height - 1 { 0 } else { row + 1 };

        let west = if column == 0 {
            self.width - 1
        } else {
            column - 1
        };

        let east = if column == self.width - 1 {
            0
        } else {
            column + 1
        };

        let nw = self.get_index(north, west);
        count += self.cells[nw] as u8;

        let n = self.get_index(north, column);
        count += self.cells[n] as u8;

        let ne = self.get_index(north, east);
        count += self.cells[ne] as u8;

        let w = self.get_index(row, west);
        count += self.cells[w] as u8;

        let e = self.get_index(row, east);
        count += self.cells[e] as u8;

        let sw = self.get_index(south, west);
        count += self.cells[sw] as u8;

        let s = self.get_index(south, column);
        count += self.cells[s] as u8;

        let se = self.get_index(south, east);
        count += self.cells[se] as u8;

        count
    }
}

/// Public methods, exported to JavaScript.
#[wasm_bindgen]
impl Universe {
    pub fn new() -> Universe {
        utils::set_panic_hook();
        let width = 64;
        let height = 64;

        let cells = (0..width * height)
            .map(|i| {
                if i % 2 == 0 || i % 7 == 0 {
                    Cell::Alive
                } else {
                    Cell::Dead
                }
            })
            .collect();

        Universe {
            width,
            height,
            cells,
        }
    }
    pub fn reset(&mut self) {
        let cells = (0..self.width * self.height)
            .map(|i| {
                if i % 2 == 0 || i % 7 == 0 {
                    Cell::Alive
                } else {
                    Cell::Dead
                }
            })
            .collect();

        self.cells = cells;
    }
    pub fn death() -> Universe {
        utils::set_panic_hook();
        let width = 64;
        let height = 64;

        let cells = (0..width * height).map(|i| Cell::Dead).collect();

        Universe {
            width,
            height,
            cells,
        }
    }
    pub fn random() -> Universe {
        utils::set_panic_hook();
        let width = 64;
        let height = 64;
        let mut key = [0u8; 16];
        OsRng.fill_bytes(&mut key);
        let random_u64 = OsRng.next_u64();
        let cells = (0..width * height)
            .map(|i| {
                if random_u64 % 2 == 0 || random_u64 % 7 == 0 {
                    Cell::Alive
                } else {
                    Cell::Dead
                }
            })
            .collect();

        Universe {
            width,
            height,
            cells,
        }
    }
    pub fn width(&self) -> u32 {
        self.width
    }

    pub fn height(&self) -> u32 {
        self.height
    }

    pub fn cells(&self) -> *const Cell {
        self.cells.as_ptr()
    }
    ///
    /// Toggle a cell on specific coordinates
    pub fn toggle_cell(&mut self, row: u32, column: u32) {
        let idx = self.get_index(row, column);
        self.cells[idx].toggle();
    }
    pub fn render(&self) -> String {
        self.to_string()
    }

    pub fn tick(&mut self) {
        let mut next = { self.cells.clone() };

        {
            for row in 0..self.height {
                for col in 0..self.width {
                    let idx = self.get_index(row, col);
                    let cell = self.cells[idx];
                    let live_neighbors = self.live_neighbor_count(row, col);

                    let next_cell = match (cell, live_neighbors) {
                        // Rule 1: Any live cell with fewer than two live neighbours
                        // dies, as if caused by underpopulation.
                        (Cell::Alive, x) if x < 2 => Cell::Dead,
                        // Rule 2: Any live cell with two or three live neighbours
                        // lives on to the next generation.
                        (Cell::Alive, 2) | (Cell::Alive, 3) => Cell::Alive,
                        // Rule 3: Any live cell with more than three live
                        // neighbours dies, as if by overpopulation.
                        (Cell::Alive, x) if x > 3 => Cell::Dead,
                        // Rule 4: Any dead cell with exactly three live neighbours
                        // becomes a live cell, as if by reproduction.
                        (Cell::Dead, 3) => Cell::Alive,
                        // All other cells remain in the same state.
                        (otherwise, _) => otherwise,
                    };

                    next[idx] = next_cell;
                }
            }
        }

        self.cells = next;
    }
    /// Set the width of the universe.
    ///
    /// Resets all cells to the dead state.
    pub fn set_width(&mut self, width: u32) {
        self.width = width;
        self.cells = (0..width * self.height).map(|_i| Cell::Dead).collect();
    }

    /// Set the height of the universe.
    ///
    /// Resets all cells to the dead state.
    pub fn set_height(&mut self, height: u32) {
        self.height = height;
        self.cells = (0..self.width * height).map(|_i| Cell::Dead).collect();
    }
    // ...
}

impl Universe {
    /// Get the dead and alive values of the entire universe.
    pub fn get_cells(&self) -> &[Cell] {
        &self.cells
    }

    /// Set cells to be alive in a universe by passing the row and column
    /// of each cell as an array.
    pub fn set_cells(&mut self, cells: &[(u32, u32)]) {
        for (row, col) in cells.iter().cloned() {
            let idx = self.get_index(row, col);
            self.cells[idx] = Cell::Alive;
        }
    }
}

impl fmt::Display for Universe {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        for line in self.cells.as_slice().chunks(self.width as usize) {
            for &cell in line {
                let symbol = if cell == Cell::Dead { '◻' } else { '◼' };
                write!(f, "{}", symbol)?;
            }
            write!(f, "\n")?;
        }

        Ok(())
    }
}

We take out the part only about Universe. We merge the 2 implementations and create a new file universe.rs

NB : my original lib.rs contains more than the what you can find on the repository from the tutorial because I worked out few of the nice exercises given by the author.

I did implemented the Random Universe and Ultimate Death.

I did not succeed to make the Random Universe works on the original game of life :(

Here are some extra dependencies we need for generating random stuff. Add this under [dependencies] in your Cargo.toml

rand = "0.7.3"
rand_core = "0.5.1"

I admit I am super lazy. There is surely an easy way to do it by hand without dependencies :D

Now in /src

universe.rs

use rand_core::{OsRng, RngCore};
use std::fmt;
// When the `wee_alloc` feature is enabled, use `wee_alloc` as the global
// allocator.
#[cfg(feature = "wee_alloc")]
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;

#[repr(u8)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Cell {
    Dead = 0,
    Alive = 1,
}
impl Cell {
    fn toggle(&mut self) {
        *self = match *self {
            Cell::Dead => Cell::Alive,
            Cell::Alive => Cell::Dead,
        };
    }
}

pub struct Universe {
    width: u32,
    height: u32,
    cells: Vec<Cell>,
}
/// Public methods, exported to JavaScript.

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 {
                    Cell::Alive
                } else {
                    Cell::Dead
                }
            })
            .collect();

        Universe {
            width,
            height,
            cells,
        }
    }
    pub fn reset(&mut self) {
        let cells = (0..self.width * self.height)
            .map(|i| {
                if i % 2 == 0 || i % 7 == 0 {
                    Cell::Alive
                } else {
                    Cell::Dead
                }
            })
            .collect();

        self.cells = cells;
    }
    /// Kill all the cells
    pub fn death() -> Universe {
        let width = 64;
        let height = 64;

        let cells = (0..width * height).map(|i| Cell::Dead).collect();

        Universe {
            width,
            height,
            cells,
        }
    }
    /// Generate random state for cell
    pub fn random() -> Universe {
        let width = 64;
        let height = 64;
        let mut key = [0u8; 16];
        OsRng.fill_bytes(&mut key);

        let cells = (0..width * height)
            .map(|i| {
                if OsRng.next_u64() % 2 == 0 {
                    Cell::Alive
                } else {
                    Cell::Dead
                }
            })
            .collect();

        Universe {
            width,
            height,
            cells,
        }
    }
    pub fn get_index(&self, row: u32, column: u32) -> usize {
        (row * self.width + column) as usize
    }

    /// Get the dead and alive values of the entire universe.
    pub fn get_cells(&self) -> &[Cell] {
        &self.cells
    }

    /// Set cells to be alive in a universe by passing the row and column
    /// of each cell as an array.
    pub fn set_cells(&mut self, cells: &[(u32, u32)]) {
        for (row, col) in cells.iter().cloned() {
            let idx = self.get_index(row, col);
            self.cells[idx] = Cell::Alive;
        }
    }
    fn live_neighbor_count(&self, row: u32, column: u32) -> u8 {
        let mut count = 0;

        let north = if row == 0 { self.height - 1 } else { row - 1 };

        let south = if row == self.height - 1 { 0 } else { row + 1 };

        let west = if column == 0 {
            self.width - 1
        } else {
            column - 1
        };

        let east = if column == self.width - 1 {
            0
        } else {
            column + 1
        };

        let nw = self.get_index(north, west);
        count += self.cells[nw] as u8;

        let n = self.get_index(north, column);
        count += self.cells[n] as u8;

        let ne = self.get_index(north, east);
        count += self.cells[ne] as u8;

        let w = self.get_index(row, west);
        count += self.cells[w] as u8;

        let e = self.get_index(row, east);
        count += self.cells[e] as u8;

        let sw = self.get_index(south, west);
        count += self.cells[sw] as u8;

        let s = self.get_index(south, column);
        count += self.cells[s] as u8;

        let se = self.get_index(south, east);
        count += self.cells[se] as u8;

        count
    }
    pub fn width(&self) -> u32 {
        self.width
    }

    pub fn height(&self) -> u32 {
        self.height
    }

    pub fn cells(&self) -> *const Cell {
        self.cells.as_ptr()
    }
    ///
    /// Toggle a cell on specific coordinates
    pub fn toggle_cell(&mut self, row: u32, column: u32) {
        let idx = self.get_index(row, column);
        self.cells[idx].toggle();
    }
    pub fn render(&self) -> String {
        self.to_string()
    }

    pub fn cell_at_index(&self, index: usize) -> Cell {
        self.cells[index]
    }

    pub fn tick(&mut self) {
        let mut next = { self.cells.clone() };

        {
            for row in 0..self.height {
                for col in 0..self.width {
                    let idx = self.get_index(row, col);
                    let cell = self.cells[idx];
                    let live_neighbors = self.live_neighbor_count(row, col);

                    let next_cell = match (cell, live_neighbors) {
                        // Rule 1: Any live cell with fewer than two live neighbours
                        // dies, as if caused by underpopulation.
                        (Cell::Alive, x) if x < 2 => Cell::Dead,
                        // Rule 2: Any live cell with two or three live neighbours
                        // lives on to the next generation.
                        (Cell::Alive, 2) | (Cell::Alive, 3) => Cell::Alive,
                        // Rule 3: Any live cell with more than three live
                        // neighbours dies, as if by overpopulation.
                        (Cell::Alive, x) if x > 3 => Cell::Dead,
                        // Rule 4: Any dead cell with exactly three live neighbours
                        // becomes a live cell, as if by reproduction.
                        (Cell::Dead, 3) => Cell::Alive,
                        // All other cells remain in the same state.
                        (otherwise, _) => otherwise,
                    };

                    next[idx] = next_cell;
                }
            }
        }

        self.cells = next;
    }
    /// Set the width of the universe.
    ///
    /// Resets all cells to the dead state.
    pub fn set_width(&mut self, width: u32) {
        self.width = width;
        self.cells = (0..width * self.height).map(|_i| Cell::Dead).collect();
    }

    /// Set the height of the universe.
    ///
    /// Resets all cells to the dead state.
    pub fn set_height(&mut self, height: u32) {
        self.height = height;
        self.cells = (0..self.width * height).map(|_i| Cell::Dead).collect();
    }
    // ...
}

impl fmt::Display for Universe {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        for line in self.cells.as_slice().chunks(self.width as usize) {
            for &cell in line {
                let symbol = if cell == Cell::Dead { '◻' } else { '◼' };
                write!(f, "{}", symbol)?;
            }
            write!(f, "\n")?;
        }

        Ok(())
    }
}


We can see major improvements there :

  • No need to use #[wasm_bindgen]
  • No need to use sys_web
  • No need to use log , Seed has one we can use if we want
  • No need to use utils module . Try to make a normal panic! and you see the line where the error appear in your rust code inside the web browser console :)
  • We merged the different impl for Universe together
  • We can directly unit test Universe if we want
  • We can directly benchmark Universe if we want

Now let's go to the core of the app

3 - Let's build the core of the app from Js to Rust.

Remember guys that we are translating from this repos which is my work when I followed this nice tutorial https://rustwasm.github.io/docs/book/introduction.html

==> to Rust/Seed

My original Js file is ugly because, well, I did not care about making it beautiful, create class or other file. And even on the example repos, the js file is messy :P.

Here is my original index.js


import { Universe, Cell } from "wasm-game-of-life";
// Import the WebAssembly memory at the top of the file.
import { memory } from "wasm-game-of-life/wasm_game_of_life_bg";
const CELL_SIZE = 5; // px
const GRID_COLOR = "#CCCCCC";
const DEAD_COLOR = "#FFFFFF";
const ALIVE_COLOR = "#000000";

// Construct the universe, and get its width and height.
let  universe = Universe.new();

// universe.set_height(100);
// universe.set_width(100);
universe.reset();
const width = universe.width();
const height = universe.height();


// Give the canvas room for all of our cells and a 1px border
// around each of them.
const canvas = document.getElementById("game-of-life-canvas");
canvas.height = (CELL_SIZE + 1) * height + 1;
canvas.width = (CELL_SIZE + 1) * width + 1;

const ctx = canvas.getContext('2d');
let animationId = null;
const fps = new class {
    constructor() {
        this.fps = document.getElementById("fps");
        this.frames = [];
        this.lastFrameTimeStamp = performance.now();
    }

    render() {
        // Convert the delta time since the last frame render into a measure
        // of frames per second.
        const now = performance.now();
        const delta = now - this.lastFrameTimeStamp;
        this.lastFrameTimeStamp = now;
        const fps = 1 / delta * 1000;

        // Save only the latest 100 timings.
        this.frames.push(fps);
        if (this.frames.length > 100) {
            this.frames.shift();
        }

        // Find the max, min, and mean of our 100 latest timings.
        let min = Infinity;
        let max = -Infinity;
        let sum = 0;
        for (let i = 0; i < this.frames.length; i++) {
            sum += this.frames[i];
            min = Math.min(this.frames[i], min);
            max = Math.max(this.frames[i], max);
        }
        let mean = sum / this.frames.length;

        // Render the statistics.
        this.fps.textContent = `
Frames per Second:
         latest = ${Math.round(fps)}
avg of last 100 = ${Math.round(mean)}
min of last 100 = ${Math.round(min)}
max of last 100 = ${Math.round(max)}
`.trim();
    }
};
const renderLoop = () => {
    fps.render();
    let ticks = document.getElementById("ticks").value;
    for (let i = 0; i <  ticks ; i++) {
        universe.tick();
    }
    drawGrid();
    drawCells();

 animationId = requestAnimationFrame(renderLoop);
};


const ultimateDeath = document.getElementById("death");

ultimateDeath.addEventListener("click", event=> {
  universe = Universe.death();
})

// const reset = document.getElementById("reset");
//
// reset.addEventListener("click", event=> {
//     universe = Universe.random();
// })
const isPaused = () => {
    return animationId === null;
};
const playPauseButton = document.getElementById("play-pause");

const play = () => {
    playPauseButton.textContent = "";
    renderLoop();
};

const pause = () => {
    playPauseButton.textContent = "";
    cancelAnimationFrame(animationId);
    animationId = null;
};

playPauseButton.addEventListener("click", event => {
    if (isPaused()) {
        play();
    } else {
        pause();
    }
});
const drawGrid = () => {
    ctx.beginPath();
    ctx.strokeStyle = GRID_COLOR;

    // Vertical lines.
    for (let i = 0; i <= width; i++) {

        ctx.moveTo(i * (CELL_SIZE + 1) + 1, 0);
        ctx.lineTo(i * (CELL_SIZE + 1) + 1, (CELL_SIZE + 1) * height + 1);
    }

    // Horizontal lines.
    for (let j = 0; j <= height; j++) {

        ctx.moveTo(0,                           j * (CELL_SIZE + 1) + 1);
        ctx.lineTo((CELL_SIZE + 1) * width + 1, j * (CELL_SIZE + 1) + 1);
    }

    ctx.stroke();
};
const getIndex = (row, column) => {
    return row * width + column;
};

const drawCells = () => {
    const cellsPtr = universe.cells();
    const cells = new Uint8Array(memory.buffer, cellsPtr, width * height);

    ctx.beginPath();

    // Alive cells.
    ctx.fillStyle = ALIVE_COLOR;
    for (let row = 0; row < height; row++) {
        for (let col = 0; col < width; col++) {
            const idx = getIndex(row, col);
            if (cells[idx] !== Cell.Alive) {
                continue;
            }

            ctx.fillRect(
                col * (CELL_SIZE + 1) + 1,
                row * (CELL_SIZE + 1) + 1,
                CELL_SIZE,
                CELL_SIZE
            );
        }
    }

// Dead cells.
    ctx.fillStyle = DEAD_COLOR;
    for (let row = 0; row < height; row++) {
        for (let col = 0; col < width; col++) {
            const idx = getIndex(row, col);
            if (cells[idx] !== Cell.Dead) {
                continue;
            }

            ctx.fillRect(
                col * (CELL_SIZE + 1) + 1,
                row * (CELL_SIZE + 1) + 1,
                CELL_SIZE,
                CELL_SIZE
            );
        }
    }

    ctx.stroke();
};


canvas.addEventListener("click", event => {
    const boundingRect = canvas.getBoundingClientRect();

    const scaleX = canvas.width / boundingRect.width;
    const scaleY = canvas.height / boundingRect.height;

    const canvasLeft = (event.clientX - boundingRect.left) * scaleX;
    const canvasTop = (event.clientY - boundingRect.top) * scaleY;

    const row = Math.min(Math.floor(canvasTop / (CELL_SIZE + 1)), height - 1);
    const col = Math.min(Math.floor(canvasLeft / (CELL_SIZE + 1)), width - 1);

    universe.toggle_cell(row, col);

    drawGrid();
    drawCells();
});



drawGrid();
drawCells();
play();

Here is the original index.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Hello wasm-pack!</title>
    <style>
        body {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
        }
        #fps {
            white-space: pre;
            font-family: monospace;
        }
    </style>
</head>
<body>
<div id="fps"></div>
<p>Tick settings:</p>

<div>
    <input type="range" id="ticks" name="ticks"
           min="0" max="10">
    <label for="ticks">Ticks</label>
</div>
<button id="reset">Random reset</button>
<button id="death">Ultimate death</button>
<button id="play-pause"></button>
<canvas id="game-of-life-canvas"></canvas>

<noscript>This page contains webassembly and javascript content, please enable javascript in your browser.</noscript>
<script src="./bootstrap.js"></script>
</body>
</html>


The content of index.js and the body of index.html will be written to pure Rust.

Seed uses a kind of pattern similar to Elm by the way.

What we need to do now

  • Define the content ( the state aka Model)
  • Define the view
  • Define how we change stuff ( update)
  • Define init

Then let's go !

Model

We need to :

  • Have width and height for canvas
  • Have Cell size
  • Have the universe
  • Know if we play or not
  • Display stuff about fps
  • Have a value for range ( I skip this one because I do it the dirty way in this tutorial, link at the end show you how it should be)
  • Have Colors for alive and dead
  • Have Color for grid

We focus at start on displaying the universe with pause/play and click on cell, so let's keep the fps & range for later :)

lib.rs

// `Model` describes our app state.
pub struct Model {
    cell_size: u32,
    grid_color: String,
    dead_color: String,
    alive_color: String,
    universe: Universe,
    pause: bool,
    canvas_height: u32,
    canvas_width: u32,
}

Let's init the stuff

// `init` describes what should happen when your app started.
fn init(_: Url, orders: &mut impl Orders<Msg>) -> Model {

    let universe = Universe::new();
    let cell_size = 5;
    let canvas_width = (cell_size + 1) * universe.width() + 1;
    let canvas_height = (cell_size + 1) * universe.height() + 1;

    Model {
        cell_size,
        grid_color: "#CCCCCC".to_string(),
        dead_color: "#FFFFFF".to_string(),
        alive_color: "#000000".to_string(),
        pause: false,
        universe,
        canvas_height,
        canvas_width,
    }
}

Let's list what's gonna happen in the app with our messages !


// `Msg` describes the different events you can modify state with.
enum Msg {
    /// We need to play the game
    Play,
    /// We need to pause
    Pause,
    /// We need to draw stuff
    Draw,
    /// We need to destroy the universe
    Destroy,
    /// We need to generate a random Universe
    Random,
    /// We need to click on a cell
    CellClick
}

This step of writing messages/events remembers me when I wrote actions/action_type in Redux/Ngrx.

It is a good process to force yourself to think about what you are actually doing and what's gonna happen.

Just for fun let's write update with empty match. We will focus on this later.

// `update` describes how to handle each `Msg`.
fn update(msg: Msg, model: &mut Model, _: &mut impl Orders<Msg>) {
    match msg {
        Msg::Play => {},
        Msg::Pause => {},
        Msg::Draw => {},
        Msg::Destroy => {},
        Msg::Random => {},
        Msg::CellClick => {}
    }
}

Let's write the view

// `view` describes what to display.


// `view` describes what to display.
fn view(model: &Model) -> Node<Msg> {
   section![
     button![
            id!("random"),
            ev(Ev::Click, |_| Msg::Random),
            "Random Reset"
        ],
        button![
            id!("destroy"),
            ev(Ev::Click, |_| Msg::Destroy),
            "Ultimate Death"
        ],
        button![
            id!("play-pause"),
            if model.pause {
                ev(Ev::Click, |_| Msg::Play)
            } else {
                ev(Ev::Click, |_| Msg::Pause)
            },
            if model.pause { "▶" } else { "⏸" }
        ],
        canvas![
            id!("game-of-life-canvas"),
            ev(Ev::Click, |event| {
                let mouse_event: web_sys::MouseEvent = event.unchecked_into();
                Msg::CellClick(mouse_event)
            })
        ],
    ]
}

Here there are few things to note :

  • As a user of Jetbrain Clion, I do not have any intellisense or strong color inside Macros so it is harder to code there.

Users of Visual Studio Code get a bit more lucky there because the intellisense and color work inside :D

  • We always need for now to encapsulate the code in one single node for Seed. I choose a section. At the end we will have 2 sections then.

  • We can use Rust code there which is an enormous benefit. So event if my intellisense and color code does not appear, all the stuff is backed up by our lovely compiler .D

Now if you refresh the page you should see something like this

Alt Text

We have some stuff to do now in the update function.

Draw our canvas ####

As explained at start, this tutorial is one brutal conversion from Js to Rust. I try to not be too much exotic :D.

When we draw, we actually draw cells and draw grid as well.

lib.rs


// `update` describes how to handle each `Msg`.
fn update(msg: Msg, model: &mut Model, _: &mut impl Orders<Msg>) {
    match msg {
        Msg::Play => {},
        Msg::Pause => {},
        Msg::Draw => {
            if model.pause {
            } else {
                model.universe.tick();
                draw_grid(model);
                draw_cells(model);           
            }
        },
        Msg::Destroy => {},
        Msg::Random => {},
        Msg::CellClick(event) => {}
    }
}

fn draw_grid(model: &mut Model) {
// equivalent to js const canvas = document.getElementById("game-of-life-canvas");
// could be written let canvas document().get_element_by_id("game-of-life-canvas");
// I used a shortcut here which is fine.
// There is even a better way that I will show you later
    let canvas = canvas("game-of-life-canvas").unwrap(); 
    canvas.set_width(model.canvas_width);
    canvas.set_height(model.canvas_height);
    let ctx = seed::canvas_context_2d(&canvas);
    ctx.begin_path();
    ctx.set_stroke_style(&JsValue::from_str(model.grid_color.as_str()));

    // Vertical lines.
    for i in 0..model.universe.width() {
        ctx.move_to((i * (model.cell_size + 1) + 1).into(), 0.);
        ctx.line_to(
            (i * (model.cell_size + 1) + 1).into(),
            ((model.cell_size + 1) * model.universe.height() + 1).into(),
        );
    }
    // Horizontal lines.
    for j in 0..model.universe.height() {
        ctx.move_to(0., (j * (model.cell_size + 1) + 1).into());
        ctx.line_to(
            ((model.cell_size + 1) * model.universe.width() + 1).into(),
            (j * (model.cell_size + 1) + 1).into(),
        )
    }

    ctx.stroke();
}

fn draw_cells(model: &mut Model) {
    let canvas = canvas("game-of-life-canvas").unwrap();
    let ctx = seed::canvas_context_2d(&canvas);
    ctx.begin_path();

    // Alive cells.
    ctx.set_fill_style(&JsValue::from_str(model.alive_color.as_str()));
    for row in 0..model.universe.height() {
        for col in 0..model.universe.width() {
            let idx = model.universe.get_index(row, col);
            if model.universe.cell_at_index(idx) != Cell::Alive {
                continue;
            }

            ctx.fill_rect(
                (col * (model.cell_size + 1) + 1).into(),
                (row * (model.cell_size + 1) + 1).into(),
                (model.cell_size).into(),
                (model.cell_size).into(),
            );
        }
    }

    // Dead cells.
    ctx.set_fill_style(&JsValue::from_str(model.dead_color.as_str()));
    for row in 0..model.universe.height() {
        for col in 0..model.universe.width() {
            let idx = model.universe.get_index(row, col);
            if model.universe.cell_at_index(idx) != Cell::Dead {
                continue;
            }

            ctx.fill_rect(
                (col * (model.cell_size + 1) + 1).into(),
                (row * (model.cell_size + 1) + 1).into(),
                (model.cell_size).into(),
                (model.cell_size).into(),
            );
        }
    }

    ctx.stroke();
}

You need to update the import on top of your file use crate::universe::{Universe, Cell};

Well, I just converted the code in lazy way without thinking much.
As explained in comment there are different way to get the canvas from the DOM.
Later I ll show you the right way.

NB : wait there are weird stuff there :

  • We draw in theory the stuff, but only if we get a message Msg::Draw
  • Where is animationId = requestAnimationFrame(renderLoop); ? There is no loop anymore

Héhé, technically we can use request_animation_frame , your intellisense will find all the API you have in Javascript as long as you use the snake_case Rust Style,

but you will also see that it has been deprecated. Even if I am lazy, I do not want to use deprecated stuff.

There are a set of some side effect with some standard methods. Seed has been making new tools to fit their pattern and give us more control.

Let's introduce Orders

Looks like Reactive programming to me. We can use them to queue messages in our case.

  • start drawing on init
  • continue drawing

lib.rs


fn init(_: Url, orders: &mut impl Orders<Msg>) -> Model {
    orders.after_next_render(|_| Msg::Draw);

//... the other stuff from before
}

lib.rs


fn update(msg: Msg, model: &mut Model, _: &mut impl Orders<Msg>) {
    match msg {
        Msg::Play => {},
        Msg::Pause => {},
        Msg::Draw => {
            if model.pause {
            } else {
                model.universe.tick();
                draw_grid(model);
                draw_cells(model);
                orders.after_next_render(|_| Msg::Draw);
            }
        },
        Msg::Destroy => {},
        Msg::Random => {},
        Msg::CellClick(event) => {}
    }
}

In both cases we just do what the code says. Simple & easy.

Refresh your page ( hot reloading is coming soon) and you should see life moving.

We need some css there that we can put in index.html inside the <head> INSERT THERE </head>


    <style>
        section {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
        }
    </style>

We need to target section since we used it in the view and voila !

Alt Text

The cells should be moving :D

NB:

  • The border on bottom and right seems to be eaten a bit ( probably because of the conversion I am doing)
  • The grid looks like a bit different that the one from the original ( Because same reason as above I think)

I am a baby Rustacean, so I did things quick & dirty. My code produces inaccurate values .

Before we go further, let's move the draw function in a file, let's call draw.rs

No code to show this time, you'll fix the import and the stuff by yourself :P

Destroy & Generate Random

Just need to call the universe::death() & universe::random() in the update function and that's it !

Play & Pause

You can do it by yourself.

Just make sure to send an other msg::Draw when it is playing time :P

Click on a cell

Still in lazy mode we just convert he old Js to Rust.


/// `update` describes how to handle each `Msg`.
fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
    match msg {
        Msg::Draw => draw::draw(model, orders),
        Msg::Play => {
            model.pause = false;
            orders.after_next_render(|_| Msg::Draw);
        }
        Msg::Pause => model.pause = true,
        Msg::Destroy => model.universe = Universe::death(),
        Msg::Random => model.universe = Universe::random(),
        Msg::CellClick(event) => {
            let canvas = canvas("game-of-life-canvas").unwrap();
            let bounding_rect = canvas.get_bounding_client_rect();

            let scale_x: f64 = f64::from(canvas.width()) / bounding_rect.width();
            let scale_y: f64 = f64::from(canvas.height()) / bounding_rect.height();

            let canvas_left: f64 = (f64::from(event.client_x()) - bounding_rect.left()) * scale_x;
            let canvas_top: f64 = (f64::from(event.client_y()) - bounding_rect.top()) * scale_y;

            let row_pos: f64 = (canvas_top / f64::from(model.cell_size + 1)).floor();
            let col_pos: f64 = (canvas_left / f64::from(model.cell_size + 1)).floor();

            let row: u32 = cmp::min(row_pos as u32, model.universe.height() - 1);
            let col: u32 = cmp::min(col_pos as u32, model.universe.width() - 1);

            model.universe.toggle_cell(row, col);
        }
    }
}

Now you can try to fix the import but you end up with an error

error[E0599]: no method named `get_bounding_client_rect` found for struct `seed::prelude::web_sys::HtmlCanvasElement` in the current scope
  --> src/lib.rs:78:40
   |
78 |             let bounding_rect = canvas.get_bounding_client_rect();
   |                                        ^^^^^^^^^^^^^^^^^^^^^^^^ method not found in `seed::prelude::web_sys::HtmlCanvasElement

Well ... what is happening there...?

I tell you why :D

Remember that Rust is made for being performant, so it let you in this context choose the stuff that you want to compile for use.

So your binaries are smaller, your IDE is also quicker to search for stuff, your web browser will not make your PC out of RAM because of x thousands of package/files ( cf node_modules ) and so many more good stuff to say about this strategy.

Cargo.toml

#stuff from before still there
[dependencies.web-sys]
version = "0.3"
features=[ "DomRect", "Element"]

Now it will work. The cargo make watch should have recompiled for you :) and you can click on a cell.

Let's move the code to click on cell to draw.rs

I renamed draw.rs to canvas.rs.

I let you make the necessary change there :P

Regarding the import, I have 2 choices for cmp

  • use seed::prelude::wasm_bindgen::__rt::std::cmp;
  • use std::cmp;

I do not know the difference between them really. I need to ask to the other more experienced Seed people. Are we gardeners ? I have so many useless jokes :( .

Choose how many ticks per frame

Okay there, from the original tutorial, I did implemented the range to choose how many ticks per frame rendering we want.

Let's start with the view because it is the easier to convert ( maybe).
I needed to check some examples from Seed repo because macro stuff is not easy without intellisense for me.

lib.rs

// `view` describes what to display.
fn view(model: &Model) -> Node<Msg> {
    section![
        p!["Ticks settings :"],
        div![
            input![
                id!("ticks"),
                1,
                attrs! {
                    At::Name => "ticks",
                    At::Type => "range",
                    At::Min =>"1",
                    At::Max =>"10"
                }
            ],
            label![attrs! { At::For => "ticks"}, "ticks"]
        ],
        button![
            id!("random"),
            ev(Ev::Click, |_| Msg::Random),
            "Random Reset"
        ],
        button![
            id!("destroy"),
            ev(Ev::Click, |_| Msg::Destroy),
            "Ultimate Death"
        ],
        button![
            id!("play-pause"),
            if model.pause {
                ev(Ev::Click, |_| Msg::Play)
            } else {
                ev(Ev::Click, |_| Msg::Pause)
            },
            if model.pause { "▶" } else { "⏸" }
        ],
        canvas![
            id!("game-of-life-canvas"),
            ev(Ev::Click, |event| {
                let mouse_event: web_sys::MouseEvent = event.unchecked_into();
                Msg::CellClick(mouse_event)
            })
        ],
    ]
}

There are a better way to do the range and the state that goes within. I will show the recommended way later. Let's make it quick & dirt for now.

lib.rs

// `update` describes how to handle each `Msg`.
fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
    match msg {
        Msg::Play => {
            model.pause = false;
            orders.after_next_render(|_| Msg::Draw);
        }
        Msg::Pause => model.pause = true,
        Msg::Draw => {
            if model.pause {
            } else {
                let tick_input = document().get_element_by_id("ticks").unwrap();
                let tick_frequency = get_value(tick_input.as_ref()).unwrap();
                let tick_number = tick_frequency.parse::<u32>().unwrap();

                for i in 0..tick_number {
                    model.universe.tick();
                }
                canvas::draw_grid(model);
                canvas::draw_cells(model);
                orders.after_next_render(|_| Msg::Draw);
            }
        }
        Msg::Destroy => {
            model.universe = Universe::death();
        }
        Msg::Random => {
            model.universe = Universe::random();
        }
        Msg::CellClick(event) => {
            let position = canvas::find_cell_from_click(model, event);

            model.universe.toggle_cell(position.0, position.1);
        }
    }
}

The part to take out the value is not as small as in Js because of Rust :

  • No inheritance
  • Need to unwrap stuff because we have result and possibility to handle errors
  • Need to cast String to u32

Even if we have a little bit of more to do there, we have also more control.
Remember that this way I wrote it is not the best nor the recommanded way. We can improve this a lot.

You can change the range and see how it goes. In my case I felt that the seeded-game-of-life perform ( or tiny better if many tabs opened / the same ) at least in standard build / --debug than the old.

Display FPS

Let's now display the number of Frames Per Second as shown in the original tutorial.

Jut add div![id!["fps"]], in fn view() body before p!["Ticks settings :"],

Now, in the original Javascript file we had a Class like this for me

const fps = new class {
    constructor() {
        this.fps = document.getElementById("fps");
        this.frames = [];
        this.lastFrameTimeStamp = performance.now();
    }

    render() {
        // Convert the delta time since the last frame render into a measure
        // of frames per second.
        const now = performance.now();
        const delta = now - this.lastFrameTimeStamp;
        this.lastFrameTimeStamp = now;
        const fps = 1 / delta * 1000;

        // Save only the latest 100 timings.
        this.frames.push(fps);
        if (this.frames.length > 100) {
            this.frames.shift();
        }

        // Find the max, min, and mean of our 100 latest timings.
        let min = Infinity;
        let max = -Infinity;
        let sum = 0;
        for (let i = 0; i < this.frames.length; i++) {
            sum += this.frames[i];
            min = Math.min(this.frames[i], min);
            max = Math.max(this.frames[i], max);
        }
        let mean = sum / this.frames.length;

        // Render the statistics.
        this.fps.textContent = `
Frames per Second:
         latest = ${Math.round(fps)}
avg of last 100 = ${Math.round(mean)}
min of last 100 = ${Math.round(min)}
max of last 100 = ${Math.round(max)}
`.trim();
    }
};

The variable was actually used only one time in index.js in renderLoop()

const renderLoop = () => {
    fps.render();
    let ticks = document.getElementById("ticks").value;
    for (let i = 0; i <  ticks ; i++) {
        universe.tick();
    }
    drawGrid();
    drawCells();

 animationId = requestAnimationFrame(renderLoop);
};

Then let's do the same. Let's use fps inside Msg::Draw and instead of a Class, let's use a struct and impl.

Create fps.rs and make some better code.

Since we want to expose fps in lib.rs, let's try to minimize calls to the Web Browser api and focus on calculation.

We can change the render method and call it calculate to return the statistics.

fps.rs

use seed::window;
use std::cmp;

pub struct FpsCounter {
    frames: Vec<f64>,
    last_frame_timestamp: f64,
}

impl FpsCounter {
    pub fn new() -> FpsCounter {
        FpsCounter {
            frames: Vec::new(),
            last_frame_timestamp: window().performance().unwrap().now(), //should have it outside so it would be  more beautiful
        }
    }
    /// Ex- Render function
    /// Same as original in JS , I moved most of call to DOM outside to make it "cleaner"
    /// Maybe I could also have passed time as an argument to make it even better
    /// If we removed calls to window() we could make unit test and benchmark    
        pub fn calculate(&mut self) -> FpsStatistic {
        let now = window().performance().unwrap().now();
        let delta = now - self.last_frame_timestamp;
        self.last_frame_timestamp = now;

        let fps = 1. / delta * 1000.;

        self.frames.push(fps);

        if self.frames.len() > 100 {
            self.frames.remove(0);
        }

        let mut min = i32::MAX;
        let mut max = i32::MIN;

        let mut sum: f64 = 0.;

        for i in 0..self.frames.len() {
            sum = sum + self.frames[i] as f64;

            min = cmp::min(self.frames[i] as i32, min);

            max = cmp::max(self.frames[i] as i32, max);
        }
        let mean = sum / self.frames.len() as f64;

        FpsStatistic {
            fps: fps as u32,
            mean: mean as u32,
            min,
            max,
        }
    }
}

pub struct FpsStatistic {
    pub fps: u32,
    pub mean: u32,
    pub min: i32,
    pub max: i32,
}

To have access to the fps at anytime let's add it to the state :)

lib.rs


// `init` describes what should happen when your app started.
fn init(_: Url, orders: &mut impl Orders<Msg>) -> Model {
    orders.after_next_render(|_| Msg::Draw);

    let universe = Universe::new();
    let cell_size = 5;
    let canvas_width = (cell_size + 1) * universe.width() + 1;
    let canvas_height = (cell_size + 1) * universe.height() + 1;

    Model {
        cell_size,
        grid_color: "#CCCCCC".to_string(),
        dead_color: "#FFFFFF".to_string(),
        alive_color: "#000000".to_string(),
        pause: false,
        universe,
        canvas_height,
        canvas_width,
        fps:FpsCounter::new()
    }
}

// `Model` describes our app state.
pub struct Model {
    cell_size: u32,
    grid_color: String,
    dead_color: String,
    alive_color: String,
    universe: Universe,
    pause: bool,
    canvas_height: u32,
    canvas_width: u32,
    fps: FpsCounter,
}

Then we can add this code in Msg::Draw just before the code to get value for tick frequencies

lib.rs

 let fps = document().get_element_by_id("fps").unwrap();
                let stats = model.fps.calculate();
                let text = format!(
                    "\
                Frames per Second:
         latest = {:?}
avg of last 100 = {:?}
min of last 100 = {:?}
max of last 100 = {:?}
                \
                ",
                    stats.fps, stats.mean, stats.min, stats.max
                );

                fps.set_text_content(Some(text.as_str()));

We need some css , let's use the same from he original tutorial, so inside the <style> in index.html

      #fps {
            white-space: pre;
            font-family: monospace;
        }

You should have something nice now :)

Alt Text

Well Let's resume few Pros & Cons from what we have compare to the original source code :

Benefits :

  • One language for everything <3
  • Only few dependencies
  • You become productive very quickly
  • Easier to split and structure the code => the code looks so much better visually
  • More control about what is happening ( more typing/syntax/tools available/IDE support )
  • We get state management for free without extra library/logic : It's included & Easy !
  • The compiler checks everything, including the Macros/html-like stuff.
  • No nodes_modules & no javascript dependencies
  • My web browser does not eat my RAM when I open the web browser dev tools
  • Choose what you compile when you need in Cargo.toml
  • & many more positive statements <3

Cost

  • You need to at least be baby Rustacean to do stuff or having somebody near by.
  • The IDE intellisense does not work everywhere ( but in Js it is also failing by catching actually too much and flooding the IDE )
  • The Typing does not always come up from web_sys stuff/Seed
  • I cannot see the content of package.wasm in the web browser, but I can in the original tutorial
  • Not sure I can debug at run time
  • Please add some pain that you feel in your comment and I will add it there :)
  • We have more generated Js inside our package.js

In the next part of this serie, we will see how to check the performance and conclude the move from wasm+js to rust/seed.

Discussion (5)

pic
Editor guide
Collapse
roka profile image
Robert Katzki

Thanks for the write-up! Started to learn Rust and am really curios on WASM.

Would love to know the difference in file size (gzipped and not) from JS to WASM and do you have some performance values?

Collapse
arnthelongbeard profile image
arn-the-long-beard Author • Edited

Hello ! Thank you for your comments :)

Regarding the size, we have a big difference there.

The seeded version of the game is much bigger in Wasm.

With cargo make build_release

wc -c pkg/package_bg.wasm -> 368597 pkg/package_bg.wasm

gzip -9 < pkg/package_bg.wasm | wc -c -> 142825

I did not succeed to do the same command on my original Js+Wasm project, but on a clone of the available source with same options in Cargo.toml ( so there not the exercises like Random Universe and others)

wasm-pack build --release

wc -c pkg/wasm_game_of_life_bg.wasm -> 40271 pkg/wasm_game_of_life_bg.wasm
My original was around 47K from what I remember.

gzip -9 < pkg/wasm_game_of_life_bg.wasm | wc -c
-> 17498

Can be interesting to compare the total loaded in the web browser between the 2 projects.

Regarding the performance, a nice way to compare could be to increase a lot the size of the grid, but for now I do not have any metrics. I am gonna investigate to have an idea :)

Collapse
roka profile image
Robert Katzki

Thanks! A comparison to the same in just JS would be interesting to see whether it’s worth the effort to use WASM.

Thread Thread
arnthelongbeard profile image
arn-the-long-beard Author

When I push a lot the size of the grid, I get better performance on wasm+js than Seed, but that is mainly because I did not optimize the Seed code.

But the code for me is still much easier to work with, update & maintain in pure Rust than js :)

Collapse
arnthelongbeard profile image
arn-the-long-beard Author

Hey guys, I just try this functionalities called series. It looks nice. Maybe I can split even more :D