From 2ee196f43d9930a280a16889853e14cee39f1acf Mon Sep 17 00:00:00 2001 From: Jet Pham Date: Wed, 4 Mar 2026 23:56:16 -0800 Subject: [PATCH] feat: use css scaling, ~1,600x less memory written per frame --- cgol/src/lib.rs | 145 +++++++++++++----------------------------------- 1 file changed, 39 insertions(+), 106 deletions(-) diff --git a/cgol/src/lib.rs b/cgol/src/lib.rs index 0a204a5..a6083e9 100644 --- a/cgol/src/lib.rs +++ b/cgol/src/lib.rs @@ -219,12 +219,18 @@ impl AppState { } fn resize_canvas_and_universe(&mut self) { - let width = self.window.inner_width().unwrap().as_f64().unwrap(); - let height = self.window.inner_height().unwrap().as_f64().unwrap(); + let css_w = self.window.inner_width().unwrap().as_f64().unwrap(); + let css_h = self.window.inner_height().unwrap().as_f64().unwrap(); + + let cols = (css_w as u32 / CELL_SIZE).max(1); + let rows = (css_h as u32 / CELL_SIZE).max(1); + + // Set canvas to grid resolution (1 pixel per cell), CSS scales it up + self.canvas_width = cols; + self.canvas_height = rows; + self.canvas.set_width(cols); + self.canvas.set_height(rows); - let dpr = self.window.device_pixel_ratio(); - let css_w = width; - let css_h = height; let element = self.canvas.dyn_ref::().unwrap(); element .set_attribute( @@ -235,88 +241,35 @@ impl AppState { ), ) .ok(); - - self.canvas_width = (css_w * dpr) as u32; - self.canvas_height = (css_h * dpr) as u32; - self.canvas.set_width(self.canvas_width); - self.canvas.set_height(self.canvas_height); - - // Disable image smoothing after resize - self.ctx.set_image_smoothing_enabled(false); - - // Clear canvas - self.ctx.set_fill_style_str("black"); - self.ctx.fill_rect(0.0, 0.0, self.canvas_width as f64, self.canvas_height as f64); - let cols = (self.canvas_width / CELL_SIZE).max(1); - let rows = (self.canvas_height / CELL_SIZE).max(1); + self.ctx.set_image_smoothing_enabled(false); + self.universe = Universe::new(cols, rows); - - // Allocate pixel buffer for the universe size (1 pixel per cell) - // We'll draw at universe resolution and let CSS scale it up - let buffer_size = (cols * rows * 4) as usize; - self.pixel_buffer = vec![0u8; buffer_size]; + self.pixel_buffer = vec![0u8; (cols * rows * 4) as usize]; } - fn draw_scaled(&mut self) { - let grid_width = self.universe.width; - let grid_height = self.universe.height; - let cell_w = CELL_SIZE; - let cell_h = CELL_SIZE; - - // Fill pixel buffer at full canvas resolution - let canvas_w = self.canvas_width as usize; - let canvas_h = self.canvas_height as usize; - - // Resize buffer if needed - let needed_size = canvas_w * canvas_h * 4; - if self.pixel_buffer.len() != needed_size { - self.pixel_buffer.resize(needed_size, 0); - } - - // Fill with black first (for dead cells) - for chunk in self.pixel_buffer.chunks_exact_mut(4) { - chunk[0] = 0; - chunk[1] = 0; - chunk[2] = 0; - chunk[3] = 255; - } - - // Draw each cell as a CELL_SIZE × CELL_SIZE block - for row in 0..grid_height { - for col in 0..grid_width { - let cell_idx = self.universe.index(row, col); - - if let Cell::Alive { hue } = self.universe.cells[cell_idx] { + fn draw(&mut self) { + let total = (self.universe.width * self.universe.height) as usize; + + for i in 0..total { + let px = i * 4; + match self.universe.cells[i] { + Cell::Alive { hue } => { let (r, g, b) = hue_to_rgb(hue); - - let start_x = (col * cell_w) as usize; - let start_y = (row * cell_h) as usize; - - for py in 0..cell_h as usize { - let y = start_y + py; - if y >= canvas_h { - break; - } - - for px in 0..cell_w as usize { - let x = start_x + px; - if x >= canvas_w { - break; - } - - let pixel_idx = (y * canvas_w + x) * 4; - self.pixel_buffer[pixel_idx] = r; - self.pixel_buffer[pixel_idx + 1] = g; - self.pixel_buffer[pixel_idx + 2] = b; - self.pixel_buffer[pixel_idx + 3] = 255; - } - } + self.pixel_buffer[px] = r; + self.pixel_buffer[px + 1] = g; + self.pixel_buffer[px + 2] = b; + self.pixel_buffer[px + 3] = 255; + } + Cell::Dead => { + self.pixel_buffer[px] = 0; + self.pixel_buffer[px + 1] = 0; + self.pixel_buffer[px + 2] = 0; + self.pixel_buffer[px + 3] = 255; } } } - - // Single putImageData call with full canvas + if let Ok(image_data) = ImageData::new_with_u8_clamped_array_and_sh( Clamped(&self.pixel_buffer), self.canvas_width, @@ -332,22 +285,15 @@ pub fn start() -> Result<(), JsValue> { let state = AppState::new()?; let state_rc = Rc::new(RefCell::new(state)); - let canvas = state_rc.borrow().canvas.clone(); let window = state_rc.borrow().window.clone(); let document = window.document().unwrap(); // Mouse move handler let state_for_mouse = state_rc.clone(); - let canvas_for_mouse = canvas.clone(); - let window_for_mouse = window.clone(); let mouse_closure = Closure::wrap(Box::new(move |event: web_sys::MouseEvent| { - let rect = canvas_for_mouse.get_bounding_client_rect(); - let dpr = window_for_mouse.device_pixel_ratio(); - let x = (event.client_x() as f64 - rect.left()) * dpr; - let y = (event.client_y() as f64 - rect.top()) * dpr; let mut s = state_for_mouse.borrow_mut(); - s.cursor_col = (x / CELL_SIZE as f64) as i32; - s.cursor_row = (y / CELL_SIZE as f64) as i32; + s.cursor_col = event.client_x() / CELL_SIZE as i32; + s.cursor_row = event.client_y() / CELL_SIZE as i32; s.cursor_active = true; }) as Box); @@ -356,18 +302,12 @@ pub fn start() -> Result<(), JsValue> { // Touch move handler let state_for_touch_move = state_rc.clone(); - let canvas_for_touch_move = canvas.clone(); - let window_for_touch_move = window.clone(); let touch_move_closure = Closure::wrap(Box::new(move |event: web_sys::TouchEvent| { event.prevent_default(); if let Some(touch) = event.touches().get(0) { - let rect = canvas_for_touch_move.get_bounding_client_rect(); - let dpr = window_for_touch_move.device_pixel_ratio(); - let x = (touch.client_x() as f64 - rect.left()) * dpr; - let y = (touch.client_y() as f64 - rect.top()) * dpr; let mut s = state_for_touch_move.borrow_mut(); - s.cursor_col = (x / CELL_SIZE as f64) as i32; - s.cursor_row = (y / CELL_SIZE as f64) as i32; + s.cursor_col = touch.client_x() / CELL_SIZE as i32; + s.cursor_row = touch.client_y() / CELL_SIZE as i32; s.cursor_active = true; } }) as Box); @@ -378,18 +318,12 @@ pub fn start() -> Result<(), JsValue> { // Touch start handler let state_for_touch_start = state_rc.clone(); - let canvas_for_touch_start = canvas.clone(); - let window_for_touch_start = window.clone(); let touch_start_closure = Closure::wrap(Box::new(move |event: web_sys::TouchEvent| { event.prevent_default(); if let Some(touch) = event.touches().get(0) { - let rect = canvas_for_touch_start.get_bounding_client_rect(); - let dpr = window_for_touch_start.device_pixel_ratio(); - let x = (touch.client_x() as f64 - rect.left()) * dpr; - let y = (touch.client_y() as f64 - rect.top()) * dpr; let mut s = state_for_touch_start.borrow_mut(); - s.cursor_col = (x / CELL_SIZE as f64) as i32; - s.cursor_row = (y / CELL_SIZE as f64) as i32; + s.cursor_col = touch.client_x() / CELL_SIZE as i32; + s.cursor_row = touch.client_y() / CELL_SIZE as i32; s.cursor_active = true; } }) as Box); @@ -448,8 +382,7 @@ pub fn start() -> Result<(), JsValue> { s.universe.set_alive_block(cursor_row, cursor_col, 2, hue); } - // Draw every frame - s.draw_scaled(); + s.draw(); } window_for_anim