feat: upgrade faster and better cursor effects
This commit is contained in:
parent
b75e506639
commit
2010438475
2 changed files with 272 additions and 234 deletions
|
|
@ -11,7 +11,7 @@ crate-type = ["cdylib", "rlib"]
|
||||||
default = ["console_error_panic_hook"]
|
default = ["console_error_panic_hook"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
wasm-bindgen = "0.2.84"
|
wasm-bindgen = "0.2"
|
||||||
js-sys = "0.3"
|
js-sys = "0.3"
|
||||||
web-sys = { version = "0.3", features = [
|
web-sys = { version = "0.3", features = [
|
||||||
"console",
|
"console",
|
||||||
|
|
@ -21,16 +21,12 @@ web-sys = { version = "0.3", features = [
|
||||||
"Window",
|
"Window",
|
||||||
"MouseEvent",
|
"MouseEvent",
|
||||||
"Element",
|
"Element",
|
||||||
"HtmlElement",
|
|
||||||
"EventTarget",
|
"EventTarget",
|
||||||
"Performance",
|
"Performance",
|
||||||
"DomRect",
|
"DomRect",
|
||||||
"CssStyleDeclaration",
|
|
||||||
"TouchEvent",
|
"TouchEvent",
|
||||||
"Touch",
|
"Touch",
|
||||||
"TouchList",
|
"TouchList",
|
||||||
"OffscreenCanvas",
|
|
||||||
"OffscreenCanvasRenderingContext2d",
|
|
||||||
"ImageData",
|
"ImageData",
|
||||||
] }
|
] }
|
||||||
|
|
||||||
|
|
@ -38,12 +34,12 @@ web-sys = { version = "0.3", features = [
|
||||||
# logging them with `console.error`. This is great for development, but requires
|
# logging them with `console.error`. This is great for development, but requires
|
||||||
# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for
|
# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for
|
||||||
# code size when deploying.
|
# code size when deploying.
|
||||||
console_error_panic_hook = { version = "0.1.7", optional = true }
|
console_error_panic_hook = { version = "0.1", optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
wasm-bindgen-test = "0.3.34"
|
wasm-bindgen-test = "0.3"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
# Tell `rustc` to optimize for small code size.
|
opt-level = 3
|
||||||
opt-level = "z"
|
|
||||||
lto = true
|
lto = true
|
||||||
|
codegen-units = 1
|
||||||
|
|
|
||||||
458
cgol/src/lib.rs
458
cgol/src/lib.rs
|
|
@ -3,60 +3,22 @@ mod utils;
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
|
use wasm_bindgen::Clamped;
|
||||||
use wasm_bindgen::JsCast;
|
use wasm_bindgen::JsCast;
|
||||||
|
|
||||||
use js_sys::Math;
|
use js_sys::Math;
|
||||||
use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement, Window, OffscreenCanvas, OffscreenCanvasRenderingContext2d, ImageData};
|
use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement, ImageData, Window};
|
||||||
|
|
||||||
const CELL_SIZE: u32 = 20;
|
const CELL_SIZE: u32 = 20;
|
||||||
const TARGET_FPS: f64 = 45.0;
|
const SIMULATION_FPS: f64 = 60.0;
|
||||||
const FRAME_DURATION_MS: f64 = 1000.0 / TARGET_FPS;
|
const SIMULATION_FRAME_MS: f64 = 1000.0 / SIMULATION_FPS;
|
||||||
|
const HUE_ROTATION_PERIOD_MS: f64 = 3000.0;
|
||||||
struct CellCache {
|
|
||||||
ctx: OffscreenCanvasRenderingContext2d,
|
|
||||||
cached_cells: std::collections::HashMap<u8, ImageData>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CellCache {
|
|
||||||
fn new() -> Result<Self, JsValue> {
|
|
||||||
let offscreen_canvas = OffscreenCanvas::new(CELL_SIZE, CELL_SIZE)?;
|
|
||||||
let context_options = js_sys::Object::new();
|
|
||||||
js_sys::Reflect::set(&context_options, &"willReadFrequently".into(), &true.into())?;
|
|
||||||
let ctx = offscreen_canvas
|
|
||||||
.get_context_with_context_options("2d", &context_options)?
|
|
||||||
.ok_or_else(|| JsValue::from_str("no 2d context"))?
|
|
||||||
.dyn_into::<OffscreenCanvasRenderingContext2d>()?;
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
ctx,
|
|
||||||
cached_cells: std::collections::HashMap::new(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_or_create_cell(&mut self, hue: u8) -> Result<&ImageData, JsValue> {
|
|
||||||
if !self.cached_cells.contains_key(&hue) {
|
|
||||||
self.ctx.set_fill_style_str("#000");
|
|
||||||
self.ctx.fill_rect(0.0, 0.0, CELL_SIZE as f64, CELL_SIZE as f64);
|
|
||||||
|
|
||||||
let hue_degrees = hue as f64 * 360.0 / 255.0;
|
|
||||||
let color_str = format!("hsl({}, 100%, 50%)", hue_degrees);
|
|
||||||
self.ctx.set_fill_style_str(&color_str);
|
|
||||||
self.ctx.fill_rect(0.0, 0.0, CELL_SIZE as f64, CELL_SIZE as f64);
|
|
||||||
|
|
||||||
let image_data = self.ctx.get_image_data(0.0, 0.0, CELL_SIZE as f64, CELL_SIZE as f64)?;
|
|
||||||
self.cached_cells.insert(hue, image_data);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(self.cached_cells.get(&hue).unwrap())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fn random_hue() -> u8 {
|
fn random_hue() -> u8 {
|
||||||
(Math::random() * 256.0) as u8
|
(Math::random() * 256.0) as u8
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mix_colors(hues: Vec<u8>) -> u8 {
|
fn mix_colors(hues: &[u8]) -> u8 {
|
||||||
if hues.is_empty() {
|
if hues.is_empty() {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
@ -71,6 +33,24 @@ fn mix_colors(hues: Vec<u8>) -> u8 {
|
||||||
(avg_hue_degrees * 255.0 / 360.0) as u8
|
(avg_hue_degrees * 255.0 / 360.0) as u8
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Convert HSL hue (0-255) to RGB
|
||||||
|
/// Using full saturation (100%) and lightness (50%)
|
||||||
|
fn hue_to_rgb(hue: u8) -> (u8, u8, u8) {
|
||||||
|
let h = hue as f64 / 255.0 * 6.0;
|
||||||
|
let x = 1.0 - (h % 2.0 - 1.0).abs();
|
||||||
|
|
||||||
|
let (r, g, b) = match h as u32 {
|
||||||
|
0 => (1.0, x, 0.0),
|
||||||
|
1 => (x, 1.0, 0.0),
|
||||||
|
2 => (0.0, 1.0, x),
|
||||||
|
3 => (0.0, x, 1.0),
|
||||||
|
4 => (x, 0.0, 1.0),
|
||||||
|
_ => (1.0, 0.0, x),
|
||||||
|
};
|
||||||
|
|
||||||
|
((r * 255.0) as u8, (g * 255.0) as u8, (b * 255.0) as u8)
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq)]
|
#[derive(Clone, Copy, PartialEq)]
|
||||||
enum Cell {
|
enum Cell {
|
||||||
Dead,
|
Dead,
|
||||||
|
|
@ -82,6 +62,7 @@ struct Universe {
|
||||||
height: u32,
|
height: u32,
|
||||||
cells: Vec<Cell>,
|
cells: Vec<Cell>,
|
||||||
next_cells: Vec<Cell>,
|
next_cells: Vec<Cell>,
|
||||||
|
neighbor_hues_buffer: Vec<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Universe {
|
impl Universe {
|
||||||
|
|
@ -92,6 +73,7 @@ impl Universe {
|
||||||
height,
|
height,
|
||||||
cells: vec![Cell::Dead; size],
|
cells: vec![Cell::Dead; size],
|
||||||
next_cells: vec![Cell::Dead; size],
|
next_cells: vec![Cell::Dead; size],
|
||||||
|
neighbor_hues_buffer: Vec::with_capacity(8),
|
||||||
};
|
};
|
||||||
u.randomize();
|
u.randomize();
|
||||||
u
|
u
|
||||||
|
|
@ -107,12 +89,13 @@ impl Universe {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
fn index(&self, row: u32, col: u32) -> usize {
|
fn index(&self, row: u32, col: u32) -> usize {
|
||||||
(row * self.width + col) as usize
|
(row * self.width + col) as usize
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_neighbor_hues(&self, row: u32, col: u32) -> Vec<u8> {
|
fn count_neighbors_and_get_hues(&mut self, row: u32, col: u32) -> u8 {
|
||||||
let mut hues = Vec::new();
|
self.neighbor_hues_buffer.clear();
|
||||||
|
|
||||||
let north = if row == 0 { self.height - 1 } else { row - 1 };
|
let north = if row == 0 { self.height - 1 } else { row - 1 };
|
||||||
let south = if row == self.height - 1 { 0 } else { row + 1 };
|
let south = if row == self.height - 1 { 0 } else { row + 1 };
|
||||||
|
|
@ -132,12 +115,12 @@ impl Universe {
|
||||||
|
|
||||||
for (nr, nc) in neighbors {
|
for (nr, nc) in neighbors {
|
||||||
let idx = self.index(nr, nc);
|
let idx = self.index(nr, nc);
|
||||||
if let Cell::Alive { hue } = &self.cells[idx] {
|
if let Cell::Alive { hue } = self.cells[idx] {
|
||||||
hues.push(*hue);
|
self.neighbor_hues_buffer.push(hue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
hues
|
self.neighbor_hues_buffer.len() as u8
|
||||||
}
|
}
|
||||||
|
|
||||||
fn tick(&mut self) {
|
fn tick(&mut self) {
|
||||||
|
|
@ -145,16 +128,16 @@ impl Universe {
|
||||||
for col in 0..self.width {
|
for col in 0..self.width {
|
||||||
let idx = self.index(row, col);
|
let idx = self.index(row, col);
|
||||||
let cell = self.cells[idx];
|
let cell = self.cells[idx];
|
||||||
let neighbor_hues = self.get_neighbor_hues(row, col);
|
let neighbor_count = self.count_neighbors_and_get_hues(row, col);
|
||||||
let neighbor_count = neighbor_hues.len() as u8;
|
|
||||||
self.next_cells[idx] = match (cell, neighbor_count) {
|
self.next_cells[idx] = match (cell, neighbor_count) {
|
||||||
(Cell::Alive { .. }, x) if x < 2 => Cell::Dead,
|
(Cell::Alive { .. }, x) if x < 2 => Cell::Dead,
|
||||||
(Cell::Alive { hue }, 2) | (Cell::Alive { hue }, 3) => Cell::Alive { hue },
|
(Cell::Alive { hue }, 2) | (Cell::Alive { hue }, 3) => Cell::Alive { hue },
|
||||||
(Cell::Alive { .. }, x) if x > 3 => Cell::Dead,
|
(Cell::Alive { .. }, x) if x > 3 => Cell::Dead,
|
||||||
(Cell::Dead, 3) => {
|
(Cell::Dead, 3) => {
|
||||||
let mixed_hue = mix_colors(neighbor_hues);
|
let mixed_hue = mix_colors(&self.neighbor_hues_buffer);
|
||||||
Cell::Alive { hue: mixed_hue }
|
Cell::Alive { hue: mixed_hue }
|
||||||
},
|
}
|
||||||
(otherwise, _) => otherwise,
|
(otherwise, _) => otherwise,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -162,7 +145,7 @@ impl Universe {
|
||||||
std::mem::swap(&mut self.cells, &mut self.next_cells);
|
std::mem::swap(&mut self.cells, &mut self.next_cells);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_alive_block(&mut self, center_row: i32, center_col: i32, half: i32) {
|
fn set_alive_block(&mut self, center_row: i32, center_col: i32, half: i32, hue: u8) {
|
||||||
let h = self.height as i32;
|
let h = self.height as i32;
|
||||||
let w = self.width as i32;
|
let w = self.width as i32;
|
||||||
for dr in -half..=half {
|
for dr in -half..=half {
|
||||||
|
|
@ -170,13 +153,12 @@ impl Universe {
|
||||||
let r = (center_row + dr).rem_euclid(h) as u32;
|
let r = (center_row + dr).rem_euclid(h) as u32;
|
||||||
let c = (center_col + dc).rem_euclid(w) as u32;
|
let c = (center_col + dc).rem_euclid(w) as u32;
|
||||||
let idx = self.index(r, c);
|
let idx = self.index(r, c);
|
||||||
self.cells[idx] = Cell::Alive { hue: random_hue() };
|
self.cells[idx] = Cell::Alive { hue };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
struct AppState {
|
struct AppState {
|
||||||
window: Window,
|
window: Window,
|
||||||
canvas: HtmlCanvasElement,
|
canvas: HtmlCanvasElement,
|
||||||
|
|
@ -186,7 +168,10 @@ struct AppState {
|
||||||
cursor_col: i32,
|
cursor_col: i32,
|
||||||
last_frame_time: f64,
|
last_frame_time: f64,
|
||||||
cursor_active: bool,
|
cursor_active: bool,
|
||||||
cell_cache: CellCache,
|
// Reusable pixel buffer for ImageData (avoids allocation each frame)
|
||||||
|
pixel_buffer: Vec<u8>,
|
||||||
|
canvas_width: u32,
|
||||||
|
canvas_height: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppState {
|
impl AppState {
|
||||||
|
|
@ -194,7 +179,9 @@ impl AppState {
|
||||||
utils::set_panic_hook();
|
utils::set_panic_hook();
|
||||||
|
|
||||||
let window = web_sys::window().ok_or_else(|| JsValue::from_str("no window"))?;
|
let window = web_sys::window().ok_or_else(|| JsValue::from_str("no window"))?;
|
||||||
let document = window.document().ok_or_else(|| JsValue::from_str("no document"))?;
|
let document = window
|
||||||
|
.document()
|
||||||
|
.ok_or_else(|| JsValue::from_str("no document"))?;
|
||||||
|
|
||||||
let canvas = document
|
let canvas = document
|
||||||
.get_element_by_id("canvas")
|
.get_element_by_id("canvas")
|
||||||
|
|
@ -206,6 +193,9 @@ impl AppState {
|
||||||
.ok_or_else(|| JsValue::from_str("no 2d context"))?
|
.ok_or_else(|| JsValue::from_str("no 2d context"))?
|
||||||
.dyn_into::<CanvasRenderingContext2d>()?;
|
.dyn_into::<CanvasRenderingContext2d>()?;
|
||||||
|
|
||||||
|
// Disable image smoothing for sharp pixel scaling
|
||||||
|
ctx.set_image_smoothing_enabled(false);
|
||||||
|
|
||||||
let mut state = Self {
|
let mut state = Self {
|
||||||
window,
|
window,
|
||||||
canvas,
|
canvas,
|
||||||
|
|
@ -215,7 +205,9 @@ impl AppState {
|
||||||
cursor_col: 0,
|
cursor_col: 0,
|
||||||
last_frame_time: 0.0,
|
last_frame_time: 0.0,
|
||||||
cursor_active: false,
|
cursor_active: false,
|
||||||
cell_cache: CellCache::new()?,
|
pixel_buffer: Vec::new(),
|
||||||
|
canvas_width: 0,
|
||||||
|
canvas_height: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
state.resize_canvas_and_universe();
|
state.resize_canvas_and_universe();
|
||||||
|
|
@ -223,10 +215,7 @@ impl AppState {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_current_time(&self) -> f64 {
|
fn get_current_time(&self) -> f64 {
|
||||||
self.window
|
self.window.performance().unwrap().now()
|
||||||
.performance()
|
|
||||||
.unwrap()
|
|
||||||
.now()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resize_canvas_and_universe(&mut self) {
|
fn resize_canvas_and_universe(&mut self) {
|
||||||
|
|
@ -236,186 +225,239 @@ impl AppState {
|
||||||
let dpr = self.window.device_pixel_ratio();
|
let dpr = self.window.device_pixel_ratio();
|
||||||
let css_w = width;
|
let css_w = width;
|
||||||
let css_h = height;
|
let css_h = height;
|
||||||
let style = self.canvas.style();
|
let element = self.canvas.dyn_ref::<web_sys::Element>().unwrap();
|
||||||
style.set_property("position", "fixed").ok();
|
element
|
||||||
style.set_property("inset", "0").ok();
|
.set_attribute(
|
||||||
style.set_property("width", &format!("{}px", css_w)).ok();
|
"style",
|
||||||
style.set_property("height", &format!("{}px", css_h)).ok();
|
&format!(
|
||||||
self.canvas.set_width((css_w * dpr) as u32);
|
"position:fixed;inset:0;width:{}px;height:{}px;image-rendering:pixelated",
|
||||||
self.canvas.set_height((css_h * dpr) as u32);
|
css_w, css_h
|
||||||
self.ctx.set_image_smoothing_enabled(false);
|
),
|
||||||
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);
|
.ok();
|
||||||
|
|
||||||
let cols = (self.canvas.width() / CELL_SIZE).max(1);
|
self.canvas_width = (css_w * dpr) as u32;
|
||||||
let rows = (self.canvas.height() / CELL_SIZE).max(1);
|
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.universe = Universe::new(cols, rows);
|
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];
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw(&mut self) {
|
fn draw_scaled(&mut self) {
|
||||||
self.ctx.set_fill_style_str("#000");
|
let grid_width = self.universe.width;
|
||||||
self.ctx.fill_rect(0.0, 0.0, self.canvas.width() as f64, self.canvas.height() as f64);
|
let grid_height = self.universe.height;
|
||||||
|
let cell_w = CELL_SIZE;
|
||||||
|
let cell_h = CELL_SIZE;
|
||||||
|
|
||||||
let cell_w = CELL_SIZE as f64;
|
// Fill pixel buffer at full canvas resolution
|
||||||
let cell_h = CELL_SIZE as f64;
|
let canvas_w = self.canvas_width as usize;
|
||||||
|
let canvas_h = self.canvas_height as usize;
|
||||||
|
|
||||||
for row in 0..self.universe.height {
|
// Resize buffer if needed
|
||||||
for col in 0..self.universe.width {
|
let needed_size = canvas_w * canvas_h * 4;
|
||||||
let idx = self.universe.index(row, col);
|
if self.pixel_buffer.len() != needed_size {
|
||||||
if let Cell::Alive { hue } = self.universe.cells[idx] {
|
self.pixel_buffer.resize(needed_size, 0);
|
||||||
let x = col as f64 * cell_w;
|
}
|
||||||
let y = row as f64 * cell_h;
|
|
||||||
|
|
||||||
if let Ok(image_data) = self.cell_cache.get_or_create_cell(hue) {
|
// Fill with black first (for dead cells)
|
||||||
self.ctx.put_image_data(image_data, x, y).ok();
|
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] {
|
||||||
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
self.canvas_height,
|
||||||
|
) {
|
||||||
|
self.ctx.put_image_data(&image_data, 0.0, 0.0).ok();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub fn start() -> Result<(), JsValue> {
|
pub fn start() -> Result<(), JsValue> {
|
||||||
let state = AppState::new()?;
|
let state = AppState::new()?;
|
||||||
|
let state_rc = Rc::new(RefCell::new(state));
|
||||||
|
|
||||||
{
|
let canvas = state_rc.borrow().canvas.clone();
|
||||||
let canvas = state.canvas.clone();
|
let window = state_rc.borrow().window.clone();
|
||||||
let window = state.window.clone();
|
let document = window.document().unwrap();
|
||||||
let state_rc = Rc::new(RefCell::new(state));
|
|
||||||
let state_for_mouse = state_rc.clone();
|
// Mouse move handler
|
||||||
let mouse_closure = Closure::wrap(Box::new(move |event: web_sys::MouseEvent| {
|
let state_for_mouse = state_rc.clone();
|
||||||
let rect = canvas.get_bounding_client_rect();
|
let canvas_for_mouse = canvas.clone();
|
||||||
let dpr = window.device_pixel_ratio();
|
let window_for_mouse = window.clone();
|
||||||
let x = (event.client_x() as f64 - rect.left()) * dpr;
|
let mouse_closure = Closure::wrap(Box::new(move |event: web_sys::MouseEvent| {
|
||||||
let y = (event.client_y() as f64 - rect.top()) * dpr;
|
let rect = canvas_for_mouse.get_bounding_client_rect();
|
||||||
let mut s = state_for_mouse.borrow_mut();
|
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_active = true;
|
||||||
|
}) as Box<dyn FnMut(_)>);
|
||||||
|
|
||||||
|
document.add_event_listener_with_callback("mousemove", mouse_closure.as_ref().unchecked_ref())?;
|
||||||
|
mouse_closure.forget();
|
||||||
|
|
||||||
|
// 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_col = (x / CELL_SIZE as f64) as i32;
|
||||||
s.cursor_row = (y / CELL_SIZE as f64) as i32;
|
s.cursor_row = (y / CELL_SIZE as f64) as i32;
|
||||||
s.cursor_active = true;
|
s.cursor_active = true;
|
||||||
}) as Box<dyn FnMut(_)>);
|
}
|
||||||
state_rc
|
}) as Box<dyn FnMut(_)>);
|
||||||
.borrow()
|
|
||||||
.window
|
|
||||||
.document()
|
|
||||||
.unwrap()
|
|
||||||
.add_event_listener_with_callback("mousemove", mouse_closure.as_ref().unchecked_ref())?;
|
|
||||||
mouse_closure.forget();
|
|
||||||
|
|
||||||
let canvas_touch_move = state_rc.borrow().canvas.clone();
|
document
|
||||||
let window_touch_move = state_rc.borrow().window.clone();
|
.add_event_listener_with_callback("touchmove", touch_move_closure.as_ref().unchecked_ref())?;
|
||||||
let state_for_touch_move = state_rc.clone();
|
touch_move_closure.forget();
|
||||||
|
|
||||||
let touch_move_closure = Closure::wrap(Box::new(move |event: web_sys::TouchEvent| {
|
// Touch start handler
|
||||||
event.prevent_default();
|
let state_for_touch_start = state_rc.clone();
|
||||||
if let Some(touch) = event.touches().get(0) {
|
let canvas_for_touch_start = canvas.clone();
|
||||||
let rect = canvas_touch_move.get_bounding_client_rect();
|
let window_for_touch_start = window.clone();
|
||||||
let dpr = window_touch_move.device_pixel_ratio();
|
let touch_start_closure = Closure::wrap(Box::new(move |event: web_sys::TouchEvent| {
|
||||||
let x = (touch.client_x() as f64 - rect.left()) * dpr;
|
event.prevent_default();
|
||||||
let y = (touch.client_y() as f64 - rect.top()) * dpr;
|
if let Some(touch) = event.touches().get(0) {
|
||||||
let mut s = state_for_touch_move.borrow_mut();
|
let rect = canvas_for_touch_start.get_bounding_client_rect();
|
||||||
s.cursor_col = (x / CELL_SIZE as f64) as i32;
|
let dpr = window_for_touch_start.device_pixel_ratio();
|
||||||
s.cursor_row = (y / CELL_SIZE as f64) as i32;
|
let x = (touch.client_x() as f64 - rect.left()) * dpr;
|
||||||
s.cursor_active = true;
|
let y = (touch.client_y() as f64 - rect.top()) * dpr;
|
||||||
}
|
let mut s = state_for_touch_start.borrow_mut();
|
||||||
}) as Box<dyn FnMut(_)>);
|
s.cursor_col = (x / CELL_SIZE as f64) as i32;
|
||||||
|
s.cursor_row = (y / CELL_SIZE as f64) as i32;
|
||||||
|
s.cursor_active = true;
|
||||||
|
}
|
||||||
|
}) as Box<dyn FnMut(_)>);
|
||||||
|
|
||||||
let canvas_touch_start = state_rc.borrow().canvas.clone();
|
document
|
||||||
let window_touch_start = state_rc.borrow().window.clone();
|
.add_event_listener_with_callback("touchstart", touch_start_closure.as_ref().unchecked_ref())?;
|
||||||
let state_for_touch_start = state_rc.clone();
|
touch_start_closure.forget();
|
||||||
|
|
||||||
let touch_start_closure = Closure::wrap(Box::new(move |event: web_sys::TouchEvent| {
|
// Touch end handler
|
||||||
event.prevent_default();
|
let state_for_touch_end = state_rc.clone();
|
||||||
if let Some(touch) = event.touches().get(0) {
|
let touch_end_closure = Closure::wrap(Box::new(move |_event: web_sys::TouchEvent| {
|
||||||
let rect = canvas_touch_start.get_bounding_client_rect();
|
let mut s = state_for_touch_end.borrow_mut();
|
||||||
let dpr = window_touch_start.device_pixel_ratio();
|
s.cursor_active = false;
|
||||||
let x = (touch.client_x() as f64 - rect.left()) * dpr;
|
}) as Box<dyn FnMut(_)>);
|
||||||
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_active = true;
|
|
||||||
}
|
|
||||||
}) as Box<dyn FnMut(_)>);
|
|
||||||
|
|
||||||
state_rc
|
document
|
||||||
.borrow()
|
.add_event_listener_with_callback("touchend", touch_end_closure.as_ref().unchecked_ref())?;
|
||||||
.window
|
touch_end_closure.forget();
|
||||||
.document()
|
|
||||||
.unwrap()
|
|
||||||
.add_event_listener_with_callback("touchmove", touch_move_closure.as_ref().unchecked_ref())?;
|
|
||||||
state_rc
|
|
||||||
.borrow()
|
|
||||||
.window
|
|
||||||
.document()
|
|
||||||
.unwrap()
|
|
||||||
.add_event_listener_with_callback("touchstart", touch_start_closure.as_ref().unchecked_ref())?;
|
|
||||||
|
|
||||||
touch_move_closure.forget();
|
// Resize handler
|
||||||
touch_start_closure.forget();
|
let state_for_resize = state_rc.clone();
|
||||||
|
let resize_closure = Closure::wrap(Box::new(move || {
|
||||||
|
let mut s = state_for_resize.borrow_mut();
|
||||||
|
s.resize_canvas_and_universe();
|
||||||
|
}) as Box<dyn FnMut()>);
|
||||||
|
|
||||||
let state_for_touch_end = state_rc.clone();
|
window.add_event_listener_with_callback("resize", resize_closure.as_ref().unchecked_ref())?;
|
||||||
let touch_end_closure = Closure::wrap(Box::new(move |_event: web_sys::TouchEvent| {
|
resize_closure.forget();
|
||||||
let mut s = state_for_touch_end.borrow_mut();
|
|
||||||
s.cursor_active = false;
|
|
||||||
}) as Box<dyn FnMut(_)>);
|
|
||||||
|
|
||||||
state_rc
|
// Animation loop
|
||||||
.borrow()
|
let f = Rc::new(RefCell::new(None::<Closure<dyn FnMut()>>));
|
||||||
.window
|
let g = f.clone();
|
||||||
.document()
|
let state_for_anim = state_rc.clone();
|
||||||
.unwrap()
|
let window_for_anim = window.clone();
|
||||||
.add_event_listener_with_callback("touchend", touch_end_closure.as_ref().unchecked_ref())?;
|
|
||||||
touch_end_closure.forget();
|
|
||||||
|
|
||||||
let state_for_resize = state_rc.clone();
|
*g.borrow_mut() = Some(Closure::wrap(Box::new(move || {
|
||||||
let resize_closure = Closure::wrap(Box::new(move || {
|
let current_time = state_for_anim.borrow().get_current_time();
|
||||||
let mut s = state_for_resize.borrow_mut();
|
|
||||||
s.resize_canvas_and_universe();
|
|
||||||
}) as Box<dyn FnMut()>);
|
|
||||||
state_rc
|
|
||||||
.borrow()
|
|
||||||
.window
|
|
||||||
.add_event_listener_with_callback("resize", resize_closure.as_ref().unchecked_ref())?;
|
|
||||||
resize_closure.forget();
|
|
||||||
|
|
||||||
let f = Rc::new(RefCell::new(None::<Closure<dyn FnMut()>>));
|
{
|
||||||
let g = f.clone();
|
let mut s = state_for_anim.borrow_mut();
|
||||||
let state_for_anim = state_rc.clone();
|
|
||||||
let window_for_anim = state_rc.borrow().window.clone();
|
|
||||||
*g.borrow_mut() = Some(Closure::wrap(Box::new(move || {
|
|
||||||
let current_time = state_for_anim.borrow().get_current_time();
|
|
||||||
let mut should_update = false;
|
|
||||||
|
|
||||||
{
|
// Run simulation FIRST (throttled to 60 FPS max)
|
||||||
let mut s = state_for_anim.borrow_mut();
|
// This way cursor-placed cells won't be immediately killed
|
||||||
if current_time - s.last_frame_time >= FRAME_DURATION_MS {
|
if current_time - s.last_frame_time >= SIMULATION_FRAME_MS {
|
||||||
should_update = true;
|
s.last_frame_time = current_time;
|
||||||
s.last_frame_time = current_time;
|
s.universe.tick();
|
||||||
}
|
|
||||||
|
|
||||||
if should_update {
|
|
||||||
if s.cursor_active {
|
|
||||||
let cursor_row = s.cursor_row;
|
|
||||||
let cursor_col = s.cursor_col;
|
|
||||||
s.universe.set_alive_block(cursor_row, cursor_col, 2);
|
|
||||||
}
|
|
||||||
s.universe.tick();
|
|
||||||
}
|
|
||||||
|
|
||||||
s.draw();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
window_for_anim
|
// Process cursor input AFTER tick for responsiveness
|
||||||
.request_animation_frame(f.borrow().as_ref().unwrap().as_ref().unchecked_ref())
|
// Cells placed here survive until the next tick
|
||||||
.ok();
|
if s.cursor_active {
|
||||||
}) as Box<dyn FnMut()>));
|
let cursor_row = s.cursor_row;
|
||||||
|
let cursor_col = s.cursor_col;
|
||||||
|
|
||||||
state_rc
|
let hue = ((current_time % HUE_ROTATION_PERIOD_MS) / HUE_ROTATION_PERIOD_MS * 256.0) as u8;
|
||||||
.borrow()
|
s.universe.set_alive_block(cursor_row, cursor_col, 2, hue);
|
||||||
.window
|
}
|
||||||
.request_animation_frame(g.borrow().as_ref().unwrap().as_ref().unchecked_ref())?;
|
|
||||||
}
|
// Draw every frame
|
||||||
|
s.draw_scaled();
|
||||||
|
}
|
||||||
|
|
||||||
|
window_for_anim
|
||||||
|
.request_animation_frame(f.borrow().as_ref().unwrap().as_ref().unchecked_ref())
|
||||||
|
.ok();
|
||||||
|
}) as Box<dyn FnMut()>));
|
||||||
|
|
||||||
|
window.request_animation_frame(g.borrow().as_ref().unwrap().as_ref().unchecked_ref())?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue