feat(initial): initial commit

functional game of life with egui in native and wasm (build with trunk)
main
flavien 2022-12-21 16:10:42 +01:00
commit 74e626da69
10 changed files with 3703 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

7
.pre-commit-config.yaml Normal file
View File

@ -0,0 +1,7 @@
repos:
- repo: https://github.com/doublify/pre-commit-rust
rev: v1.0
hooks:
- id: fmt
- id: cargo-check
- id: clippy

3
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"liveServer.settings.root": "/dist"
}

1997
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

26
Cargo.toml Normal file
View File

@ -0,0 +1,26 @@
[package]
name = "wasm_game_of_life"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
egui = "0.20.0"
eframe = { version = "0.20.0", default-features = false, features = [
"default_fonts", # Embed the default egui fonts.
"glow", # Use the glow rendering backend. Alternative: "wgpu".
] }
rand = "0.8.5"
# native:
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
tracing-subscriber = "0.3"
# web:
[target.'cfg(target_arch = "wasm32")'.dependencies]
console_error_panic_hook = "0.1.6"
tracing-wasm = "0.2"
wasm-bindgen-futures = "0.4"
getrandom = { version = "0.2", features = ["js"] }

8
dist/index.html vendored Normal file
View File

@ -0,0 +1,8 @@
<html><head>
<link rel="preload" href="/wasm_game_of_life-2b18a98f9a6744b5_bg.wasm" as="fetch" type="application/wasm" crossorigin="">
<link rel="modulepreload" href="/wasm_game_of_life-2b18a98f9a6744b5.js"></head>
<body>
<canvas id="canvas" style="position: absolute; top: 0%; left: 0%; width: 1600px; height: 900px;"></canvas>
<script type="module">import init from '/wasm_game_of_life-2b18a98f9a6744b5.js';init('/wasm_game_of_life-2b18a98f9a6744b5_bg.wasm');</script></body></html>

1397
dist/wasm_game_of_life-2b18a98f9a6744b5.js vendored Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

7
index.html Normal file
View File

@ -0,0 +1,7 @@
<html>
<head>
</head>
<body>
<canvas id="canvas" style="position: absolute; top: 0%; left: 0%; width: 1600px; height: 900px;"></canvas>
</body>
</html>

257
src/main.rs Normal file
View File

@ -0,0 +1,257 @@
use std::fmt;
use eframe::epaint::RectShape;
use rand::Rng;
use egui::{Sense, Vec2, Color32, Stroke, pos2, Rect, Rounding, Shape};
#[repr(u8)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Cell {
Dead = 0,
Alive = 1,
}
pub struct Universe {
width: u32,
height: u32,
age: u32,
cells: (Vec<Cell>, Vec<Cell>)
}
impl Universe {
pub fn new(width: u32, height: u32) -> Universe {
let mut rng = rand::thread_rng();
let cells = (0..width * height)
.map(|_| {
if rng.gen_range(0..10) > 5 {
Cell::Alive
} else {
Cell::Dead
}
})
.collect::<Vec<Cell>>();
Universe {
width,
height,
age: 0,
cells: (cells.clone(), cells),
}
}
// pub fn render(&self) -> String {
// self.to_string()
// }
pub fn render(&self, canvas_origin: Vec2, canvas_size: Vec2) -> Vec<Shape> {
let fill = Color32::from_gray(128);
let rounding = Rounding::none();
let cell_side = canvas_size.x / self.width as f32;
let stroke = Stroke::NONE;
let mut rectangles_cache = Vec::new();
let current = match self.age % 2 {
0 => &self.cells.0,
_ => &self.cells.1
};
let mut width_counter = 0;
let mut width_origin = 0.0;
for row in 0..self.height {
for col in 0..self.width {
let idx = Universe::get_index(self.width, row, col);
if let Cell::Alive = current[idx] {
if width_counter == 0 {
width_origin = col as f32 * cell_side;
}
width_counter += 1;
} else if width_counter != 0 {
let y = row as f32 * cell_side;
let rect = Rect::from_min_size(pos2(width_origin, y) + canvas_origin, Vec2::new(cell_side*width_counter as f32, cell_side));
rectangles_cache.push(Shape::Rect(RectShape {rect, rounding, fill, stroke}));
width_counter = 0;
}
}
}
return rectangles_cache;
}
fn get_index(width: u32, row: u32, column: u32) -> usize {
(row * width + column) as usize
}
fn live_neighbor_count(universe: &Vec<Cell>, height:u32, width: u32, row: u32, column: u32) -> u8 {
let mut count = 0;
for delta_row in [height - 1, 0, 1] {
for delta_col in [width - 1, 0, 1] {
if delta_row == 0 && delta_col == 0 {
continue;
}
let neighbor_row = (row + delta_row) % height;
let neighbor_col = (column + delta_col) % width;
let idx = Universe::get_index(width, neighbor_row, neighbor_col);
count += universe[idx] as u8;
}
}
count
}
pub fn tick(&mut self) {
let (next, current) = match self.age % 2 {
0 => (&mut self.cells.0, &self.cells.1),
_ => (&mut self.cells.1, &self.cells.0),
};
for row in 0..self.height {
for col in 0..self.width {
let idx = Universe::get_index(self.width, row, col);
let cell = current[idx];
let live_neighbors = Universe::live_neighbor_count(current, self.height, self.width, 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.age += 1;
}
}
impl fmt::Display for Universe {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let current = match self.age % 2 {
0 => &self.cells.0,
_ => &self.cells.1
};
for line in current.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(())
}
}
const CANVAS_WIDTH: f32 = 1600.0;
const CANVAS_HEIGHT: f32 = 900.0;
#[cfg(target_arch = "wasm32")]
fn main() {
// Make sure panics are logged using `console.error`.
console_error_panic_hook::set_once();
tracing_wasm::set_as_global_default();
let web_options = eframe::WebOptions::default();
wasm_bindgen_futures::spawn_local(async {
eframe::start_web(
// this is the id of the `<canvas>` element we have
// in our `index.html`
"canvas",
web_options,
Box::new(|cc| Box::new(MyEguiApp::new(cc))),
)
.await
.expect("failed to start eframe");
});
}
#[cfg(not(target_arch = "wasm32"))]
fn main() {
use egui::vec2;
let options = eframe::NativeOptions {
initial_window_size: Some(vec2(CANVAS_WIDTH, CANVAS_HEIGHT)),
resizable: false,
..Default::default()
};
eframe::run_native("My egui App", options, Box::new(|cc| Box::new(MyEguiApp::new(cc))));
}
#[repr(u8)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Speed {
Slow = 16,
Normal = 8,
Fast = 4,
Fastest = 2
}
impl Default for Speed {
fn default() -> Self {
Speed::Fastest
}
}
#[repr(u32)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Size {
Small = 8,
Medium = 4,
Large = 2,
}
impl Default for Size {
fn default() -> Self {
Size::Large
}
}
struct MyEguiApp {
game: Universe,
frame_counter: u8,
speed: Speed,
size: Size,
cache: Vec<Shape>,
}
impl MyEguiApp {
fn new(_cc: &eframe::CreationContext<'_>) -> Self {
// Customize egui here with cc.egui_ctx.set_fonts and cc.egui_ctx.set_visuals.
// Restore app state using cc.storage (requires the "persistence" feature).
// Use the cc.gl (a glow::Context) to create graphics shaders and buffers that you can use
// for e.g. egui::PaintCallback.
let size = Size::default();
Self {
game: Universe::new(CANVAS_WIDTH as u32 / size as u32, CANVAS_HEIGHT as u32 / size as u32),
frame_counter: 0,
speed: Speed::default(),
size,
cache: Vec::new(),
}
}
}
impl eframe::App for MyEguiApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
const PAINTER_SIZE: Vec2 = egui::vec2(CANVAS_WIDTH, CANVAS_HEIGHT);
egui::CentralPanel::default().show(ctx, |ui| {
// add click sense later for interaction
let (response, painter) = ui.allocate_painter(PAINTER_SIZE, Sense::hover());
let origin = response.rect.left_top().to_vec2();
if self.frame_counter % self.speed as u8 == 0 {
self.game.tick();
self.cache = self.game.render(origin, PAINTER_SIZE);
}
painter.extend(self.cache.iter().cloned());
});
self.frame_counter += 1;
self.frame_counter %= self.speed as u8;
ctx.request_repaint();
}
}