Compare commits
No commits in common. "17d708eb7a6331ed53a1579d1d4698234bb14594" and "38efffa9b95cc61cd9d4e964e6e0063f64a712cf" have entirely different histories.
17d708eb7a
...
38efffa9b9
24 changed files with 1613 additions and 1758 deletions
20
README.md
20
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 WebGL2 Conway's Game of Life background.
|
||||
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.
|
||||
|
||||
## Features
|
||||
|
||||
- ASCII/ANSI-inspired visual style with the IBM VGA font
|
||||
- Conway's Game of Life running in the background via WebGL2
|
||||
- Conway's Game of Life running in the background via Rust + WebAssembly
|
||||
- Q+A page backed by the site API
|
||||
- Single-file oriented frontend build with Vite
|
||||
- Fullscreen GPU blur/composite for the frosted panel effect
|
||||
- Reduced-motion aware background controls with a static fallback mode
|
||||
|
||||
## Stack
|
||||
|
||||
- Vite
|
||||
- TypeScript
|
||||
- Tailwind CSS v4
|
||||
- WebGL2
|
||||
- Rust + WebAssembly
|
||||
- npm
|
||||
|
||||
## Development
|
||||
|
|
@ -25,6 +25,9 @@ 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
|
||||
|
||||
|
|
@ -32,6 +35,12 @@ 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
|
||||
|
|
@ -54,9 +63,10 @@ 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.
|
||||
- The background renderer targets WebGL2 and falls back to the site shell background if WebGL2 is unavailable.
|
||||
- If WebAssembly fails or motion is disabled, the site falls back to a static background treatment.
|
||||
|
|
|
|||
5
cgol/.cargo/config.toml
Normal file
5
cgol/.cargo/config.toml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
[build]
|
||||
target = "wasm32-unknown-unknown"
|
||||
|
||||
[target.wasm32-unknown-unknown]
|
||||
rustflags = ["-C", "link-arg=--strip-all"]
|
||||
5
cgol/.gitignore
vendored
Normal file
5
cgol/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
/target
|
||||
**/*.rs.bk
|
||||
bin/
|
||||
pkg/
|
||||
wasm-pack.log
|
||||
3
cgol/.vscode/settings.json
vendored
Normal file
3
cgol/.vscode/settings.json
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"rust-analyzer.cargo.target": "wasm32-unknown-unknown",
|
||||
}
|
||||
427
cgol/Cargo.lock
generated
Normal file
427
cgol/Cargo.lock
generated
Normal file
|
|
@ -0,0 +1,427 @@
|
|||
# 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"
|
||||
47
cgol/Cargo.toml
Normal file
47
cgol/Cargo.toml
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
[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
|
||||
84
cgol/README.md
Normal file
84
cgol/README.md
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
<div align="center">
|
||||
|
||||
<h1><code>wasm-pack-template</code></h1>
|
||||
|
||||
<strong>A template for kick starting a Rust and WebAssembly project using <a href="https://github.com/rustwasm/wasm-pack">wasm-pack</a>.</strong>
|
||||
|
||||
<p>
|
||||
<a href="https://travis-ci.org/rustwasm/wasm-pack-template"><img src="https://img.shields.io/travis/rustwasm/wasm-pack-template.svg?style=flat-square" alt="Build Status" /></a>
|
||||
</p>
|
||||
|
||||
<h3>
|
||||
<a href="https://rustwasm.github.io/docs/wasm-pack/tutorials/npm-browser-packages/index.html">Tutorial</a>
|
||||
<span> | </span>
|
||||
<a href="https://discordapp.com/channels/442252698964721669/443151097398296587">Chat</a>
|
||||
</h3>
|
||||
|
||||
<sub>Built with 🦀🕸 by <a href="https://rustwasm.github.io/">The Rust and WebAssembly Working Group</a></sub>
|
||||
</div>
|
||||
|
||||
## 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.
|
||||
483
cgol/src/lib.rs
Normal file
483
cgol/src/lib.rs
Normal file
|
|
@ -0,0 +1,483 @@
|
|||
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<u8>,
|
||||
buf: Vec<u8>,
|
||||
hues: Vec<u8>,
|
||||
}
|
||||
|
||||
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<u8>,
|
||||
cw: u32,
|
||||
ch: u32,
|
||||
crow: i32,
|
||||
ccol: i32,
|
||||
cursor_on: bool,
|
||||
last_tick: f64,
|
||||
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();
|
||||
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::<web_sys::Element>()
|
||||
.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<Rc<RefCell<App>>, 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<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();
|
||||
|
||||
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<dyn FnMut(_)>);
|
||||
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<dyn FnMut(_)>);
|
||||
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<dyn FnMut(_)>);
|
||||
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<dyn FnMut(_)>);
|
||||
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<dyn FnMut()>);
|
||||
win.add_event_listener_with_callback("resize", cb.as_ref().unchecked_ref())?;
|
||||
cb.forget();
|
||||
|
||||
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(())
|
||||
}
|
||||
10
cgol/src/utils.rs
Normal file
10
cgol/src/utils.rs
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
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();
|
||||
}
|
||||
|
|
@ -1,15 +1,15 @@
|
|||
import tseslint from "typescript-eslint";
|
||||
import tseslint from 'typescript-eslint';
|
||||
|
||||
export default tseslint.config(
|
||||
{
|
||||
ignores: ["dist"],
|
||||
ignores: ['dist', 'cgol/pkg/**/*']
|
||||
},
|
||||
{
|
||||
files: ["**/*.ts"],
|
||||
files: ['**/*.ts'],
|
||||
extends: [
|
||||
...tseslint.configs.recommended,
|
||||
...tseslint.configs.recommendedTypeChecked,
|
||||
...tseslint.configs.stylisticTypeChecked,
|
||||
...tseslint.configs.stylisticTypeChecked
|
||||
],
|
||||
rules: {
|
||||
"@typescript-eslint/array-type": "off",
|
||||
|
|
@ -18,10 +18,7 @@ export default tseslint.config(
|
|||
"warn",
|
||||
{ prefer: "type-imports", fixStyle: "inline-type-imports" },
|
||||
],
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"warn",
|
||||
{ argsIgnorePattern: "^_" },
|
||||
],
|
||||
"@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_" }],
|
||||
"@typescript-eslint/require-await": "off",
|
||||
"@typescript-eslint/no-misused-promises": [
|
||||
"error",
|
||||
|
|
@ -31,12 +28,12 @@ export default tseslint.config(
|
|||
},
|
||||
{
|
||||
linterOptions: {
|
||||
reportUnusedDisableDirectives: true,
|
||||
reportUnusedDisableDirectives: true
|
||||
},
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
projectService: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
projectService: true
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
|
|||
24
index.html
24
index.html
|
|
@ -103,27 +103,15 @@
|
|||
<a class="skip-link" href="#outlet">Skip to content</a>
|
||||
<canvas
|
||||
id="canvas"
|
||||
class="pointer-events-none fixed inset-0 z-0 h-screen w-screen"
|
||||
class="fixed top-0 left-0 -z-10 h-screen w-screen"
|
||||
aria-hidden="true"
|
||||
></canvas>
|
||||
<div class="page-frame relative z-10">
|
||||
<div class="page-frame">
|
||||
<nav aria-label="Main navigation" class="site-region">
|
||||
<div class="site-shell site-panel-frame px-[2ch] py-[1ch]">
|
||||
<div class="site-panel-frost" aria-hidden="true"></div>
|
||||
<div class="site-panel-border" aria-hidden="true"></div>
|
||||
<div
|
||||
class="site-panel-content site-nav-links flex justify-center gap-[2ch]"
|
||||
>
|
||||
<a href="/" data-nav-link class="site-nav-link"
|
||||
><span class="site-nav-marker" aria-hidden="true">></span
|
||||
><span>Home</span
|
||||
><span class="site-nav-marker" aria-hidden="true"><</span></a
|
||||
>
|
||||
<a href="/qa" data-nav-link class="site-nav-link"
|
||||
><span class="site-nav-marker" aria-hidden="true">></span
|
||||
><span>Q&A</span
|
||||
><span class="site-nav-marker" aria-hidden="true"><</span></a
|
||||
>
|
||||
<div class="site-shell site-panel px-[2ch] py-[1ch]">
|
||||
<div class="flex justify-center gap-[2ch]">
|
||||
<a href="/" data-nav-link>[HOME]</a>
|
||||
<a href="/qa" data-nav-link>[Q&A]</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
|
|
|||
306
package-lock.json
generated
306
package-lock.json
generated
|
|
@ -7,6 +7,9 @@
|
|||
"": {
|
||||
"name": "website",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"cgol": "file:./cgol/pkg"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.2.1",
|
||||
"@types/node": "^25.3.3",
|
||||
|
|
@ -19,9 +22,15 @@
|
|||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.56.1",
|
||||
"vite": "^7.3.1",
|
||||
"vite-plugin-singlefile": "^2.3.0"
|
||||
"vite-plugin-singlefile": "^2.3.0",
|
||||
"vite-plugin-top-level-await": "^1.6.0",
|
||||
"vite-plugin-wasm": "^3.5.0"
|
||||
}
|
||||
},
|
||||
"cgol/pkg": {
|
||||
"name": "cgol",
|
||||
"version": "0.1.0"
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz",
|
||||
|
|
@ -684,6 +693,24 @@
|
|||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/plugin-virtual": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/plugin-virtual/-/plugin-virtual-3.0.2.tgz",
|
||||
"integrity": "sha512-10monEYsBp3scM4/ND4LNH5Rxvh3e/cVeL3jWTgZ2SrQ+BmUoQcopVQvnaMcOnykb1VkxUFuDAN+0FnpTFRy2A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"rollup": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz",
|
||||
|
|
@ -1034,6 +1061,239 @@
|
|||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@swc/core": {
|
||||
"version": "1.15.18",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.18.tgz",
|
||||
"integrity": "sha512-z87aF9GphWp//fnkRsqvtY+inMVPgYW3zSlXH1kJFvRT5H/wiAn+G32qW5l3oEk63KSF1x3Ov0BfHCObAmT8RA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@swc/counter": "^0.1.3",
|
||||
"@swc/types": "^0.1.25"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/swc"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@swc/core-darwin-arm64": "1.15.18",
|
||||
"@swc/core-darwin-x64": "1.15.18",
|
||||
"@swc/core-linux-arm-gnueabihf": "1.15.18",
|
||||
"@swc/core-linux-arm64-gnu": "1.15.18",
|
||||
"@swc/core-linux-arm64-musl": "1.15.18",
|
||||
"@swc/core-linux-x64-gnu": "1.15.18",
|
||||
"@swc/core-linux-x64-musl": "1.15.18",
|
||||
"@swc/core-win32-arm64-msvc": "1.15.18",
|
||||
"@swc/core-win32-ia32-msvc": "1.15.18",
|
||||
"@swc/core-win32-x64-msvc": "1.15.18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@swc/helpers": ">=0.5.17"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@swc/helpers": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-darwin-arm64": {
|
||||
"version": "1.15.18",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.18.tgz",
|
||||
"integrity": "sha512-+mIv7uBuSaywN3C9LNuWaX1jJJ3SKfiJuE6Lr3bd+/1Iv8oMU7oLBjYMluX1UrEPzwN2qCdY6Io0yVicABoCwQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 AND MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-darwin-x64": {
|
||||
"version": "1.15.18",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.18.tgz",
|
||||
"integrity": "sha512-wZle0eaQhnzxWX5V/2kEOI6Z9vl/lTFEC6V4EWcn+5pDjhemCpQv9e/TDJ0GIoiClX8EDWRvuZwh+Z3dhL1NAg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 AND MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-arm-gnueabihf": {
|
||||
"version": "1.15.18",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.18.tgz",
|
||||
"integrity": "sha512-ao61HGXVqrJFHAcPtF4/DegmwEkVCo4HApnotLU8ognfmU8x589z7+tcf3hU+qBiU1WOXV5fQX6W9Nzs6hjxDw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-arm64-gnu": {
|
||||
"version": "1.15.18",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.18.tgz",
|
||||
"integrity": "sha512-3xnctOBLIq3kj8PxOCgPrGjBLP/kNOddr6f5gukYt/1IZxsITQaU9TDyjeX6jG+FiCIHjCuWuffsyQDL5Ew1bg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 AND MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-arm64-musl": {
|
||||
"version": "1.15.18",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.18.tgz",
|
||||
"integrity": "sha512-0a+Lix+FSSHBSBOA0XznCcHo5/1nA6oLLjcnocvzXeqtdjnPb+SvchItHI+lfeiuj1sClYPDvPMLSLyXFaiIKw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 AND MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-x64-gnu": {
|
||||
"version": "1.15.18",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.18.tgz",
|
||||
"integrity": "sha512-wG9J8vReUlpaHz4KOD/5UE1AUgirimU4UFT9oZmupUDEofxJKYb1mTA/DrMj0s78bkBiNI+7Fo2EgPuvOJfuAA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 AND MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-x64-musl": {
|
||||
"version": "1.15.18",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.18.tgz",
|
||||
"integrity": "sha512-4nwbVvCphKzicwNWRmvD5iBaZj8JYsRGa4xOxJmOyHlMDpsvvJ2OR2cODlvWyGFH6BYL1MfIAK3qph3hp0Az6g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 AND MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-win32-arm64-msvc": {
|
||||
"version": "1.15.18",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.18.tgz",
|
||||
"integrity": "sha512-zk0RYO+LjiBCat2RTMHzAWaMky0cra9loH4oRrLKLLNuL+jarxKLFDA8xTZWEkCPLjUTwlRN7d28eDLLMgtUcQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 AND MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-win32-ia32-msvc": {
|
||||
"version": "1.15.18",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.18.tgz",
|
||||
"integrity": "sha512-yVuTrZ0RccD5+PEkpcLOBAuPbYBXS6rslENvIXfvJGXSdX5QGi1ehC4BjAMl5FkKLiam4kJECUI0l7Hq7T1vwg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 AND MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-win32-x64-msvc": {
|
||||
"version": "1.15.18",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.18.tgz",
|
||||
"integrity": "sha512-7NRmE4hmUQNCbYU3Hn9Tz57mK9Qq4c97ZS+YlamlK6qG9Fb5g/BB3gPDe0iLlJkns/sYv2VWSkm8c3NmbEGjbg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 AND MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/counter": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
|
||||
"integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@swc/types": {
|
||||
"version": "0.1.25",
|
||||
"resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.25.tgz",
|
||||
"integrity": "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@swc/counter": "^0.1.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/wasm": {
|
||||
"version": "1.15.18",
|
||||
"resolved": "https://registry.npmjs.org/@swc/wasm/-/wasm-1.15.18.tgz",
|
||||
"integrity": "sha512-zeSORFArxqUwfVMTRHu8AN9k9LlfSn0CKDSzLhJDITpgLoS0xpnocxsgMjQjUcVYDgO47r9zLP49HEjH/iGsFg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@tailwindcss/node": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.1.tgz",
|
||||
|
|
@ -1650,6 +1910,10 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/cgol": {
|
||||
"resolved": "cgol/pkg",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
|
|
@ -3009,6 +3273,20 @@
|
|||
"punycode": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
|
||||
"integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "7.3.1",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
|
||||
|
|
@ -3101,6 +3379,32 @@
|
|||
"vite": "^5.4.11 || ^6.0.0 || ^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vite-plugin-top-level-await": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/vite-plugin-top-level-await/-/vite-plugin-top-level-await-1.6.0.tgz",
|
||||
"integrity": "sha512-bNhUreLamTIkoulCR9aDXbTbhLk6n1YE8NJUTTxl5RYskNRtzOR0ASzSjBVRtNdjIfngDXo11qOsybGLNsrdww==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rollup/plugin-virtual": "^3.0.2",
|
||||
"@swc/core": "^1.12.14",
|
||||
"@swc/wasm": "^1.12.14",
|
||||
"uuid": "10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vite": ">=2.8"
|
||||
}
|
||||
},
|
||||
"node_modules/vite-plugin-wasm": {
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/vite-plugin-wasm/-/vite-plugin-wasm-3.5.0.tgz",
|
||||
"integrity": "sha512-X5VWgCnqiQEGb+omhlBVsvTfxikKtoOgAzQ95+BZ8gQ+VfMHIjSHr0wyvXFQCa0eKQ0fKyaL0kWcEnYqBac4lQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"vite": "^2 || ^3 || ^4 || ^5 || ^6 || ^7"
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
|
|
|
|||
14
package.json
14
package.json
|
|
@ -5,6 +5,7 @@
|
|||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"build:wasm": "cd cgol && wasm-pack build --release --target web && wasm-opt pkg/cgol_bg.wasm -o pkg/cgol_bg.wasm -O4 --enable-bulk-memory --enable-nontrapping-float-to-int --enable-sign-ext --low-memory-unused --converge",
|
||||
"check": "npm run lint && tsc --noEmit",
|
||||
"dev": "vite",
|
||||
"format:check": "prettier --check \"**/*.{ts,js,jsx,mdx}\" --cache",
|
||||
|
|
@ -14,6 +15,9 @@
|
|||
"preview": "vite preview",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"cgol": "file:./cgol/pkg"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.2.1",
|
||||
"@types/node": "^25.3.3",
|
||||
|
|
@ -26,7 +30,13 @@
|
|||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.56.1",
|
||||
"vite": "^7.3.1",
|
||||
"vite-plugin-singlefile": "^2.3.0"
|
||||
"vite-plugin-singlefile": "^2.3.0",
|
||||
"vite-plugin-top-level-await": "^1.6.0",
|
||||
"vite-plugin-wasm": "^3.5.0"
|
||||
},
|
||||
"knip": {}
|
||||
"knip": {
|
||||
"ignore": [
|
||||
"cgol/pkg/**"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,13 @@
|
|||
export function frostedBox(content: string, extraClasses?: string): string {
|
||||
return `
|
||||
<div class="site-shell site-panel-frame relative my-[2ch] overflow-hidden px-[2ch] py-[2ch] ${extraClasses ?? ""}">
|
||||
<div class="site-panel-frost pointer-events-none" aria-hidden="true"></div>
|
||||
<div class="site-panel-border" aria-hidden="true"></div>
|
||||
<div class="site-panel-content h-full min-h-0">
|
||||
<div class="site-shell relative my-[2ch] overflow-hidden px-[2ch] py-[2ch] ${extraClasses ?? ""}">
|
||||
<div
|
||||
class="pointer-events-none absolute inset-0 h-[200%]"
|
||||
style="background-color: rgba(0, 0, 0, 0.75); backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); mask-image: linear-gradient(to bottom, black 0% 50%, transparent 50% 100%); -webkit-mask-image: linear-gradient(to bottom, black 0% 50%, transparent 50% 100%);"
|
||||
aria-hidden="true"
|
||||
></div>
|
||||
<div class="absolute inset-0 border-2 border-white" aria-hidden="true"></div>
|
||||
<div class="relative z-10 h-full min-h-0">
|
||||
${content}
|
||||
</div>
|
||||
</div>`;
|
||||
|
|
|
|||
78
src/lib/background.ts
Normal file
78
src/lib/background.ts
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
const MOTION_QUERY = "(prefers-reduced-motion: reduce)";
|
||||
const STILL_STEPS = 5;
|
||||
|
||||
type BackgroundMode = "animated" | "still" | "failed";
|
||||
|
||||
interface BackgroundActions {
|
||||
start: () => void;
|
||||
stop: () => void;
|
||||
renderStill: (steps: number) => void;
|
||||
}
|
||||
|
||||
function getMode(reducedMotion: boolean): BackgroundMode {
|
||||
return reducedMotion ? "still" : "animated";
|
||||
}
|
||||
|
||||
function applyCanvasState(mode: BackgroundMode) {
|
||||
const canvas = document.getElementById("canvas");
|
||||
document.body.dataset.backgroundMode = mode;
|
||||
if (canvas) {
|
||||
canvas.toggleAttribute("hidden", mode === "failed");
|
||||
}
|
||||
}
|
||||
|
||||
export function initBackgroundControls(actions: BackgroundActions) {
|
||||
const media = window.matchMedia(MOTION_QUERY);
|
||||
let mode: BackgroundMode = getMode(media.matches);
|
||||
let failed = false;
|
||||
|
||||
const applyMode = (restartAnimation = false) => {
|
||||
if (failed) return;
|
||||
|
||||
mode = getMode(media.matches);
|
||||
applyCanvasState(mode);
|
||||
|
||||
if (mode === "animated") {
|
||||
if (restartAnimation) {
|
||||
actions.stop();
|
||||
}
|
||||
|
||||
actions.start();
|
||||
return;
|
||||
}
|
||||
|
||||
actions.stop();
|
||||
actions.renderStill(STILL_STEPS);
|
||||
};
|
||||
|
||||
const restartAnimation = () => {
|
||||
if (document.visibilityState === "hidden" || media.matches) {
|
||||
return;
|
||||
}
|
||||
|
||||
applyMode(true);
|
||||
};
|
||||
|
||||
media.addEventListener("change", () => {
|
||||
applyMode();
|
||||
});
|
||||
|
||||
document.addEventListener("visibilitychange", () => {
|
||||
restartAnimation();
|
||||
});
|
||||
|
||||
window.addEventListener("pageshow", () => {
|
||||
restartAnimation();
|
||||
});
|
||||
|
||||
return {
|
||||
applyInitialMode() {
|
||||
applyMode();
|
||||
},
|
||||
setFailed() {
|
||||
failed = true;
|
||||
mode = "failed";
|
||||
applyCanvasState(mode);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -28,17 +28,15 @@ export function renderFooter() {
|
|||
const mirror = getMirrorLink();
|
||||
|
||||
footer.innerHTML = `
|
||||
<div class="site-panel-frame px-[2ch] py-[1ch]">
|
||||
<div class="site-panel-frost" aria-hidden="true"></div>
|
||||
<div class="site-panel-border" aria-hidden="true"></div>
|
||||
<div class="site-panel-content site-footer-inner">
|
||||
<a href="${REPO_URL}">Src</a>
|
||||
<div class="site-panel px-[2ch] py-[1ch]">
|
||||
<div class="site-footer-inner">
|
||||
<a href="${REPO_URL}">src</a>
|
||||
<span aria-hidden="true">|</span>
|
||||
<a href="/qa/rss.xml" data-native-link>RSS</a>
|
||||
<a href="/qa/rss.xml" data-native-link>rss</a>
|
||||
<span aria-hidden="true">|</span>
|
||||
<a href="/pgp.txt" data-native-link>PGP</a>
|
||||
<a href="/pgp.txt" data-native-link>pgp</a>
|
||||
<span aria-hidden="true">|</span>
|
||||
<a href="/ssh.txt" data-native-link>SSH</a>
|
||||
<a href="/ssh.txt" data-native-link>ssh</a>
|
||||
<span aria-hidden="true">|</span>
|
||||
<a href="${mirror.href}">${mirror.label}</a>
|
||||
</div>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
24
src/main.ts
24
src/main.ts
|
|
@ -1,6 +1,7 @@
|
|||
import "~/styles/globals.css";
|
||||
import init, { render_still, start, stop } from "cgol";
|
||||
import { route, initRouter } from "~/router";
|
||||
import { initWebGLBackground } from "~/lib/webgl-background";
|
||||
import { initBackgroundControls } from "~/lib/background";
|
||||
import { renderFooter } from "~/lib/site";
|
||||
import { homePage } from "~/pages/home";
|
||||
import { qaPage } from "~/pages/qa";
|
||||
|
|
@ -11,5 +12,24 @@ route("/qa", "Jet Pham - Q+A", qaPage);
|
|||
route("*", "404 - Jet Pham", notFoundPage);
|
||||
|
||||
renderFooter();
|
||||
initWebGLBackground();
|
||||
const background = initBackgroundControls({
|
||||
start() {
|
||||
start();
|
||||
},
|
||||
stop() {
|
||||
stop();
|
||||
},
|
||||
renderStill(steps) {
|
||||
render_still(steps);
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
await init();
|
||||
background.applyInitialMode();
|
||||
} catch (e) {
|
||||
background.setFailed();
|
||||
console.error("WASM init failed:", e);
|
||||
}
|
||||
|
||||
initRouter();
|
||||
|
|
|
|||
|
|
@ -6,31 +6,30 @@ export function homePage(outlet: HTMLElement) {
|
|||
outlet.innerHTML = `
|
||||
<div class="flex flex-col items-center justify-start">
|
||||
${frostedBox(`
|
||||
<div class="flex flex-col items-center justify-center gap-[1.25ch] md:gap-[2ch] md:flex-row">
|
||||
<div class="flex flex-col items-center justify-center gap-[2ch] md:flex-row">
|
||||
<div class="order-1 flex flex-col items-center md:order-2">
|
||||
<h1 class="sr-only">Jet Pham</h1>
|
||||
<div aria-hidden="true" data-emitter-ansi>${Jet}</div>
|
||||
<div aria-hidden="true">${Jet}</div>
|
||||
<p class="mt-[2ch]">Software Extremist</p>
|
||||
</div>
|
||||
<div class="order-2 shrink-0 md:order-1">
|
||||
<img
|
||||
data-emitter-image
|
||||
src="/jet.svg"
|
||||
alt="A picture of Jet wearing a beanie in purple and blue lighting"
|
||||
width="250"
|
||||
height="250"
|
||||
class="aspect-square w-full max-w-[220px] object-cover md:h-[263px] md:w-[175px] md:max-w-none"
|
||||
class="aspect-square w-full max-w-[250px] object-cover md:h-[263px] md:w-[175px] md:max-w-none"
|
||||
style="background-color: #a80055; color: transparent"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<fieldset class="section-block mt-[2ch] border-2 px-[calc(1.5ch-0.5px)] pb-[1ch] pt-0" style="border-color: var(--white)">
|
||||
<legend class="-mx-[0.5ch] px-[0.5ch]" style="color: var(--white)">Contact</legend>
|
||||
<fieldset class="section-block mt-[2ch] border-2 border-white px-[calc(1.5ch-0.5px)] pb-[1ch] pt-0">
|
||||
<legend class="-mx-[0.5ch] px-[0.5ch] text-white">Contact</legend>
|
||||
<button type="button" id="copy-email" class="qa-inline-action">jet@extremist.software</button>
|
||||
<span id="copy-email-status" class="qa-meta ml-[1ch]" aria-live="polite"></span>
|
||||
</fieldset>
|
||||
<fieldset class="section-block mt-[2ch] border-2 px-[calc(1.5ch-0.5px)] pb-[1ch] pt-0" style="border-color: var(--white)">
|
||||
<legend class="-mx-[0.5ch] px-[0.5ch]" style="color: var(--white)">Links</legend>
|
||||
<fieldset class="section-block mt-[2ch] border-2 border-white px-[calc(1.5ch-0.5px)] pb-[1ch] pt-0">
|
||||
<legend class="-mx-[0.5ch] px-[0.5ch] text-white">Links</legend>
|
||||
<ol>
|
||||
<li><a href="https://git.extremist.software" class="inline-flex items-center">Forgejo</a></li>
|
||||
<li><a href="https://github.com/jetpham" class="inline-flex items-center">GitHub</a></li>
|
||||
|
|
|
|||
|
|
@ -61,7 +61,6 @@ async function render() {
|
|||
outlet.innerHTML = "";
|
||||
await r.handler(outlet, params);
|
||||
document.title = r.title;
|
||||
document.dispatchEvent(new Event("site:render"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
@ -71,7 +70,6 @@ async function render() {
|
|||
await notFoundHandler(outlet, {});
|
||||
}
|
||||
document.title = notFoundTitle;
|
||||
document.dispatchEvent(new Event("site:render"));
|
||||
}
|
||||
|
||||
export function initRouter() {
|
||||
|
|
|
|||
|
|
@ -33,14 +33,6 @@
|
|||
--light-magenta: #ff55ff;
|
||||
--yellow: #ffff55;
|
||||
--white: #ffffff;
|
||||
--panel-bg-alpha: 0.2;
|
||||
--panel-gloss-alpha: 0.05;
|
||||
--panel-border-inset: 0.6rem;
|
||||
--panel-bg: rgba(0, 0, 0, var(--panel-bg-alpha));
|
||||
--panel-blur: 10px;
|
||||
--fallback-glow-a: rgba(85, 255, 255, 0.18);
|
||||
--fallback-glow-b: rgba(85, 85, 255, 0.16);
|
||||
--fallback-glow-c: rgba(255, 85, 85, 0.14);
|
||||
}
|
||||
|
||||
html {
|
||||
|
|
@ -51,8 +43,6 @@ html {
|
|||
body {
|
||||
min-height: 100vh;
|
||||
min-height: 100dvh;
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
background-color: var(--black);
|
||||
|
|
@ -63,38 +53,6 @@ body {
|
|||
line-height: 1;
|
||||
}
|
||||
|
||||
body::before {
|
||||
content: "";
|
||||
position: fixed;
|
||||
inset: -20vmax;
|
||||
z-index: -30;
|
||||
background: var(--black);
|
||||
opacity: 0;
|
||||
transform: translate3d(0, 0, 0) scale(1);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
body[data-background-mode="failed"]::before {
|
||||
opacity: 1;
|
||||
animation: fallback-drift 18s linear infinite alternate;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
body[data-background-mode="failed"]::before {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fallback-drift {
|
||||
from {
|
||||
transform: translate3d(-3%, -2%, 0) scale(1);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translate3d(3%, 2%, 0) scale(1.12);
|
||||
}
|
||||
}
|
||||
|
||||
::selection {
|
||||
background-color: var(--light-blue);
|
||||
color: var(--black);
|
||||
|
|
@ -140,47 +98,18 @@ body[data-background-mode="failed"]::before {
|
|||
}
|
||||
|
||||
.site-shell {
|
||||
width: 100%;
|
||||
width: min(100%, 60%);
|
||||
box-sizing: border-box;
|
||||
margin: 0 auto;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.site-shell {
|
||||
width: min(100%, 60%);
|
||||
}
|
||||
}
|
||||
|
||||
.site-panel-frame {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.site-panel-frost {
|
||||
position: absolute;
|
||||
inset: var(--panel-border-inset);
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
background:
|
||||
linear-gradient(
|
||||
to bottom,
|
||||
rgba(255, 255, 255, var(--panel-gloss-alpha)),
|
||||
transparent 24%
|
||||
),
|
||||
var(--panel-bg);
|
||||
}
|
||||
|
||||
.site-panel-border {
|
||||
position: absolute;
|
||||
inset: var(--panel-border-inset);
|
||||
.site-panel {
|
||||
border: 2px solid var(--white);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.site-panel-content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
background-color: rgba(0, 0, 0, 0.75);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.site-region {
|
||||
|
|
@ -221,36 +150,6 @@ a[aria-current="page"] {
|
|||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.site-nav-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
color: var(--light-blue);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.site-nav-link:hover,
|
||||
.site-nav-link:focus-visible {
|
||||
background-color: transparent;
|
||||
color: var(--light-blue);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.site-nav-link[aria-current="page"] {
|
||||
color: var(--yellow);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.site-nav-marker {
|
||||
display: inline-block;
|
||||
width: 1ch;
|
||||
color: currentColor;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.site-nav-link[aria-current="page"] .site-nav-marker {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.skip-link {
|
||||
position: absolute;
|
||||
left: 1rem;
|
||||
|
|
|
|||
|
|
@ -13,5 +13,5 @@
|
|||
}
|
||||
},
|
||||
"include": ["src", "vite.config.ts", "vite-plugin-ansi.ts"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
"exclude": ["node_modules", "dist", "cgol/pkg"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,51 +4,51 @@ import fs from "node:fs";
|
|||
import type { Plugin } from "vite";
|
||||
|
||||
const colorMap: Record<string, string> = {
|
||||
"ansi-black": "color: var(--black)",
|
||||
"ansi-red": "color: var(--red)",
|
||||
"ansi-green": "color: var(--green)",
|
||||
"ansi-yellow": "color: var(--brown)",
|
||||
"ansi-blue": "color: var(--blue)",
|
||||
"ansi-magenta": "color: var(--magenta)",
|
||||
"ansi-cyan": "color: var(--cyan)",
|
||||
"ansi-white": "color: var(--light-gray)",
|
||||
"ansi-bright-black": "color: var(--dark-gray)",
|
||||
"ansi-bright-red": "color: var(--light-red)",
|
||||
"ansi-bright-green": "color: var(--light-green)",
|
||||
"ansi-bright-yellow": "color: var(--yellow)",
|
||||
"ansi-bright-blue": "color: var(--light-blue)",
|
||||
"ansi-bright-magenta": "color: var(--light-magenta)",
|
||||
"ansi-bright-cyan": "color: var(--light-cyan)",
|
||||
"ansi-bright-white": "color: var(--white)",
|
||||
"ansi-black": "text-[var(--black)]",
|
||||
"ansi-red": "text-[var(--red)]",
|
||||
"ansi-green": "text-[var(--green)]",
|
||||
"ansi-yellow": "text-[var(--brown)]",
|
||||
"ansi-blue": "text-[var(--blue)]",
|
||||
"ansi-magenta": "text-[var(--magenta)]",
|
||||
"ansi-cyan": "text-[var(--cyan)]",
|
||||
"ansi-white": "text-[var(--light-gray)]",
|
||||
"ansi-bright-black": "text-[var(--dark-gray)]",
|
||||
"ansi-bright-red": "text-[var(--light-red)]",
|
||||
"ansi-bright-green": "text-[var(--light-green)]",
|
||||
"ansi-bright-yellow": "text-[var(--yellow)]",
|
||||
"ansi-bright-blue": "text-[var(--light-blue)]",
|
||||
"ansi-bright-magenta": "text-[var(--light-magenta)]",
|
||||
"ansi-bright-cyan": "text-[var(--light-cyan)]",
|
||||
"ansi-bright-white": "text-[var(--white)]",
|
||||
};
|
||||
|
||||
const bgColorMap: Record<string, string> = {
|
||||
"ansi-black": "background-color: transparent",
|
||||
"ansi-red": "background-color: var(--red)",
|
||||
"ansi-green": "background-color: var(--green)",
|
||||
"ansi-yellow": "background-color: var(--brown)",
|
||||
"ansi-blue": "background-color: var(--blue)",
|
||||
"ansi-magenta": "background-color: var(--magenta)",
|
||||
"ansi-cyan": "background-color: var(--cyan)",
|
||||
"ansi-white": "background-color: var(--light-gray)",
|
||||
"ansi-bright-black": "background-color: var(--dark-gray)",
|
||||
"ansi-bright-red": "background-color: var(--light-red)",
|
||||
"ansi-bright-green": "background-color: var(--light-green)",
|
||||
"ansi-bright-yellow": "background-color: var(--yellow)",
|
||||
"ansi-bright-blue": "background-color: var(--light-blue)",
|
||||
"ansi-bright-magenta": "background-color: var(--light-magenta)",
|
||||
"ansi-bright-cyan": "background-color: var(--light-cyan)",
|
||||
"ansi-bright-white": "background-color: var(--white)",
|
||||
"ansi-black": "bg-transparent",
|
||||
"ansi-red": "bg-[var(--red)]",
|
||||
"ansi-green": "bg-[var(--green)]",
|
||||
"ansi-yellow": "bg-[var(--brown)]",
|
||||
"ansi-blue": "bg-[var(--blue)]",
|
||||
"ansi-magenta": "bg-[var(--magenta)]",
|
||||
"ansi-cyan": "bg-[var(--cyan)]",
|
||||
"ansi-white": "bg-[var(--light-gray)]",
|
||||
"ansi-bright-black": "bg-[var(--dark-gray)]",
|
||||
"ansi-bright-red": "bg-[var(--light-red)]",
|
||||
"ansi-bright-green": "bg-[var(--light-green)]",
|
||||
"ansi-bright-yellow": "bg-[var(--yellow)]",
|
||||
"ansi-bright-blue": "bg-[var(--light-blue)]",
|
||||
"ansi-bright-magenta": "bg-[var(--light-magenta)]",
|
||||
"ansi-bright-cyan": "bg-[var(--light-cyan)]",
|
||||
"ansi-bright-white": "bg-[var(--white)]",
|
||||
};
|
||||
|
||||
const decorationMap: Record<string, string> = {
|
||||
bold: "font-weight: 700",
|
||||
dim: "opacity: 0.5",
|
||||
italic: "font-style: italic",
|
||||
hidden: "visibility: hidden",
|
||||
strikethrough: "text-decoration: line-through",
|
||||
underline: "text-decoration: underline",
|
||||
blink: "animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite",
|
||||
bold: "font-bold",
|
||||
dim: "opacity-50",
|
||||
italic: "italic",
|
||||
hidden: "invisible",
|
||||
strikethrough: "line-through",
|
||||
underline: "underline",
|
||||
blink: "animate-pulse",
|
||||
};
|
||||
|
||||
function fixBackspace(txt: string): string {
|
||||
|
|
@ -60,18 +60,18 @@ function fixBackspace(txt: string): string {
|
|||
return txt;
|
||||
}
|
||||
|
||||
function createStyle(bundle: AnserJsonEntry): string | null {
|
||||
const declarations: string[] = [];
|
||||
function createClass(bundle: AnserJsonEntry): string | null {
|
||||
const classes: string[] = [];
|
||||
if (bundle.bg && bgColorMap[bundle.bg]) {
|
||||
declarations.push(bgColorMap[bundle.bg]!);
|
||||
classes.push(bgColorMap[bundle.bg]!);
|
||||
}
|
||||
if (bundle.fg && colorMap[bundle.fg]) {
|
||||
declarations.push(colorMap[bundle.fg]!);
|
||||
classes.push(colorMap[bundle.fg]!);
|
||||
}
|
||||
if (bundle.decoration && decorationMap[bundle.decoration]) {
|
||||
declarations.push(decorationMap[bundle.decoration]!);
|
||||
classes.push(decorationMap[bundle.decoration]!);
|
||||
}
|
||||
return declarations.length ? declarations.join("; ") : null;
|
||||
return classes.length ? classes.join(" ") : null;
|
||||
}
|
||||
|
||||
function escapeHtml(str: string): string {
|
||||
|
|
@ -92,10 +92,10 @@ function renderAnsiToHtml(raw: string): string {
|
|||
|
||||
const spans = bundles
|
||||
.map((bundle) => {
|
||||
const style = createStyle(bundle);
|
||||
const cls = createClass(bundle);
|
||||
const content = escapeHtml(bundle.content);
|
||||
if (style) {
|
||||
return `<span style="${style}">${content}</span>`;
|
||||
if (cls) {
|
||||
return `<span class="${cls}">${content}</span>`;
|
||||
}
|
||||
return `<span>${content}</span>`;
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import { defineConfig } from "vite";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import wasm from "vite-plugin-wasm";
|
||||
import topLevelAwait from "vite-plugin-top-level-await";
|
||||
import { viteSingleFile } from "vite-plugin-singlefile";
|
||||
import ansi from "./vite-plugin-ansi";
|
||||
|
||||
|
|
@ -7,6 +9,8 @@ export default defineConfig({
|
|||
plugins: [
|
||||
ansi(),
|
||||
tailwindcss(),
|
||||
wasm(),
|
||||
topLevelAwait(),
|
||||
viteSingleFile({ useRecommendedBuildConfig: false }),
|
||||
],
|
||||
resolve: {
|
||||
|
|
@ -16,14 +20,12 @@ export default defineConfig({
|
|||
},
|
||||
server: {
|
||||
headers: {
|
||||
"Cache-Control": "no-store",
|
||||
"Cross-Origin-Opener-Policy": "same-origin",
|
||||
"Cross-Origin-Embedder-Policy": "require-corp",
|
||||
},
|
||||
},
|
||||
preview: {
|
||||
headers: {
|
||||
"Cache-Control": "no-store",
|
||||
"Cross-Origin-Opener-Policy": "same-origin",
|
||||
"Cross-Origin-Embedder-Policy": "require-corp",
|
||||
},
|
||||
|
|
@ -32,6 +34,5 @@ export default defineConfig({
|
|||
target: "esnext",
|
||||
assetsInlineLimit: 0,
|
||||
cssCodeSplit: false,
|
||||
cssMinify: false,
|
||||
},
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue