summary refs log tree commit diff
path: root/src/lib.rs
diff options
context:
space:
mode:
authorMalte Voos <git@mal.tc>2023-04-02 12:24:23 +0200
committerMalte Voos <git@mal.tc>2023-04-02 12:24:23 +0200
commit58b1fced2563f40990123ab362b0df53b5a91c0e (patch)
tree84faaa57a23633041baffe5d50467e60abc9d5e9 /src/lib.rs
parent7d9836ebef1950c550081b2ea9cd3f7718280a02 (diff)
downloadlife-58b1fced2563f40990123ab362b0df53b5a91c0e.tar.gz
life-58b1fced2563f40990123ab362b0df53b5a91c0e.zip
make it run in the browser
Diffstat (limited to 'src/lib.rs')
-rw-r--r--src/lib.rs308
1 files changed, 308 insertions, 0 deletions
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<dyn FnMut(_)>);
+        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<Vec<Cell>>,
+}
+
+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<Cell> for Standard {
+    fn sample<R: rand::Rng + ?Sized>(&self, rng: &mut R) -> Cell {
+        if rng.gen() {
+            Cell::Alive
+        } else {
+            Cell::Dead { since: 0xff }
+        }
+    }
+}