From 58b1fced2563f40990123ab362b0df53b5a91c0e Mon Sep 17 00:00:00 2001 From: Malte Voos Date: Sun, 2 Apr 2023 12:24:23 +0200 Subject: make it run in the browser --- src/lib.rs | 308 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 308 insertions(+) create mode 100644 src/lib.rs (limited to 'src/lib.rs') diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..cbac12e --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,308 @@ +use std::rc::Rc; + +use pixels::wgpu::Color; +use pixels::PixelsBuilder; +use pixels::SurfaceTexture; +use rand::distributions::Distribution; +use rand::distributions::Standard; +use wasm_bindgen::prelude::*; +use winit::dpi::LogicalSize; +use winit::event::Event; +use winit::event_loop::EventLoop; +use winit::window::WindowBuilder; +use winit_input_helper::WinitInputHelper; + +// const WIDTH: usize = 256; +// const HEIGHT: usize = 256; + +#[wasm_bindgen(start)] +async fn start() -> Result<(), JsValue> { + return run().await.map_err(|err| JsValue::from(err.to_string())); +} + +async fn run() -> anyhow::Result<()> { + let event_loop = EventLoop::new(); + + // let size = LogicalSize { + // width: WIDTH as f64, + // height: HEIGHT as f64, + // }; + + let window = WindowBuilder::new() + // .with_inner_size(size) + // .with_min_inner_size(size) + .build(&event_loop)?; + + let window = Rc::new(window); + + #[cfg(target_arch = "wasm32")] + { + use winit::platform::web::WindowExtWebSys; + + // Retrieve current width and height dimensions of browser client window + let get_window_size = || { + let client_window = web_sys::window().unwrap(); + LogicalSize::new( + client_window.inner_width().unwrap().as_f64().unwrap(), + client_window.inner_height().unwrap().as_f64().unwrap(), + ) + }; + + let window = Rc::clone(&window); + + // Initialize winit window with current dimensions of browser client + window.set_inner_size(get_window_size()); + + let client_window = web_sys::window().unwrap(); + + // Attach winit canvas to body element + web_sys::window() + .and_then(|win| win.document()) + .and_then(|doc| doc.body()) + .and_then(|body| { + body.append_child(&web_sys::Element::from(window.canvas())) + .ok() + }) + .expect("couldn't append canvas to document body"); + + // Listen for resize event on browser client. Adjust winit window dimensions + // on event trigger + let closure = wasm_bindgen::closure::Closure::wrap(Box::new(move |_e: web_sys::Event| { + let size = get_window_size(); + window.set_inner_size(size) + }) as Box); + client_window + .add_event_listener_with_callback("resize", closure.as_ref().unchecked_ref()) + .unwrap(); + closure.forget(); + } + + let mut input = WinitInputHelper::new(); + + let window_size = window.inner_size(); + let surface_texture = + SurfaceTexture::new(window_size.width, window_size.height, window.as_ref()); + + let width = window_size.width / 8; + let height = window_size.height / 8; + + web_sys::console::log_1(&format!("{} x {}", width, height).into()); + + let mut pixels = PixelsBuilder::new(width, height, surface_texture) + .clear_color(Color { + r: 0.0, + g: 0.0, + b: 0.0, + a: 1.0, + }) + .build_async() + .await?; + let mut life = Life::new(width as usize, height as usize); + + event_loop.run(move |event, _, control_flow| { + control_flow.set_poll(); + + if let Event::RedrawRequested(_) = event { + life.draw(pixels.frame_mut()); + pixels.render().expect("failed to render"); + } + + if input.update(&event) { + if input.close_requested() || input.destroyed() { + control_flow.set_exit(); + return; + }; + + life.update(); + window.request_redraw(); + }; + }); +} + +#[derive(Clone, Copy, Debug)] +enum Cell { + Alive, + Dead { since: u8 }, +} + +#[derive(Debug)] +struct Grid { + pub width: usize, + pub height: usize, + pub cells: Vec>, +} + +enum FrontGrid { + A, + B, +} + +struct Life { + pub width: usize, + pub height: usize, + grid_a: Grid, + grid_b: Grid, + front_grid: FrontGrid, +} + +impl Cell { + pub fn is_alive(&self) -> bool { + match self { + Cell::Alive => true, + Cell::Dead { .. } => false, + } + } +} + +impl Grid { + pub fn blank(width: usize, height: usize) -> Self { + Self { + width, + height, + cells: vec![vec![Cell::Dead { since: 0xff }; width]; height], + } + } + + pub fn with_random_borders(width: usize, height: usize) -> Self { + let mut ret = Self::blank(width, height); + ret.randomize_border(); + ret + } + + pub fn randomize_border(&mut self) { + for col in 0..self.width { + self.cells[self.height - 1][col] = rand::random(); + } + } + + pub fn draw(&self, frame: &mut [u8]) { + for row in 0..self.height { + for col in 0..self.width { + let pixel_idx = 4 * (self.width * row + col); + + match self.cells[row][col] { + Cell::Alive => { + frame[pixel_idx + 0] = 0xff; // R + frame[pixel_idx + 1] = 0xff; // G + frame[pixel_idx + 2] = 0xff; // B + frame[pixel_idx + 3] = 0xff; // A + } + Cell::Dead { since } => { + frame[pixel_idx + 0] = 0xff - since.saturating_mul(16); // R + frame[pixel_idx + 1] = if since < 0x8 { + 0 + } else { + (since - 0x8).saturating_mul(0x8) / 2 + }; + // G + frame[pixel_idx + 2] = 0xff; // B + frame[pixel_idx + 3] = 0xff - since; // A + } + }; + } + } + } + + pub fn num_alive_neighbors(&self, row: usize, col: usize) -> u8 { + let mut ret = 0; + + for i in row.saturating_sub(1)..=(row + 1).min(self.height - 1) { + for j in col.saturating_sub(1)..=(col + 1).min(self.width - 1) { + if self.cells[i][j].is_alive() { + ret += 1; + } + } + } + if self.cells[row][col].is_alive() { + ret -= 1; + } + + ret + } + + pub fn new_state(&self, row: usize, col: usize) -> Cell { + match self.cells[row][col] { + Cell::Alive => { + if (2..=3).contains(&self.num_alive_neighbors(row, col)) { + Cell::Alive + } else { + Cell::Dead { since: 0 } + } + } + Cell::Dead { since } => { + if self.num_alive_neighbors(row, col) == 3 { + Cell::Alive + } else { + Cell::Dead { + since: since.saturating_add(1), + } + } + } + } + } +} + +impl Life { + pub fn new(width: usize, height: usize) -> Self { + Life { + width, + height, + grid_a: Grid::with_random_borders(width, height), + grid_b: Grid::blank(width, height), + front_grid: FrontGrid::A, + } + } + + pub fn draw(&self, frame: &mut [u8]) { + self.front_grid().draw(frame); + } + + pub fn update(&mut self) { + let (width, height) = self.dimensions(); + let (front, back) = self.grids(); + + for row in 0..height { + for col in 0..width { + back.cells[row][col] = front.new_state(row, col); + } + } + back.randomize_border(); + + self.swap_grids(); + } + + fn dimensions(&self) -> (usize, usize) { + (self.width, self.height) + } + + fn grids(&mut self) -> (&Grid, &mut Grid) { + match self.front_grid { + FrontGrid::A => (&self.grid_a, &mut self.grid_b), + FrontGrid::B => (&self.grid_b, &mut self.grid_a), + } + } + + fn front_grid(&self) -> &Grid { + match self.front_grid { + FrontGrid::A => &self.grid_a, + FrontGrid::B => &self.grid_b, + } + } + + fn swap_grids(&mut self) { + self.front_grid = match self.front_grid { + FrontGrid::A => FrontGrid::B, + FrontGrid::B => FrontGrid::A, + } + } +} + +impl Distribution for Standard { + fn sample(&self, rng: &mut R) -> Cell { + if rng.gen() { + Cell::Alive + } else { + Cell::Dead { since: 0xff } + } + } +} -- cgit 1.4.1