From 51dc6c9ee6994d9077ccd54875633c60fdd98c80 Mon Sep 17 00:00:00 2001 From: Jet Date: Fri, 27 Mar 2026 15:08:34 -0700 Subject: [PATCH] feat: use webgl shaders instead of wasm --- README.md | 20 +- cgol/.cargo/config.toml | 5 - cgol/.gitignore | 5 - cgol/.vscode/settings.json | 3 - cgol/Cargo.lock | 427 ---------- cgol/Cargo.toml | 47 - cgol/README.md | 84 -- cgol/src/lib.rs | 483 ----------- cgol/src/utils.rs | 10 - eslint.config.js | 65 +- index.html | 10 +- package-lock.json | 306 +------ package.json | 14 +- src/components/frosted-box.ts | 12 +- src/lib/background.ts | 78 -- src/lib/site.ts | 6 +- src/lib/webgl-background.ts | 1515 +++++++++++++++++++++++++++++++++ src/main.ts | 24 +- src/pages/home.ts | 15 +- src/router.ts | 2 + src/styles/globals.css | 83 +- tsconfig.json | 2 +- vite-plugin-ansi.ts | 96 +-- vite.config.ts | 7 +- 24 files changed, 1712 insertions(+), 1607 deletions(-) delete mode 100644 cgol/.cargo/config.toml delete mode 100644 cgol/.gitignore delete mode 100644 cgol/.vscode/settings.json delete mode 100644 cgol/Cargo.lock delete mode 100644 cgol/Cargo.toml delete mode 100644 cgol/README.md delete mode 100644 cgol/src/lib.rs delete mode 100644 cgol/src/utils.rs delete mode 100644 src/lib/background.ts create mode 100644 src/lib/webgl-background.ts diff --git a/README.md b/README.md index 8e4f453..3e8e94b 100644 --- a/README.md +++ b/README.md @@ -2,22 +2,22 @@ Personal site for Jet Pham. -The site is a small Vite app with a terminal-style UI, ANSI-rendered text, and a Rust/WebAssembly Conway's Game of Life background. +The site is a small Vite app with a terminal-style UI, ANSI-rendered text, and a WebGL2 Conway's Game of Life background. ## Features - ASCII/ANSI-inspired visual style with the IBM VGA font -- Conway's Game of Life running in the background via Rust + WebAssembly +- Conway's Game of Life running in the background via WebGL2 - Q+A page backed by the site API - Single-file oriented frontend build with Vite -- Reduced-motion aware background controls with a static fallback mode +- Fullscreen GPU blur/composite for the frosted panel effect ## Stack - Vite - TypeScript - Tailwind CSS v4 -- Rust + WebAssembly +- WebGL2 - npm ## Development @@ -25,9 +25,6 @@ The site is a small Vite app with a terminal-style UI, ANSI-rendered text, and a ### Prerequisites - Node.js + npm -- Rust -- `wasm-pack` -- `wasm-opt` (used by `build:wasm`) ### Install @@ -35,12 +32,6 @@ The site is a small Vite app with a terminal-style UI, ANSI-rendered text, and a npm install ``` -### Build the WASM package - -```bash -npm run build:wasm -``` - ### Start the dev server ```bash @@ -63,10 +54,9 @@ npm run build ```text src/ frontend app -cgol/ Rust + WASM Conway's Game of Life module ``` ## Notes - The homepage and Q+A page are the intended public pages. -- If WebAssembly fails or motion is disabled, the site falls back to a static background treatment. +- The background renderer targets WebGL2 and falls back to the site shell background if WebGL2 is unavailable. diff --git a/cgol/.cargo/config.toml b/cgol/.cargo/config.toml deleted file mode 100644 index 39c6f19..0000000 --- a/cgol/.cargo/config.toml +++ /dev/null @@ -1,5 +0,0 @@ -[build] -target = "wasm32-unknown-unknown" - -[target.wasm32-unknown-unknown] -rustflags = ["-C", "link-arg=--strip-all"] diff --git a/cgol/.gitignore b/cgol/.gitignore deleted file mode 100644 index 9f33f25..0000000 --- a/cgol/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -/target -**/*.rs.bk -bin/ -pkg/ -wasm-pack.log \ No newline at end of file diff --git a/cgol/.vscode/settings.json b/cgol/.vscode/settings.json deleted file mode 100644 index 8974c7a..0000000 --- a/cgol/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "rust-analyzer.cargo.target": "wasm32-unknown-unknown", -} diff --git a/cgol/Cargo.lock b/cgol/Cargo.lock deleted file mode 100644 index 4a58688..0000000 --- a/cgol/Cargo.lock +++ /dev/null @@ -1,427 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "async-trait" -version = "0.1.89" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - -[[package]] -name = "bumpalo" -version = "3.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" - -[[package]] -name = "cast" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" - -[[package]] -name = "cc" -version = "1.2.56" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" -dependencies = [ - "find-msvc-tools", - "shlex", -] - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "cgol" -version = "0.1.0" -dependencies = [ - "console_error_panic_hook", - "js-sys", - "wasm-bindgen", - "wasm-bindgen-test", - "web-sys", -] - -[[package]] -name = "console_error_panic_hook" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" -dependencies = [ - "cfg-if", - "wasm-bindgen", -] - -[[package]] -name = "find-msvc-tools" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" - -[[package]] -name = "futures-core" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" - -[[package]] -name = "futures-task" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" - -[[package]] -name = "futures-util" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" -dependencies = [ - "futures-core", - "futures-task", - "pin-project-lite", - "slab", -] - -[[package]] -name = "itoa" -version = "1.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" - -[[package]] -name = "js-sys" -version = "0.3.91" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" -dependencies = [ - "once_cell", - "wasm-bindgen", -] - -[[package]] -name = "libm" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" - -[[package]] -name = "memchr" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" - -[[package]] -name = "minicov" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4869b6a491569605d66d3952bcdf03df789e5b536e5f0cf7758a7f08a55ae24d" -dependencies = [ - "cc", - "walkdir", -] - -[[package]] -name = "nu-ansi-term" -version = "0.50.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" -dependencies = [ - "windows-sys", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", - "libm", -] - -[[package]] -name = "once_cell" -version = "1.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" - -[[package]] -name = "oorandom" -version = "11.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" - -[[package]] -name = "pin-project-lite" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" - -[[package]] -name = "proc-macro2" -version = "1.0.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.149" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" -dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "slab" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" - -[[package]] -name = "syn" -version = "2.0.117" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.114" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.64" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" -dependencies = [ - "cfg-if", - "futures-util", - "js-sys", - "once_cell", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.114" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.114" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.114" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "wasm-bindgen-test" -version = "0.3.64" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6311c867385cc7d5602463b31825d454d0837a3aba7cdb5e56d5201792a3f7fe" -dependencies = [ - "async-trait", - "cast", - "js-sys", - "libm", - "minicov", - "nu-ansi-term", - "num-traits", - "oorandom", - "serde", - "serde_json", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-bindgen-test-macro", - "wasm-bindgen-test-shared", -] - -[[package]] -name = "wasm-bindgen-test-macro" -version = "0.3.64" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67008cdde4769831958536b0f11b3bdd0380bde882be17fff9c2f34bb4549abd" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "wasm-bindgen-test-shared" -version = "0.2.114" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfe29135b180b72b04c74aa97b2b4a2ef275161eff9a6c7955ea9eaedc7e1d4e" - -[[package]] -name = "web-sys" -version = "0.3.91" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "winapi-util" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" -dependencies = [ - "windows-sys", -] - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link", -] - -[[package]] -name = "zmij" -version = "1.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/cgol/Cargo.toml b/cgol/Cargo.toml deleted file mode 100644 index 71116a6..0000000 --- a/cgol/Cargo.toml +++ /dev/null @@ -1,47 +0,0 @@ -[package] -name = "cgol" -version = "0.1.0" -authors = ["jet"] -edition = "2021" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -wasm-bindgen = "0.2" -js-sys = "0.3" -web-sys = { version = "0.3", features = [ - "CanvasRenderingContext2d", - "Document", - "HtmlCanvasElement", - "Window", - "MouseEvent", - "Element", - "EventTarget", - "Performance", - "TouchEvent", - "Touch", - "TouchList", - "ImageData", -] } -console_error_panic_hook = { version = "0.1", optional = true } - -[features] -default = ["console_error_panic_hook"] - -[dev-dependencies] -wasm-bindgen-test = "0.3" - -[package.metadata.wasm-pack.profile.release] -wasm-opt = false - -[profile.dev] -debug = "line-tables-only" - -[profile.release] -debug = 0 -opt-level = 3 -lto = true -codegen-units = 1 -panic = "abort" -strip = true diff --git a/cgol/README.md b/cgol/README.md deleted file mode 100644 index 6b68408..0000000 --- a/cgol/README.md +++ /dev/null @@ -1,84 +0,0 @@ -
- -

wasm-pack-template

- - A template for kick starting a Rust and WebAssembly project using wasm-pack. - -

- Build Status -

- -

- Tutorial - | - Chat -

- - Built with 🦀🕸 by The Rust and WebAssembly Working Group -
- -## About - -[**📚 Read this template tutorial! 📚**][template-docs] - -This template is designed for compiling Rust libraries into WebAssembly and -publishing the resulting package to NPM. - -Be sure to check out [other `wasm-pack` tutorials online][tutorials] for other -templates and usages of `wasm-pack`. - -[tutorials]: https://rustwasm.github.io/docs/wasm-pack/tutorials/index.html -[template-docs]: https://rustwasm.github.io/docs/wasm-pack/tutorials/npm-browser-packages/index.html - -## 🚴 Usage - -### 🐑 Use `cargo generate` to Clone this Template - -[Learn more about `cargo generate` here.](https://github.com/ashleygwilliams/cargo-generate) - -``` -cargo generate --git https://github.com/rustwasm/wasm-pack-template.git --name my-project -cd my-project -``` - -### 🛠️ Build with `wasm-pack build` - -``` -wasm-pack build -``` - -### 🔬 Test in Headless Browsers with `wasm-pack test` - -``` -wasm-pack test --headless --firefox -``` - -### 🎁 Publish to NPM with `wasm-pack publish` - -``` -wasm-pack publish -``` - -## 🔋 Batteries Included - -* [`wasm-bindgen`](https://github.com/rustwasm/wasm-bindgen) for communicating - between WebAssembly and JavaScript. -* [`console_error_panic_hook`](https://github.com/rustwasm/console_error_panic_hook) - for logging panic messages to the developer console. -* `LICENSE-APACHE` and `LICENSE-MIT`: most Rust projects are licensed this way, so these are included for you - -## License - -Licensed under either of - -* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) -* MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) - -at your option. - -### Contribution - -Unless you explicitly state otherwise, any contribution intentionally -submitted for inclusion in the work by you, as defined in the Apache-2.0 -license, shall be dual licensed as above, without any additional terms or -conditions. diff --git a/cgol/src/lib.rs b/cgol/src/lib.rs deleted file mode 100644 index 31d55b0..0000000 --- a/cgol/src/lib.rs +++ /dev/null @@ -1,483 +0,0 @@ -mod utils; - -use std::cell::Cell; -use std::cell::RefCell; -use std::rc::Rc; -use wasm_bindgen::prelude::*; -use wasm_bindgen::Clamped; -use wasm_bindgen::JsCast; - -use js_sys::Math; -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 - } -} - -// ── Lookup tables (computed once) ────────────────────────────────── - -struct Luts { - /// RGBA for each hue. Index 0 = black (dead cell). - rgb: [[u8; 4]; 256], - sin: [f32; 256], - cos: [f32; 256], -} - -impl Luts { - fn new() -> Self { - let mut t = Self { - rgb: [[0, 0, 0, 255]; 256], - sin: [0.0; 256], - cos: [0.0; 256], - }; - for i in 1..256u16 { - let h = i as f32 / 255.0 * 6.0; - let x = 1.0 - (h % 2.0 - 1.0).abs(); - let (r, g, b) = match h as u32 { - 0 => (1.0f32, 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), - }; - t.rgb[i as usize] = [(r * 255.0) as u8, (g * 255.0) as u8, (b * 255.0) as u8, 255]; - } - for i in 0..256 { - let rad = i as f32 * std::f32::consts::TAU / 256.0; - t.sin[i] = rad.sin(); - t.cos[i] = rad.cos(); - } - t - } - - #[inline] - fn mix(&self, hues: &[u8]) -> u8 { - let mut s = 0.0f32; - let mut c = 0.0f32; - for &h in hues { - 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, - ) - } -} - -// ── Grid ─────────────────────────────────────────────────────────── - -struct Grid { - w: u32, - h: u32, - cells: Vec, - buf: Vec, - hues: Vec, -} - -impl Grid { - fn new(w: u32, h: u32) -> Self { - let n = (w * h) as usize; - let mut g = Self { - w, - h, - cells: vec![DEAD; n], - buf: vec![DEAD; n], - hues: Vec::with_capacity(8), - }; - g.randomize(); - g - } - - fn randomize(&mut self) { - for c in &mut self.cells { - *c = if Math::random() < 0.5 { - safe_hue((Math::random() * 255.0) as u8) - } else { - DEAD - }; - } - } - - fn tick(&mut self, luts: &Luts) { - let (w, h) = (self.w, self.h); - - for row in 0..h { - let n_off = (if row == 0 { h - 1 } else { row - 1 }) * w; - let r_off = row * w; - let s_off = (if row == h - 1 { 0 } else { row + 1 }) * w; - - for col in 0..w { - let we = if col == 0 { w - 1 } else { col - 1 }; - let ea = if col == w - 1 { 0 } else { col + 1 }; - - // Read all 8 neighbors - let ns = [ - self.cells[(n_off + we) as usize], - self.cells[(n_off + col) as usize], - self.cells[(n_off + ea) as usize], - self.cells[(r_off + we) as usize], - self.cells[(r_off + ea) as usize], - self.cells[(s_off + we) as usize], - self.cells[(s_off + col) as usize], - self.cells[(s_off + ea) as usize], - ]; - - // Count alive neighbors (branchless) - let count = ns.iter().fold(0u8, |a, &v| a + (v != DEAD) as u8); - - let i = (r_off + col) as usize; - let cell = self.cells[i]; - let alive = cell != DEAD; - - self.buf[i] = if alive { - if count == 2 || count == 3 { - cell - } else { - DEAD - } - } else if count == 3 { - // Only collect hues for births - self.hues.clear(); - for &v in &ns { - if v != DEAD { - self.hues.push(v); - } - } - luts.mix(&self.hues) - } else { - DEAD - }; - } - } - std::mem::swap(&mut self.cells, &mut self.buf); - } - - fn stamp(&mut self, cr: i32, cc: i32, half: i32, hue: u8) { - let (h, w) = (self.h as i32, self.w as i32); - let hue = safe_hue(hue); - for dr in -half..=half { - for dc in -half..=half { - let r = (cr + dr).rem_euclid(h) as u32; - let c = (cc + dc).rem_euclid(w) as u32; - self.cells[(r * self.w + c) as usize] = hue; - } - } - } -} - -// ── App state ────────────────────────────────────────────────────── - -struct App { - win: Window, - canvas: HtmlCanvasElement, - ctx: CanvasRenderingContext2d, - luts: Luts, - grid: Grid, - pixels: Vec, - cw: u32, - ch: u32, - crow: i32, - ccol: i32, - cursor_on: bool, - last_tick: f64, - dirty: bool, -} - -thread_local! { - static APP_STATE: RefCell>>> = const { RefCell::new(None) }; - static RUNNING: Cell = const { Cell::new(false) }; - static LISTENERS_READY: Cell = const { Cell::new(false) }; - static RAF_CLOSURE: RefCell>> = const { RefCell::new(None) }; -} - -impl App { - fn new() -> Result { - utils::set_panic_hook(); - let win = web_sys::window().ok_or("no window")?; - let doc = win.document().ok_or("no document")?; - let canvas: HtmlCanvasElement = doc - .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()?; - ctx.set_image_smoothing_enabled(false); - - let mut app = Self { - win, - canvas, - ctx, - luts: Luts::new(), - grid: Grid::new(1, 1), - pixels: Vec::new(), - cw: 0, - ch: 0, - crow: 0, - ccol: 0, - cursor_on: false, - last_tick: 0.0, - dirty: true, - }; - app.resize(); - Ok(app) - } - - fn now(&self) -> f64 { - self.win.performance().unwrap().now() - } - - fn resize(&mut self) { - let vw = self.win.inner_width().unwrap().as_f64().unwrap(); - let vh = self.win.inner_height().unwrap().as_f64().unwrap(); - let cols = (vw as u32 / CELL_SIZE).max(1); - let rows = (vh as u32 / CELL_SIZE).max(1); - - self.cw = cols; - self.ch = rows; - self.canvas.set_width(cols); - self.canvas.set_height(rows); - self.canvas - .dyn_ref::() - .unwrap() - .set_attribute( - "style", - &format!( - "position:fixed;inset:0;width:{vw}px;height:{vh}px;image-rendering:pixelated" - ), - ) - .ok(); - self.ctx.set_image_smoothing_enabled(false); - - self.grid = Grid::new(cols, rows); - self.pixels = vec![0u8; (cols * rows * 4) as usize]; - self.dirty = true; - } - - fn draw(&mut self) { - if !self.dirty { - return; - } - self.dirty = false; - - let cells = &self.grid.cells; - let rgb = &self.luts.rgb; - let px = &mut self.pixels; - - for (i, &cell) in cells.iter().enumerate() { - 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) - { - 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(); - } -} - -fn app() -> Result>, JsValue> { - APP_STATE.with(|state| { - if let Some(app) = state.borrow().as_ref() { - return Ok(app.clone()); - } - - let app = Rc::new(RefCell::new(App::new()?)); - state.borrow_mut().replace(app.clone()); - Ok(app) - }) -} - -fn init_listeners(app: Rc>) -> 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(); - - 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; - a.crow = e.client_y() / CELL_SIZE as i32; - a.cursor_on = true; - }) as Box); - doc.add_event_listener_with_callback("mousemove", cb.as_ref().unchecked_ref())?; - cb.forget(); - - 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) { - let mut a = s.borrow_mut(); - a.ccol = t.client_x() / CELL_SIZE as i32; - a.crow = t.client_y() / CELL_SIZE as i32; - a.cursor_on = true; - } - }) as Box); - doc.add_event_listener_with_callback("touchmove", cb.as_ref().unchecked_ref())?; - cb.forget(); - - 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) { - let mut a = s.borrow_mut(); - a.ccol = t.client_x() / CELL_SIZE as i32; - a.crow = t.client_y() / CELL_SIZE as i32; - a.cursor_on = true; - } - }) as Box); - doc.add_event_listener_with_callback("touchstart", cb.as_ref().unchecked_ref())?; - cb.forget(); - - let s = app.clone(); - let cb = Closure::wrap(Box::new(move |_: web_sys::TouchEvent| { - s.borrow_mut().cursor_on = false; - }) as Box); - doc.add_event_listener_with_callback("touchend", cb.as_ref().unchecked_ref())?; - cb.forget(); - - let s = app; - let cb = Closure::wrap(Box::new(move || { - let mut a = s.borrow_mut(); - a.resize(); - if RUNNING.with(|running| running.get()) { - a.draw(); - } else { - a.render_steps(STILL_STEPS); - } - }) as Box); - win.add_event_listener_with_callback("resize", cb.as_ref().unchecked_ref())?; - cb.forget(); - - Ok(()) -} - -fn ensure_animation_loop(app: Rc>) { - RAF_CLOSURE.with(|slot| { - if slot.borrow().is_some() { - return; - } - - let win = app.borrow().win.clone(); - let f: Rc>>> = 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)); - - 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(()) -} diff --git a/cgol/src/utils.rs b/cgol/src/utils.rs deleted file mode 100644 index b1d7929..0000000 --- a/cgol/src/utils.rs +++ /dev/null @@ -1,10 +0,0 @@ -pub fn set_panic_hook() { - // When the `console_error_panic_hook` feature is enabled, we can call the - // `set_panic_hook` function at least once during initialization, and then - // we will get better error messages if our code ever panics. - // - // For more details see - // https://github.com/rustwasm/console_error_panic_hook#readme - #[cfg(feature = "console_error_panic_hook")] - console_error_panic_hook::set_once(); -} diff --git a/eslint.config.js b/eslint.config.js index 46809a3..a06087d 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,39 +1,42 @@ -import tseslint from 'typescript-eslint'; +import tseslint from "typescript-eslint"; export default tseslint.config( { - ignores: ['dist', 'cgol/pkg/**/*'] + ignores: ["dist"], }, { - files: ['**/*.ts'], - extends: [ - ...tseslint.configs.recommended, - ...tseslint.configs.recommendedTypeChecked, - ...tseslint.configs.stylisticTypeChecked - ], - rules: { - "@typescript-eslint/array-type": "off", - "@typescript-eslint/consistent-type-definitions": "off", - "@typescript-eslint/consistent-type-imports": [ - "warn", - { prefer: "type-imports", fixStyle: "inline-type-imports" }, + files: ["**/*.ts"], + extends: [ + ...tseslint.configs.recommended, + ...tseslint.configs.recommendedTypeChecked, + ...tseslint.configs.stylisticTypeChecked, ], - "@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_" }], - "@typescript-eslint/require-await": "off", - "@typescript-eslint/no-misused-promises": [ - "error", - { checksVoidReturn: { attributes: false } }, - ], - }, + rules: { + "@typescript-eslint/array-type": "off", + "@typescript-eslint/consistent-type-definitions": "off", + "@typescript-eslint/consistent-type-imports": [ + "warn", + { prefer: "type-imports", fixStyle: "inline-type-imports" }, + ], + "@typescript-eslint/no-unused-vars": [ + "warn", + { argsIgnorePattern: "^_" }, + ], + "@typescript-eslint/require-await": "off", + "@typescript-eslint/no-misused-promises": [ + "error", + { checksVoidReturn: { attributes: false } }, + ], + }, }, { - linterOptions: { - reportUnusedDisableDirectives: true - }, - languageOptions: { - parserOptions: { - projectService: true - } - } - } -) + linterOptions: { + reportUnusedDisableDirectives: true, + }, + languageOptions: { + parserOptions: { + projectService: true, + }, + }, + }, +); diff --git a/index.html b/index.html index 51ddb40..4ba9f20 100644 --- a/index.html +++ b/index.html @@ -103,13 +103,15 @@ -
+