feat: rewrite readme and clean up support
This commit is contained in:
parent
38af26d959
commit
7758be92b4
16 changed files with 383 additions and 453 deletions
226
cgol/src/lib.rs
226
cgol/src/lib.rs
|
|
@ -1,5 +1,6 @@
|
|||
mod utils;
|
||||
|
||||
use std::cell::Cell;
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
|
@ -12,13 +13,18 @@ use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement, ImageData, Window};
|
|||
const CELL_SIZE: u32 = 10;
|
||||
const TICK_MS: f64 = 1000.0 / 60.0;
|
||||
const HUE_PERIOD_MS: f64 = 3000.0;
|
||||
const STILL_STEPS: u32 = 5;
|
||||
|
||||
/// Cells: 0 = dead, 1-255 = alive with that hue value.
|
||||
const DEAD: u8 = 0;
|
||||
|
||||
#[inline(always)]
|
||||
fn safe_hue(h: u8) -> u8 {
|
||||
if h == DEAD { 1 } else { h }
|
||||
if h == DEAD {
|
||||
1
|
||||
} else {
|
||||
h
|
||||
}
|
||||
}
|
||||
|
||||
// ── Lookup tables (computed once) ──────────────────────────────────
|
||||
|
|
@ -66,7 +72,9 @@ impl Luts {
|
|||
s += self.sin[h as usize];
|
||||
c += self.cos[h as usize];
|
||||
}
|
||||
safe_hue((s.atan2(c).rem_euclid(std::f32::consts::TAU) * 256.0 / std::f32::consts::TAU) as u8)
|
||||
safe_hue(
|
||||
(s.atan2(c).rem_euclid(std::f32::consts::TAU) * 256.0 / std::f32::consts::TAU) as u8,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -136,7 +144,11 @@ impl Grid {
|
|||
let alive = cell != DEAD;
|
||||
|
||||
self.buf[i] = if alive {
|
||||
if count == 2 || count == 3 { cell } else { DEAD }
|
||||
if count == 2 || count == 3 {
|
||||
cell
|
||||
} else {
|
||||
DEAD
|
||||
}
|
||||
} else if count == 3 {
|
||||
// Only collect hues for births
|
||||
self.hues.clear();
|
||||
|
|
@ -185,6 +197,13 @@ struct App {
|
|||
dirty: bool,
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
static APP_STATE: RefCell<Option<Rc<RefCell<App>>>> = const { RefCell::new(None) };
|
||||
static RUNNING: Cell<bool> = const { Cell::new(false) };
|
||||
static LISTENERS_READY: Cell<bool> = const { Cell::new(false) };
|
||||
static RAF_CLOSURE: RefCell<Option<Closure<dyn FnMut()>>> = const { RefCell::new(None) };
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn new() -> Result<Self, JsValue> {
|
||||
utils::set_panic_hook();
|
||||
|
|
@ -194,7 +213,8 @@ impl App {
|
|||
.get_element_by_id("canvas")
|
||||
.ok_or("no canvas")?
|
||||
.dyn_into()?;
|
||||
let ctx: CanvasRenderingContext2d = canvas.get_context("2d")?.ok_or("no 2d ctx")?.dyn_into()?;
|
||||
let ctx: CanvasRenderingContext2d =
|
||||
canvas.get_context("2d")?.ok_or("no 2d ctx")?.dyn_into()?;
|
||||
ctx.set_image_smoothing_enabled(false);
|
||||
|
||||
let mut app = Self {
|
||||
|
|
@ -261,26 +281,53 @@ impl App {
|
|||
px[i * 4..i * 4 + 4].copy_from_slice(&rgb[cell as usize]);
|
||||
}
|
||||
|
||||
if let Ok(img) =
|
||||
ImageData::new_with_u8_clamped_array_and_sh(Clamped(px), self.cw, self.ch)
|
||||
if let Ok(img) = ImageData::new_with_u8_clamped_array_and_sh(Clamped(px), self.cw, self.ch)
|
||||
{
|
||||
self.ctx.put_image_data(&img, 0.0, 0.0).ok();
|
||||
}
|
||||
}
|
||||
|
||||
fn render_steps(&mut self, steps: u32) {
|
||||
for _ in 0..steps {
|
||||
let luts = &self.luts as *const Luts;
|
||||
// SAFETY: luts is not mutated during tick()
|
||||
self.grid.tick(unsafe { &*luts });
|
||||
}
|
||||
self.dirty = true;
|
||||
self.draw();
|
||||
}
|
||||
}
|
||||
|
||||
// ── Entry point ────────────────────────────────────────────────────
|
||||
fn app() -> Result<Rc<RefCell<App>>, JsValue> {
|
||||
APP_STATE.with(|state| {
|
||||
if let Some(app) = state.borrow().as_ref() {
|
||||
return Ok(app.clone());
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn start() -> Result<(), JsValue> {
|
||||
let app = App::new()?;
|
||||
let rc = Rc::new(RefCell::new(app));
|
||||
let app = Rc::new(RefCell::new(App::new()?));
|
||||
state.borrow_mut().replace(app.clone());
|
||||
Ok(app)
|
||||
})
|
||||
}
|
||||
|
||||
let win = rc.borrow().win.clone();
|
||||
fn init_listeners(app: Rc<RefCell<App>>) -> Result<(), JsValue> {
|
||||
let already_ready = LISTENERS_READY.with(|ready| {
|
||||
if ready.get() {
|
||||
true
|
||||
} else {
|
||||
ready.set(true);
|
||||
false
|
||||
}
|
||||
});
|
||||
|
||||
if already_ready {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let win = app.borrow().win.clone();
|
||||
let doc = win.document().unwrap();
|
||||
|
||||
// Mouse
|
||||
let s = rc.clone();
|
||||
let s = app.clone();
|
||||
let cb = Closure::wrap(Box::new(move |e: web_sys::MouseEvent| {
|
||||
let mut a = s.borrow_mut();
|
||||
a.ccol = e.client_x() / CELL_SIZE as i32;
|
||||
|
|
@ -290,8 +337,7 @@ pub fn start() -> Result<(), JsValue> {
|
|||
doc.add_event_listener_with_callback("mousemove", cb.as_ref().unchecked_ref())?;
|
||||
cb.forget();
|
||||
|
||||
// Touch move
|
||||
let s = rc.clone();
|
||||
let s = app.clone();
|
||||
let cb = Closure::wrap(Box::new(move |e: web_sys::TouchEvent| {
|
||||
e.prevent_default();
|
||||
if let Some(t) = e.touches().get(0) {
|
||||
|
|
@ -304,8 +350,7 @@ pub fn start() -> Result<(), JsValue> {
|
|||
doc.add_event_listener_with_callback("touchmove", cb.as_ref().unchecked_ref())?;
|
||||
cb.forget();
|
||||
|
||||
// Touch start
|
||||
let s = rc.clone();
|
||||
let s = app.clone();
|
||||
let cb = Closure::wrap(Box::new(move |e: web_sys::TouchEvent| {
|
||||
e.prevent_default();
|
||||
if let Some(t) = e.touches().get(0) {
|
||||
|
|
@ -318,56 +363,121 @@ pub fn start() -> Result<(), JsValue> {
|
|||
doc.add_event_listener_with_callback("touchstart", cb.as_ref().unchecked_ref())?;
|
||||
cb.forget();
|
||||
|
||||
// Touch end
|
||||
let s = rc.clone();
|
||||
let s = app.clone();
|
||||
let cb = Closure::wrap(Box::new(move |_: web_sys::TouchEvent| {
|
||||
s.borrow_mut().cursor_on = false;
|
||||
}) as Box<dyn FnMut(_)>);
|
||||
doc.add_event_listener_with_callback("touchend", cb.as_ref().unchecked_ref())?;
|
||||
cb.forget();
|
||||
|
||||
// Resize
|
||||
let s = rc.clone();
|
||||
let s = app;
|
||||
let cb = Closure::wrap(Box::new(move || {
|
||||
s.borrow_mut().resize();
|
||||
let mut a = s.borrow_mut();
|
||||
a.resize();
|
||||
if RUNNING.with(|running| running.get()) {
|
||||
a.draw();
|
||||
} else {
|
||||
a.render_steps(STILL_STEPS);
|
||||
}
|
||||
}) as Box<dyn FnMut()>);
|
||||
win.add_event_listener_with_callback("resize", cb.as_ref().unchecked_ref())?;
|
||||
cb.forget();
|
||||
|
||||
// Animation loop
|
||||
let f: Rc<RefCell<Option<Closure<dyn FnMut()>>>> = Rc::new(RefCell::new(None));
|
||||
let g = f.clone();
|
||||
let s = rc.clone();
|
||||
let w = win.clone();
|
||||
|
||||
*g.borrow_mut() = Some(Closure::wrap(Box::new(move || {
|
||||
let now = s.borrow().now();
|
||||
|
||||
{
|
||||
let mut a = s.borrow_mut();
|
||||
|
||||
if now - a.last_tick >= TICK_MS {
|
||||
a.last_tick = now;
|
||||
let luts = &a.luts as *const Luts;
|
||||
// SAFETY: luts is not mutated during tick()
|
||||
a.grid.tick(unsafe { &*luts });
|
||||
a.dirty = true;
|
||||
}
|
||||
|
||||
if a.cursor_on {
|
||||
let (cr, cc) = (a.crow, a.ccol);
|
||||
let hue = ((now % HUE_PERIOD_MS) / HUE_PERIOD_MS * 256.0) as u8;
|
||||
a.grid.stamp(cr, cc, 2, hue);
|
||||
a.dirty = true;
|
||||
}
|
||||
|
||||
a.draw();
|
||||
}
|
||||
|
||||
w.request_animation_frame(f.borrow().as_ref().unwrap().as_ref().unchecked_ref())
|
||||
.ok();
|
||||
}) as Box<dyn FnMut()>));
|
||||
|
||||
win.request_animation_frame(g.borrow().as_ref().unwrap().as_ref().unchecked_ref())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ensure_animation_loop(app: Rc<RefCell<App>>) {
|
||||
RAF_CLOSURE.with(|slot| {
|
||||
if slot.borrow().is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
let win = app.borrow().win.clone();
|
||||
let f: Rc<RefCell<Option<Closure<dyn FnMut()>>>> = Rc::new(RefCell::new(None));
|
||||
let g = f.clone();
|
||||
let s = app.clone();
|
||||
let w = win.clone();
|
||||
|
||||
*g.borrow_mut() = Some(Closure::wrap(Box::new(move || {
|
||||
if !RUNNING.with(|running| running.get()) {
|
||||
return;
|
||||
}
|
||||
|
||||
let now = s.borrow().now();
|
||||
|
||||
{
|
||||
let mut a = s.borrow_mut();
|
||||
|
||||
if now - a.last_tick >= TICK_MS {
|
||||
a.last_tick = now;
|
||||
let luts = &a.luts as *const Luts;
|
||||
// SAFETY: luts is not mutated during tick()
|
||||
a.grid.tick(unsafe { &*luts });
|
||||
a.dirty = true;
|
||||
}
|
||||
|
||||
if a.cursor_on {
|
||||
let (cr, cc) = (a.crow, a.ccol);
|
||||
let hue = ((now % HUE_PERIOD_MS) / HUE_PERIOD_MS * 256.0) as u8;
|
||||
a.grid.stamp(cr, cc, 2, hue);
|
||||
a.dirty = true;
|
||||
}
|
||||
|
||||
a.draw();
|
||||
}
|
||||
|
||||
if RUNNING.with(|running| running.get()) {
|
||||
w.request_animation_frame(f.borrow().as_ref().unwrap().as_ref().unchecked_ref())
|
||||
.ok();
|
||||
}
|
||||
}) as Box<dyn FnMut()>));
|
||||
|
||||
slot.borrow_mut().replace(g.borrow_mut().take().unwrap());
|
||||
});
|
||||
}
|
||||
|
||||
// ── Entry point ────────────────────────────────────────────────────
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn start() -> Result<(), JsValue> {
|
||||
let app = app()?;
|
||||
init_listeners(app.clone())?;
|
||||
|
||||
if RUNNING.with(|running| running.get()) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
RUNNING.with(|running| running.set(true));
|
||||
ensure_animation_loop(app.clone());
|
||||
|
||||
RAF_CLOSURE.with(|slot| {
|
||||
if let Some(cb) = slot.borrow().as_ref() {
|
||||
app.borrow()
|
||||
.win
|
||||
.request_animation_frame(cb.as_ref().unchecked_ref())
|
||||
} else {
|
||||
Err(JsValue::from_str("no animation closure"))
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn stop() -> Result<(), JsValue> {
|
||||
let _ = app()?;
|
||||
RUNNING.with(|running| running.set(false));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn render_still(steps: u32) -> Result<(), JsValue> {
|
||||
let app = app()?;
|
||||
init_listeners(app.clone())?;
|
||||
RUNNING.with(|running| running.set(false));
|
||||
let steps = steps.max(1);
|
||||
let mut app = app.borrow_mut();
|
||||
app.resize();
|
||||
app.render_steps(steps);
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue