Compare commits
7 commits
afb117b42b
...
56f8e05ae0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
56f8e05ae0 | ||
|
|
3364a6ae9b | ||
|
|
7b726be760 | ||
|
|
2ee196f43d | ||
|
|
e7b44ba112 | ||
|
|
bf5900edbb | ||
|
|
fc80ead81e |
14
.env.example
|
|
@ -1,14 +0,0 @@
|
||||||
# Since the ".env" file is gitignored, you can use the ".env.example" file to
|
|
||||||
# build a new ".env" file when you clone the repo. Keep this file up-to-date
|
|
||||||
# when you add new variables to `.env`.
|
|
||||||
|
|
||||||
# This file will be committed to version control, so make sure not to have any
|
|
||||||
# secrets in it. If you are cloning this repo, create a copy of this file named
|
|
||||||
# ".env" and populate it with your secrets.
|
|
||||||
|
|
||||||
# When adding additional environment variables, the schema in "/src/env.js"
|
|
||||||
# should be updated accordingly.
|
|
||||||
|
|
||||||
# Prisma
|
|
||||||
# https://www.prisma.io/docs/reference/database-reference/connection-urls#env
|
|
||||||
DATABASE_URL="postgresql://postgres:password@localhost:5432/website"
|
|
||||||
25
.gitignore
vendored
|
|
@ -1,25 +1,11 @@
|
||||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
|
||||||
|
|
||||||
# dependencies
|
# dependencies
|
||||||
/node_modules
|
/node_modules
|
||||||
/.pnp
|
|
||||||
.pnp.js
|
|
||||||
|
|
||||||
# testing
|
# testing
|
||||||
/coverage
|
/coverage
|
||||||
|
|
||||||
# database
|
# vite
|
||||||
/prisma/db.sqlite
|
/dist
|
||||||
/prisma/db.sqlite-journal
|
|
||||||
db.sqlite
|
|
||||||
|
|
||||||
# next.js
|
|
||||||
/.next/
|
|
||||||
/out/
|
|
||||||
next-env.d.ts
|
|
||||||
|
|
||||||
# production
|
|
||||||
/build
|
|
||||||
|
|
||||||
# misc
|
# misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
@ -31,14 +17,10 @@ yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
.pnpm-debug.log*
|
.pnpm-debug.log*
|
||||||
|
|
||||||
# local env files
|
# env files
|
||||||
# do not commit any .env files to git, except for the .env.example file. https://create.t3.gg/en/usage/env-variables#using-environment-variables
|
|
||||||
.env
|
.env
|
||||||
.env*.local
|
.env*.local
|
||||||
|
|
||||||
# vercel
|
|
||||||
.vercel
|
|
||||||
|
|
||||||
# typescript
|
# typescript
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
|
@ -46,4 +28,3 @@ yarn-error.log*
|
||||||
.idea
|
.idea
|
||||||
|
|
||||||
/.direnv
|
/.direnv
|
||||||
/generated
|
|
||||||
|
|
@ -1,2 +1,8 @@
|
||||||
[build]
|
[build]
|
||||||
target = "wasm32-unknown-unknown"
|
target = "wasm32-unknown-unknown"
|
||||||
|
|
||||||
|
[target.wasm32-unknown-unknown]
|
||||||
|
rustflags = ["-C", "link-arg=--strip-all"]
|
||||||
|
|
||||||
|
[unstable]
|
||||||
|
build-std = ["core", "alloc", "std", "panic_abort"]
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
cargo-features = ["panic-immediate-abort"]
|
||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "cgol"
|
name = "cgol"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
@ -5,16 +7,12 @@ authors = ["jet"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
crate-type = ["cdylib", "rlib"]
|
crate-type = ["cdylib"]
|
||||||
|
|
||||||
[features]
|
|
||||||
default = ["console_error_panic_hook"]
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
wasm-bindgen = "0.2"
|
wasm-bindgen = "0.2"
|
||||||
js-sys = "0.3"
|
js-sys = "0.3"
|
||||||
web-sys = { version = "0.3", features = [
|
web-sys = { version = "0.3", features = [
|
||||||
"console",
|
|
||||||
"CanvasRenderingContext2d",
|
"CanvasRenderingContext2d",
|
||||||
"Document",
|
"Document",
|
||||||
"HtmlCanvasElement",
|
"HtmlCanvasElement",
|
||||||
|
|
@ -23,23 +21,28 @@ web-sys = { version = "0.3", features = [
|
||||||
"Element",
|
"Element",
|
||||||
"EventTarget",
|
"EventTarget",
|
||||||
"Performance",
|
"Performance",
|
||||||
"DomRect",
|
|
||||||
"TouchEvent",
|
"TouchEvent",
|
||||||
"Touch",
|
"Touch",
|
||||||
"TouchList",
|
"TouchList",
|
||||||
"ImageData",
|
"ImageData",
|
||||||
] }
|
] }
|
||||||
|
|
||||||
# The `console_error_panic_hook` crate provides better debugging of panics by
|
|
||||||
# logging them with `console.error`. This is great for development, but requires
|
|
||||||
# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for
|
|
||||||
# code size when deploying.
|
|
||||||
console_error_panic_hook = { version = "0.1", optional = true }
|
console_error_panic_hook = { version = "0.1", optional = true }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = ["console_error_panic_hook"]
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
wasm-bindgen-test = "0.3"
|
wasm-bindgen-test = "0.3"
|
||||||
|
|
||||||
|
[package.metadata.wasm-pack.profile.release]
|
||||||
|
wasm-opt = false
|
||||||
|
|
||||||
|
[profile.dev]
|
||||||
|
debug = "line-tables-only"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
opt-level = 3
|
opt-level = 3
|
||||||
lto = true
|
lto = true
|
||||||
codegen-units = 1
|
codegen-units = 1
|
||||||
|
panic = "immediate-abort"
|
||||||
|
strip = true
|
||||||
|
|
|
||||||
574
cgol/src/lib.rs
|
|
@ -10,454 +10,364 @@ use js_sys::Math;
|
||||||
use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement, ImageData, Window};
|
use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement, ImageData, Window};
|
||||||
|
|
||||||
const CELL_SIZE: u32 = 20;
|
const CELL_SIZE: u32 = 20;
|
||||||
const SIMULATION_FPS: f64 = 60.0;
|
const TICK_MS: f64 = 1000.0 / 60.0;
|
||||||
const SIMULATION_FRAME_MS: f64 = 1000.0 / SIMULATION_FPS;
|
const HUE_PERIOD_MS: f64 = 3000.0;
|
||||||
const HUE_ROTATION_PERIOD_MS: f64 = 3000.0;
|
|
||||||
|
|
||||||
fn random_hue() -> u8 {
|
/// Cells: 0 = dead, 1-255 = alive with that hue value.
|
||||||
(Math::random() * 256.0) as u8
|
const DEAD: u8 = 0;
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
fn safe_hue(h: u8) -> u8 {
|
||||||
|
if h == DEAD { 1 } else { h }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mix_colors(hues: &[u8]) -> u8 {
|
// ── Lookup tables (computed once) ──────────────────────────────────
|
||||||
if hues.is_empty() {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
let (sum_sin, sum_cos) = hues
|
struct Luts {
|
||||||
.iter()
|
/// RGBA for each hue. Index 0 = black (dead cell).
|
||||||
.map(|&hue| (hue as f64 * 360.0 / 255.0).to_radians())
|
rgb: [[u8; 4]; 256],
|
||||||
.fold((0.0, 0.0), |acc, h_rad| {
|
sin: [f32; 256],
|
||||||
(acc.0 + h_rad.sin(), acc.1 + h_rad.cos())
|
cos: [f32; 256],
|
||||||
});
|
|
||||||
let avg_hue_degrees = sum_sin.atan2(sum_cos).to_degrees().rem_euclid(360.0);
|
|
||||||
(avg_hue_degrees * 255.0 / 360.0) as u8
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert HSL hue (0-255) to RGB
|
impl Luts {
|
||||||
/// Using full saturation (100%) and lightness (50%)
|
fn new() -> Self {
|
||||||
fn hue_to_rgb(hue: u8) -> (u8, u8, u8) {
|
let mut t = Self {
|
||||||
let h = hue as f64 / 255.0 * 6.0;
|
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 x = 1.0 - (h % 2.0 - 1.0).abs();
|
||||||
|
|
||||||
let (r, g, b) = match h as u32 {
|
let (r, g, b) = match h as u32 {
|
||||||
0 => (1.0, x, 0.0),
|
0 => (1.0f32, x, 0.0),
|
||||||
1 => (x, 1.0, 0.0),
|
1 => (x, 1.0, 0.0),
|
||||||
2 => (0.0, 1.0, x),
|
2 => (0.0, 1.0, x),
|
||||||
3 => (0.0, x, 1.0),
|
3 => (0.0, x, 1.0),
|
||||||
4 => (x, 0.0, 1.0),
|
4 => (x, 0.0, 1.0),
|
||||||
_ => (1.0, 0.0, x),
|
_ => (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
|
||||||
|
}
|
||||||
|
|
||||||
((r * 255.0) as u8, (g * 255.0) as u8, (b * 255.0) as u8)
|
#[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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq)]
|
// ── Grid ───────────────────────────────────────────────────────────
|
||||||
enum Cell {
|
|
||||||
Dead,
|
struct Grid {
|
||||||
Alive { hue: u8 },
|
w: u32,
|
||||||
|
h: u32,
|
||||||
|
cells: Vec<u8>,
|
||||||
|
buf: Vec<u8>,
|
||||||
|
hues: Vec<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Universe {
|
impl Grid {
|
||||||
width: u32,
|
fn new(w: u32, h: u32) -> Self {
|
||||||
height: u32,
|
let n = (w * h) as usize;
|
||||||
cells: Vec<Cell>,
|
let mut g = Self {
|
||||||
next_cells: Vec<Cell>,
|
w,
|
||||||
neighbor_hues_buffer: Vec<u8>,
|
h,
|
||||||
}
|
cells: vec![DEAD; n],
|
||||||
|
buf: vec![DEAD; n],
|
||||||
impl Universe {
|
hues: Vec::with_capacity(8),
|
||||||
fn new(width: u32, height: u32) -> Self {
|
|
||||||
let size = (width * height) as usize;
|
|
||||||
let mut u = Self {
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
cells: vec![Cell::Dead; size],
|
|
||||||
next_cells: vec![Cell::Dead; size],
|
|
||||||
neighbor_hues_buffer: Vec::with_capacity(8),
|
|
||||||
};
|
};
|
||||||
u.randomize();
|
g.randomize();
|
||||||
u
|
g
|
||||||
}
|
}
|
||||||
|
|
||||||
fn randomize(&mut self) {
|
fn randomize(&mut self) {
|
||||||
for c in &mut self.cells {
|
for c in &mut self.cells {
|
||||||
*c = if Math::random() < 0.5 {
|
*c = if Math::random() < 0.5 {
|
||||||
Cell::Alive { hue: random_hue() }
|
safe_hue((Math::random() * 255.0) as u8)
|
||||||
} else {
|
} else {
|
||||||
Cell::Dead
|
DEAD
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline(always)]
|
fn tick(&mut self, luts: &Luts) {
|
||||||
fn index(&self, row: u32, col: u32) -> usize {
|
let (w, h) = (self.w, self.h);
|
||||||
(row * self.width + col) as usize
|
|
||||||
}
|
|
||||||
|
|
||||||
fn count_neighbors_and_get_hues(&mut self, row: u32, col: u32) -> u8 {
|
for row in 0..h {
|
||||||
self.neighbor_hues_buffer.clear();
|
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;
|
||||||
|
|
||||||
let north = if row == 0 { self.height - 1 } else { row - 1 };
|
for col in 0..w {
|
||||||
let south = if row == self.height - 1 { 0 } else { row + 1 };
|
let we = if col == 0 { w - 1 } else { col - 1 };
|
||||||
let west = if col == 0 { self.width - 1 } else { col - 1 };
|
let ea = if col == w - 1 { 0 } else { col + 1 };
|
||||||
let east = if col == self.width - 1 { 0 } else { col + 1 };
|
|
||||||
|
|
||||||
let neighbors = [
|
// Read all 8 neighbors
|
||||||
(north, west),
|
let ns = [
|
||||||
(north, col),
|
self.cells[(n_off + we) as usize],
|
||||||
(north, east),
|
self.cells[(n_off + col) as usize],
|
||||||
(row, west),
|
self.cells[(n_off + ea) as usize],
|
||||||
(row, east),
|
self.cells[(r_off + we) as usize],
|
||||||
(south, west),
|
self.cells[(r_off + ea) as usize],
|
||||||
(south, col),
|
self.cells[(s_off + we) as usize],
|
||||||
(south, east),
|
self.cells[(s_off + col) as usize],
|
||||||
|
self.cells[(s_off + ea) as usize],
|
||||||
];
|
];
|
||||||
|
|
||||||
for (nr, nc) in neighbors {
|
// Count alive neighbors (branchless)
|
||||||
let idx = self.index(nr, nc);
|
let count = ns.iter().fold(0u8, |a, &v| a + (v != DEAD) as u8);
|
||||||
if let Cell::Alive { hue } = self.cells[idx] {
|
|
||||||
self.neighbor_hues_buffer.push(hue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.neighbor_hues_buffer.len() as u8
|
let i = (r_off + col) as usize;
|
||||||
}
|
let cell = self.cells[i];
|
||||||
|
let alive = cell != DEAD;
|
||||||
|
|
||||||
fn tick(&mut self) {
|
self.buf[i] = if alive {
|
||||||
for row in 0..self.height {
|
if count == 2 || count == 3 { cell } else { DEAD }
|
||||||
for col in 0..self.width {
|
} else if count == 3 {
|
||||||
let idx = self.index(row, col);
|
// Only collect hues for births
|
||||||
let cell = self.cells[idx];
|
self.hues.clear();
|
||||||
let neighbor_count = self.count_neighbors_and_get_hues(row, col);
|
for &v in &ns {
|
||||||
|
if v != DEAD {
|
||||||
self.next_cells[idx] = match (cell, neighbor_count) {
|
self.hues.push(v);
|
||||||
(Cell::Alive { .. }, x) if x < 2 => Cell::Dead,
|
|
||||||
(Cell::Alive { hue }, 2) | (Cell::Alive { hue }, 3) => Cell::Alive { hue },
|
|
||||||
(Cell::Alive { .. }, x) if x > 3 => Cell::Dead,
|
|
||||||
(Cell::Dead, 3) => {
|
|
||||||
let mixed_hue = mix_colors(&self.neighbor_hues_buffer);
|
|
||||||
Cell::Alive { hue: mixed_hue }
|
|
||||||
}
|
}
|
||||||
(otherwise, _) => otherwise,
|
}
|
||||||
|
luts.mix(&self.hues)
|
||||||
|
} else {
|
||||||
|
DEAD
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
std::mem::swap(&mut self.cells, &mut self.next_cells);
|
std::mem::swap(&mut self.cells, &mut self.buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_alive_block(&mut self, center_row: i32, center_col: i32, half: i32, hue: u8) {
|
fn stamp(&mut self, cr: i32, cc: i32, half: i32, hue: u8) {
|
||||||
let h = self.height as i32;
|
let (h, w) = (self.h as i32, self.w as i32);
|
||||||
let w = self.width as i32;
|
let hue = safe_hue(hue);
|
||||||
for dr in -half..=half {
|
for dr in -half..=half {
|
||||||
for dc in -half..=half {
|
for dc in -half..=half {
|
||||||
let r = (center_row + dr).rem_euclid(h) as u32;
|
let r = (cr + dr).rem_euclid(h) as u32;
|
||||||
let c = (center_col + dc).rem_euclid(w) as u32;
|
let c = (cc + dc).rem_euclid(w) as u32;
|
||||||
let idx = self.index(r, c);
|
self.cells[(r * self.w + c) as usize] = hue;
|
||||||
self.cells[idx] = Cell::Alive { hue };
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct AppState {
|
// ── App state ──────────────────────────────────────────────────────
|
||||||
window: Window,
|
|
||||||
|
struct App {
|
||||||
|
win: Window,
|
||||||
canvas: HtmlCanvasElement,
|
canvas: HtmlCanvasElement,
|
||||||
ctx: CanvasRenderingContext2d,
|
ctx: CanvasRenderingContext2d,
|
||||||
universe: Universe,
|
luts: Luts,
|
||||||
cursor_row: i32,
|
grid: Grid,
|
||||||
cursor_col: i32,
|
pixels: Vec<u8>,
|
||||||
last_frame_time: f64,
|
cw: u32,
|
||||||
cursor_active: bool,
|
ch: u32,
|
||||||
// Reusable pixel buffer for ImageData (avoids allocation each frame)
|
crow: i32,
|
||||||
pixel_buffer: Vec<u8>,
|
ccol: i32,
|
||||||
canvas_width: u32,
|
cursor_on: bool,
|
||||||
canvas_height: u32,
|
last_tick: f64,
|
||||||
|
dirty: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppState {
|
impl App {
|
||||||
fn new() -> Result<Self, JsValue> {
|
fn new() -> Result<Self, JsValue> {
|
||||||
utils::set_panic_hook();
|
utils::set_panic_hook();
|
||||||
|
let win = web_sys::window().ok_or("no window")?;
|
||||||
let window = web_sys::window().ok_or_else(|| JsValue::from_str("no window"))?;
|
let doc = win.document().ok_or("no document")?;
|
||||||
let document = window
|
let canvas: HtmlCanvasElement = doc
|
||||||
.document()
|
|
||||||
.ok_or_else(|| JsValue::from_str("no document"))?;
|
|
||||||
|
|
||||||
let canvas = document
|
|
||||||
.get_element_by_id("canvas")
|
.get_element_by_id("canvas")
|
||||||
.and_then(|e| e.dyn_into::<HtmlCanvasElement>().ok())
|
.ok_or("no canvas")?
|
||||||
.ok_or_else(|| JsValue::from_str("canvas element not found"))?;
|
.dyn_into()?;
|
||||||
|
let ctx: CanvasRenderingContext2d = canvas.get_context("2d")?.ok_or("no 2d ctx")?.dyn_into()?;
|
||||||
let ctx = canvas
|
|
||||||
.get_context("2d")?
|
|
||||||
.ok_or_else(|| JsValue::from_str("no 2d context"))?
|
|
||||||
.dyn_into::<CanvasRenderingContext2d>()?;
|
|
||||||
|
|
||||||
// Disable image smoothing for sharp pixel scaling
|
|
||||||
ctx.set_image_smoothing_enabled(false);
|
ctx.set_image_smoothing_enabled(false);
|
||||||
|
|
||||||
let mut state = Self {
|
let mut app = Self {
|
||||||
window,
|
win,
|
||||||
canvas,
|
canvas,
|
||||||
ctx,
|
ctx,
|
||||||
universe: Universe::new(1, 1),
|
luts: Luts::new(),
|
||||||
cursor_row: 0,
|
grid: Grid::new(1, 1),
|
||||||
cursor_col: 0,
|
pixels: Vec::new(),
|
||||||
last_frame_time: 0.0,
|
cw: 0,
|
||||||
cursor_active: false,
|
ch: 0,
|
||||||
pixel_buffer: Vec::new(),
|
crow: 0,
|
||||||
canvas_width: 0,
|
ccol: 0,
|
||||||
canvas_height: 0,
|
cursor_on: false,
|
||||||
|
last_tick: 0.0,
|
||||||
|
dirty: true,
|
||||||
};
|
};
|
||||||
|
app.resize();
|
||||||
state.resize_canvas_and_universe();
|
Ok(app)
|
||||||
Ok(state)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_current_time(&self) -> f64 {
|
fn now(&self) -> f64 {
|
||||||
self.window.performance().unwrap().now()
|
self.win.performance().unwrap().now()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resize_canvas_and_universe(&mut self) {
|
fn resize(&mut self) {
|
||||||
let width = self.window.inner_width().unwrap().as_f64().unwrap();
|
let vw = self.win.inner_width().unwrap().as_f64().unwrap();
|
||||||
let height = self.window.inner_height().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);
|
||||||
|
|
||||||
let dpr = self.window.device_pixel_ratio();
|
self.cw = cols;
|
||||||
let css_w = width;
|
self.ch = rows;
|
||||||
let css_h = height;
|
self.canvas.set_width(cols);
|
||||||
let element = self.canvas.dyn_ref::<web_sys::Element>().unwrap();
|
self.canvas.set_height(rows);
|
||||||
element
|
self.canvas
|
||||||
|
.dyn_ref::<web_sys::Element>()
|
||||||
|
.unwrap()
|
||||||
.set_attribute(
|
.set_attribute(
|
||||||
"style",
|
"style",
|
||||||
&format!(
|
&format!(
|
||||||
"position:fixed;inset:0;width:{}px;height:{}px;image-rendering:pixelated",
|
"position:fixed;inset:0;width:{vw}px;height:{vh}px;image-rendering:pixelated"
|
||||||
css_w, css_h
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.ok();
|
.ok();
|
||||||
|
|
||||||
self.canvas_width = (css_w * dpr) as u32;
|
|
||||||
self.canvas_height = (css_h * dpr) as u32;
|
|
||||||
self.canvas.set_width(self.canvas_width);
|
|
||||||
self.canvas.set_height(self.canvas_height);
|
|
||||||
|
|
||||||
// Disable image smoothing after resize
|
|
||||||
self.ctx.set_image_smoothing_enabled(false);
|
self.ctx.set_image_smoothing_enabled(false);
|
||||||
|
|
||||||
// Clear canvas
|
self.grid = Grid::new(cols, rows);
|
||||||
self.ctx.set_fill_style_str("black");
|
self.pixels = vec![0u8; (cols * rows * 4) as usize];
|
||||||
self.ctx.fill_rect(0.0, 0.0, self.canvas_width as f64, self.canvas_height as f64);
|
self.dirty = true;
|
||||||
|
|
||||||
let cols = (self.canvas_width / CELL_SIZE).max(1);
|
|
||||||
let rows = (self.canvas_height / CELL_SIZE).max(1);
|
|
||||||
self.universe = Universe::new(cols, rows);
|
|
||||||
|
|
||||||
// Allocate pixel buffer for the universe size (1 pixel per cell)
|
|
||||||
// We'll draw at universe resolution and let CSS scale it up
|
|
||||||
let buffer_size = (cols * rows * 4) as usize;
|
|
||||||
self.pixel_buffer = vec![0u8; buffer_size];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_scaled(&mut self) {
|
fn draw(&mut self) {
|
||||||
let grid_width = self.universe.width;
|
if !self.dirty {
|
||||||
let grid_height = self.universe.height;
|
return;
|
||||||
let cell_w = CELL_SIZE;
|
}
|
||||||
let cell_h = CELL_SIZE;
|
self.dirty = false;
|
||||||
|
|
||||||
// Fill pixel buffer at full canvas resolution
|
let cells = &self.grid.cells;
|
||||||
let canvas_w = self.canvas_width as usize;
|
let rgb = &self.luts.rgb;
|
||||||
let canvas_h = self.canvas_height as usize;
|
let px = &mut self.pixels;
|
||||||
|
|
||||||
// Resize buffer if needed
|
for (i, &cell) in cells.iter().enumerate() {
|
||||||
let needed_size = canvas_w * canvas_h * 4;
|
px[i * 4..i * 4 + 4].copy_from_slice(&rgb[cell as usize]);
|
||||||
if self.pixel_buffer.len() != needed_size {
|
|
||||||
self.pixel_buffer.resize(needed_size, 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fill with black first (for dead cells)
|
if let Ok(img) =
|
||||||
for chunk in self.pixel_buffer.chunks_exact_mut(4) {
|
ImageData::new_with_u8_clamped_array_and_sh(Clamped(px), self.cw, self.ch)
|
||||||
chunk[0] = 0;
|
{
|
||||||
chunk[1] = 0;
|
self.ctx.put_image_data(&img, 0.0, 0.0).ok();
|
||||||
chunk[2] = 0;
|
|
||||||
chunk[3] = 255;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw each cell as a CELL_SIZE × CELL_SIZE block
|
|
||||||
for row in 0..grid_height {
|
|
||||||
for col in 0..grid_width {
|
|
||||||
let cell_idx = self.universe.index(row, col);
|
|
||||||
|
|
||||||
if let Cell::Alive { hue } = self.universe.cells[cell_idx] {
|
|
||||||
let (r, g, b) = hue_to_rgb(hue);
|
|
||||||
|
|
||||||
let start_x = (col * cell_w) as usize;
|
|
||||||
let start_y = (row * cell_h) as usize;
|
|
||||||
|
|
||||||
for py in 0..cell_h as usize {
|
|
||||||
let y = start_y + py;
|
|
||||||
if y >= canvas_h {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
for px in 0..cell_w as usize {
|
|
||||||
let x = start_x + px;
|
|
||||||
if x >= canvas_w {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
let pixel_idx = (y * canvas_w + x) * 4;
|
|
||||||
self.pixel_buffer[pixel_idx] = r;
|
|
||||||
self.pixel_buffer[pixel_idx + 1] = g;
|
|
||||||
self.pixel_buffer[pixel_idx + 2] = b;
|
|
||||||
self.pixel_buffer[pixel_idx + 3] = 255;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Single putImageData call with full canvas
|
|
||||||
if let Ok(image_data) = ImageData::new_with_u8_clamped_array_and_sh(
|
|
||||||
Clamped(&self.pixel_buffer),
|
|
||||||
self.canvas_width,
|
|
||||||
self.canvas_height,
|
|
||||||
) {
|
|
||||||
self.ctx.put_image_data(&image_data, 0.0, 0.0).ok();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Entry point ────────────────────────────────────────────────────
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub fn start() -> Result<(), JsValue> {
|
pub fn start() -> Result<(), JsValue> {
|
||||||
let state = AppState::new()?;
|
let app = App::new()?;
|
||||||
let state_rc = Rc::new(RefCell::new(state));
|
let rc = Rc::new(RefCell::new(app));
|
||||||
|
|
||||||
let canvas = state_rc.borrow().canvas.clone();
|
let win = rc.borrow().win.clone();
|
||||||
let window = state_rc.borrow().window.clone();
|
let doc = win.document().unwrap();
|
||||||
let document = window.document().unwrap();
|
|
||||||
|
|
||||||
// Mouse move handler
|
// Mouse
|
||||||
let state_for_mouse = state_rc.clone();
|
let s = rc.clone();
|
||||||
let canvas_for_mouse = canvas.clone();
|
let cb = Closure::wrap(Box::new(move |e: web_sys::MouseEvent| {
|
||||||
let window_for_mouse = window.clone();
|
let mut a = s.borrow_mut();
|
||||||
let mouse_closure = Closure::wrap(Box::new(move |event: web_sys::MouseEvent| {
|
a.ccol = e.client_x() / CELL_SIZE as i32;
|
||||||
let rect = canvas_for_mouse.get_bounding_client_rect();
|
a.crow = e.client_y() / CELL_SIZE as i32;
|
||||||
let dpr = window_for_mouse.device_pixel_ratio();
|
a.cursor_on = true;
|
||||||
let x = (event.client_x() as f64 - rect.left()) * dpr;
|
|
||||||
let y = (event.client_y() as f64 - rect.top()) * dpr;
|
|
||||||
let mut s = state_for_mouse.borrow_mut();
|
|
||||||
s.cursor_col = (x / CELL_SIZE as f64) as i32;
|
|
||||||
s.cursor_row = (y / CELL_SIZE as f64) as i32;
|
|
||||||
s.cursor_active = true;
|
|
||||||
}) as Box<dyn FnMut(_)>);
|
}) as Box<dyn FnMut(_)>);
|
||||||
|
doc.add_event_listener_with_callback("mousemove", cb.as_ref().unchecked_ref())?;
|
||||||
|
cb.forget();
|
||||||
|
|
||||||
document.add_event_listener_with_callback("mousemove", mouse_closure.as_ref().unchecked_ref())?;
|
// Touch move
|
||||||
mouse_closure.forget();
|
let s = rc.clone();
|
||||||
|
let cb = Closure::wrap(Box::new(move |e: web_sys::TouchEvent| {
|
||||||
// Touch move handler
|
e.prevent_default();
|
||||||
let state_for_touch_move = state_rc.clone();
|
if let Some(t) = e.touches().get(0) {
|
||||||
let canvas_for_touch_move = canvas.clone();
|
let mut a = s.borrow_mut();
|
||||||
let window_for_touch_move = window.clone();
|
a.ccol = t.client_x() / CELL_SIZE as i32;
|
||||||
let touch_move_closure = Closure::wrap(Box::new(move |event: web_sys::TouchEvent| {
|
a.crow = t.client_y() / CELL_SIZE as i32;
|
||||||
event.prevent_default();
|
a.cursor_on = true;
|
||||||
if let Some(touch) = event.touches().get(0) {
|
|
||||||
let rect = canvas_for_touch_move.get_bounding_client_rect();
|
|
||||||
let dpr = window_for_touch_move.device_pixel_ratio();
|
|
||||||
let x = (touch.client_x() as f64 - rect.left()) * dpr;
|
|
||||||
let y = (touch.client_y() as f64 - rect.top()) * dpr;
|
|
||||||
let mut s = state_for_touch_move.borrow_mut();
|
|
||||||
s.cursor_col = (x / CELL_SIZE as f64) as i32;
|
|
||||||
s.cursor_row = (y / CELL_SIZE as f64) as i32;
|
|
||||||
s.cursor_active = true;
|
|
||||||
}
|
}
|
||||||
}) as Box<dyn FnMut(_)>);
|
}) as Box<dyn FnMut(_)>);
|
||||||
|
doc.add_event_listener_with_callback("touchmove", cb.as_ref().unchecked_ref())?;
|
||||||
|
cb.forget();
|
||||||
|
|
||||||
document
|
// Touch start
|
||||||
.add_event_listener_with_callback("touchmove", touch_move_closure.as_ref().unchecked_ref())?;
|
let s = rc.clone();
|
||||||
touch_move_closure.forget();
|
let cb = Closure::wrap(Box::new(move |e: web_sys::TouchEvent| {
|
||||||
|
e.prevent_default();
|
||||||
// Touch start handler
|
if let Some(t) = e.touches().get(0) {
|
||||||
let state_for_touch_start = state_rc.clone();
|
let mut a = s.borrow_mut();
|
||||||
let canvas_for_touch_start = canvas.clone();
|
a.ccol = t.client_x() / CELL_SIZE as i32;
|
||||||
let window_for_touch_start = window.clone();
|
a.crow = t.client_y() / CELL_SIZE as i32;
|
||||||
let touch_start_closure = Closure::wrap(Box::new(move |event: web_sys::TouchEvent| {
|
a.cursor_on = true;
|
||||||
event.prevent_default();
|
|
||||||
if let Some(touch) = event.touches().get(0) {
|
|
||||||
let rect = canvas_for_touch_start.get_bounding_client_rect();
|
|
||||||
let dpr = window_for_touch_start.device_pixel_ratio();
|
|
||||||
let x = (touch.client_x() as f64 - rect.left()) * dpr;
|
|
||||||
let y = (touch.client_y() as f64 - rect.top()) * dpr;
|
|
||||||
let mut s = state_for_touch_start.borrow_mut();
|
|
||||||
s.cursor_col = (x / CELL_SIZE as f64) as i32;
|
|
||||||
s.cursor_row = (y / CELL_SIZE as f64) as i32;
|
|
||||||
s.cursor_active = true;
|
|
||||||
}
|
}
|
||||||
}) as Box<dyn FnMut(_)>);
|
}) as Box<dyn FnMut(_)>);
|
||||||
|
doc.add_event_listener_with_callback("touchstart", cb.as_ref().unchecked_ref())?;
|
||||||
|
cb.forget();
|
||||||
|
|
||||||
document
|
// Touch end
|
||||||
.add_event_listener_with_callback("touchstart", touch_start_closure.as_ref().unchecked_ref())?;
|
let s = rc.clone();
|
||||||
touch_start_closure.forget();
|
let cb = Closure::wrap(Box::new(move |_: web_sys::TouchEvent| {
|
||||||
|
s.borrow_mut().cursor_on = false;
|
||||||
// Touch end handler
|
|
||||||
let state_for_touch_end = state_rc.clone();
|
|
||||||
let touch_end_closure = Closure::wrap(Box::new(move |_event: web_sys::TouchEvent| {
|
|
||||||
let mut s = state_for_touch_end.borrow_mut();
|
|
||||||
s.cursor_active = false;
|
|
||||||
}) as Box<dyn FnMut(_)>);
|
}) as Box<dyn FnMut(_)>);
|
||||||
|
doc.add_event_listener_with_callback("touchend", cb.as_ref().unchecked_ref())?;
|
||||||
|
cb.forget();
|
||||||
|
|
||||||
document
|
// Resize
|
||||||
.add_event_listener_with_callback("touchend", touch_end_closure.as_ref().unchecked_ref())?;
|
let s = rc.clone();
|
||||||
touch_end_closure.forget();
|
let cb = Closure::wrap(Box::new(move || {
|
||||||
|
s.borrow_mut().resize();
|
||||||
// Resize handler
|
|
||||||
let state_for_resize = state_rc.clone();
|
|
||||||
let resize_closure = Closure::wrap(Box::new(move || {
|
|
||||||
let mut s = state_for_resize.borrow_mut();
|
|
||||||
s.resize_canvas_and_universe();
|
|
||||||
}) as Box<dyn FnMut()>);
|
}) as Box<dyn FnMut()>);
|
||||||
|
win.add_event_listener_with_callback("resize", cb.as_ref().unchecked_ref())?;
|
||||||
window.add_event_listener_with_callback("resize", resize_closure.as_ref().unchecked_ref())?;
|
cb.forget();
|
||||||
resize_closure.forget();
|
|
||||||
|
|
||||||
// Animation loop
|
// Animation loop
|
||||||
let f = Rc::new(RefCell::new(None::<Closure<dyn FnMut()>>));
|
let f: Rc<RefCell<Option<Closure<dyn FnMut()>>>> = Rc::new(RefCell::new(None));
|
||||||
let g = f.clone();
|
let g = f.clone();
|
||||||
let state_for_anim = state_rc.clone();
|
let s = rc.clone();
|
||||||
let window_for_anim = window.clone();
|
let w = win.clone();
|
||||||
|
|
||||||
*g.borrow_mut() = Some(Closure::wrap(Box::new(move || {
|
*g.borrow_mut() = Some(Closure::wrap(Box::new(move || {
|
||||||
let current_time = state_for_anim.borrow().get_current_time();
|
let now = s.borrow().now();
|
||||||
|
|
||||||
{
|
{
|
||||||
let mut s = state_for_anim.borrow_mut();
|
let mut a = s.borrow_mut();
|
||||||
|
|
||||||
// Run simulation FIRST (throttled to 60 FPS max)
|
if now - a.last_tick >= TICK_MS {
|
||||||
// This way cursor-placed cells won't be immediately killed
|
a.last_tick = now;
|
||||||
if current_time - s.last_frame_time >= SIMULATION_FRAME_MS {
|
let luts = &a.luts as *const Luts;
|
||||||
s.last_frame_time = current_time;
|
// SAFETY: luts is not mutated during tick()
|
||||||
s.universe.tick();
|
a.grid.tick(unsafe { &*luts });
|
||||||
|
a.dirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process cursor input AFTER tick for responsiveness
|
if a.cursor_on {
|
||||||
// Cells placed here survive until the next tick
|
let (cr, cc) = (a.crow, a.ccol);
|
||||||
if s.cursor_active {
|
let hue = ((now % HUE_PERIOD_MS) / HUE_PERIOD_MS * 256.0) as u8;
|
||||||
let cursor_row = s.cursor_row;
|
a.grid.stamp(cr, cc, 2, hue);
|
||||||
let cursor_col = s.cursor_col;
|
a.dirty = true;
|
||||||
|
|
||||||
let hue = ((current_time % HUE_ROTATION_PERIOD_MS) / HUE_ROTATION_PERIOD_MS * 256.0) as u8;
|
|
||||||
s.universe.set_alive_block(cursor_row, cursor_col, 2, hue);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw every frame
|
a.draw();
|
||||||
s.draw_scaled();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
window_for_anim
|
w.request_animation_frame(f.borrow().as_ref().unwrap().as_ref().unchecked_ref())
|
||||||
.request_animation_frame(f.borrow().as_ref().unwrap().as_ref().unchecked_ref())
|
|
||||||
.ok();
|
.ok();
|
||||||
}) as Box<dyn FnMut()>));
|
}) as Box<dyn FnMut()>));
|
||||||
|
|
||||||
window.request_animation_frame(g.borrow().as_ref().unwrap().as_ref().unchecked_ref())?;
|
win.request_animation_frame(g.borrow().as_ref().unwrap().as_ref().unchecked_ref())?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,8 @@
|
||||||
import { FlatCompat } from "@eslint/eslintrc";
|
|
||||||
import tseslint from 'typescript-eslint';
|
import tseslint from 'typescript-eslint';
|
||||||
|
|
||||||
const compat = new FlatCompat({
|
|
||||||
baseDirectory: import.meta.dirname,
|
|
||||||
});
|
|
||||||
|
|
||||||
export default tseslint.config(
|
export default tseslint.config(
|
||||||
{
|
{
|
||||||
ignores: ['.next', 'cgol/pkg/**/*', 'next-env.d.ts']
|
ignores: ['dist', 'cgol/pkg/**/*']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
files: ['**/*.ts', '**/*.tsx'],
|
files: ['**/*.ts', '**/*.tsx'],
|
||||||
|
|
|
||||||
12
flake.lock
generated
|
|
@ -20,11 +20,11 @@
|
||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1771369470,
|
"lastModified": 1772542754,
|
||||||
"narHash": "sha256-0NBlEBKkN3lufyvFegY4TYv5mCNHbi5OmBDrzihbBMQ=",
|
"narHash": "sha256-WGV2hy+VIeQsYXpsLjdr4GvHv5eECMISX1zKLTedhdg=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "0182a361324364ae3f436a63005877674cf45efb",
|
"rev": "8c809a146a140c5c8806f13399592dbcb1bb5dc4",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
@ -62,11 +62,11 @@
|
||||||
"nixpkgs": "nixpkgs_2"
|
"nixpkgs": "nixpkgs_2"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1771384185,
|
"lastModified": 1772679930,
|
||||||
"narHash": "sha256-KvmjUeA7uODwzbcQoN/B8DCZIbhT/Q/uErF1BBMcYnw=",
|
"narHash": "sha256-FxYmdacqrdDVeE9QqZKTIpNLjv2B8GSKssgwlZuTR98=",
|
||||||
"owner": "oxalica",
|
"owner": "oxalica",
|
||||||
"repo": "rust-overlay",
|
"repo": "rust-overlay",
|
||||||
"rev": "23dd7fa91602a68bd04847ac41bc10af1e6e2fd2",
|
"rev": "9b741db17141331fdb26270a1b66b81be8be9edd",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
|
||||||
75
flake.nix
|
|
@ -1,63 +1,44 @@
|
||||||
{
|
{
|
||||||
description = "CTF Jet development environment (Bun)";
|
description = "Jet Pham's personal website";
|
||||||
|
|
||||||
inputs = {
|
inputs = {
|
||||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
rust-overlay.url = "github:oxalica/rust-overlay";
|
rust-overlay.url = "github:oxalica/rust-overlay";
|
||||||
flake-utils.url = "github:numtide/flake-utils";
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs = { self, nixpkgs, rust-overlay, flake-utils }:
|
outputs = { self, nixpkgs, rust-overlay, flake-utils }:
|
||||||
flake-utils.lib.eachDefaultSystem (system:
|
(flake-utils.lib.eachDefaultSystem (system:
|
||||||
let
|
let
|
||||||
overlays = [ (import rust-overlay) ];
|
overlays = [ (import rust-overlay) ];
|
||||||
pkgs = import nixpkgs {
|
pkgs = import nixpkgs { inherit system overlays; };
|
||||||
inherit system overlays;
|
rustToolchain = pkgs.rust-bin.selectLatestNightlyWith (toolchain:
|
||||||
};
|
toolchain.default.override {
|
||||||
|
|
||||||
bun = pkgs.bun;
|
|
||||||
|
|
||||||
# Prisma engines for NixOS
|
|
||||||
prismaEngines = pkgs.prisma-engines;
|
|
||||||
|
|
||||||
devTools = with pkgs; [
|
|
||||||
git
|
|
||||||
postgresql
|
|
||||||
curl
|
|
||||||
wget
|
|
||||||
typescript-language-server
|
|
||||||
pkg-config
|
|
||||||
wasm-pack
|
|
||||||
binaryen
|
|
||||||
(rust-bin.selectLatestNightlyWith (toolchain: toolchain.default.override {
|
|
||||||
extensions = [ "rust-src" ];
|
extensions = [ "rust-src" ];
|
||||||
targets = [ "wasm32-unknown-unknown" ];
|
targets = [ "wasm32-unknown-unknown" ];
|
||||||
}))
|
});
|
||||||
];
|
website = pkgs.stdenv.mkDerivation {
|
||||||
|
pname = "jet-website";
|
||||||
|
version = "0.1.0";
|
||||||
|
src = pkgs.lib.cleanSource ./.;
|
||||||
|
nativeBuildInputs = [ pkgs.nodejs rustToolchain pkgs.wasm-pack pkgs.binaryen pkgs.pkg-config ];
|
||||||
|
buildPhase = ''
|
||||||
|
export HOME=$TMPDIR
|
||||||
|
cd cgol && wasm-pack build --release --target web && cd ..
|
||||||
|
npm ci
|
||||||
|
npm run build
|
||||||
|
'';
|
||||||
|
installPhase = ''
|
||||||
|
mkdir -p $out
|
||||||
|
cp -r dist/* $out/
|
||||||
|
'';
|
||||||
|
};
|
||||||
in {
|
in {
|
||||||
|
packages = { default = website; };
|
||||||
devShells.default = pkgs.mkShell {
|
devShells.default = pkgs.mkShell {
|
||||||
buildInputs = [
|
buildInputs = with pkgs; [
|
||||||
bun
|
nodejs git curl typescript-language-server
|
||||||
prismaEngines
|
pkg-config wasm-pack binaryen rustToolchain
|
||||||
] ++ devTools;
|
];
|
||||||
|
|
||||||
NIXPKGS_ALLOW_UNFREE = "1";
|
|
||||||
|
|
||||||
PRISMA_QUERY_ENGINE_BINARY = "${prismaEngines}/bin/query-engine";
|
|
||||||
PRISMA_SCHEMA_ENGINE_BINARY = "${prismaEngines}/bin/schema-engine";
|
|
||||||
PRISMA_INTROSPECTION_ENGINE_BINARY = "${prismaEngines}/bin/introspection-engine";
|
|
||||||
PRISMA_ENGINES_CHECKSUM_IGNORE_MISSING = "1";
|
|
||||||
};
|
|
||||||
|
|
||||||
packages = {
|
|
||||||
inherit bun prismaEngines;
|
|
||||||
|
|
||||||
default = pkgs.symlinkJoin {
|
|
||||||
name = "ctfjet-dev-bun";
|
|
||||||
paths = [ bun prismaEngines ];
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
18
index.html
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<meta name="theme-color" content="#000000" />
|
||||||
|
<meta name="apple-mobile-web-app-title" content="Jet Pham" />
|
||||||
|
<title>Jet Pham - Software Extremist</title>
|
||||||
|
<meta name="description" content="Jet Pham's personal website" />
|
||||||
|
<link rel="manifest" href="/manifest.json" />
|
||||||
|
<link rel="icon" href="/favicon.ico" />
|
||||||
|
<link rel="apple-touch-icon" href="/apple-icon.png" />
|
||||||
|
</head>
|
||||||
|
<body style="background:#000">
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
37
install.sh
|
|
@ -1,37 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# fix home issues
|
|
||||||
export HOME=/root
|
|
||||||
|
|
||||||
# Install Rustup
|
|
||||||
if ! command -v rustup
|
|
||||||
then
|
|
||||||
echo "Installing Rustup..."
|
|
||||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y -t wasm32-unknown-unknown --profile minimal
|
|
||||||
source "$HOME/.cargo/env"
|
|
||||||
else
|
|
||||||
echo "Rustup already installed."
|
|
||||||
rustup target add wasm32-unknown-unknown
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Install wasm-pack
|
|
||||||
if ! command -v wasm-pack
|
|
||||||
then
|
|
||||||
echo "Installing wasm-pack..."
|
|
||||||
curl https://drager.github.io/wasm-pack/installer/init.sh -sSf | sh
|
|
||||||
echo "wasm-pack installation complete."
|
|
||||||
else
|
|
||||||
echo "wasm-pack already installed."
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Build cgol WASM package
|
|
||||||
echo "Building cgol WASM package..."
|
|
||||||
cd cgol
|
|
||||||
wasm-pack build --release --target web
|
|
||||||
cd ..
|
|
||||||
|
|
||||||
# Install Next.js dependencies with bun
|
|
||||||
echo "Installing Next.js dependencies with bun..."
|
|
||||||
bun install
|
|
||||||
|
|
||||||
100
next.config.js
|
|
@ -1,100 +0,0 @@
|
||||||
/**
|
|
||||||
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially useful
|
|
||||||
* for Docker builds.
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
/** @type {import("next").NextConfig} */
|
|
||||||
const config = {
|
|
||||||
webpack: (config, { isServer }) => {
|
|
||||||
config.module.rules.push({
|
|
||||||
test: /\.txt$/,
|
|
||||||
type: "asset/source",
|
|
||||||
});
|
|
||||||
|
|
||||||
config.experiments = {
|
|
||||||
...config.experiments,
|
|
||||||
asyncWebAssembly: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
config.module.rules.push({
|
|
||||||
test: /\.wasm$/,
|
|
||||||
type: "asset/resource",
|
|
||||||
generator: {
|
|
||||||
filename: "static/wasm/[name].[hash][ext]",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Ensure WASM files are properly handled
|
|
||||||
config.resolve.fallback = {
|
|
||||||
...config.resolve.fallback,
|
|
||||||
fs: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Ensure WASM files are properly served
|
|
||||||
config.output.webassemblyModuleFilename = "static/wasm/[modulehash].wasm";
|
|
||||||
|
|
||||||
return config;
|
|
||||||
},
|
|
||||||
turbopack: {
|
|
||||||
rules: {
|
|
||||||
"*.txt": {
|
|
||||||
loaders: ["raw-loader"],
|
|
||||||
as: "*.js",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
productionBrowserSourceMaps: false,
|
|
||||||
// Redirect /_not-found to /
|
|
||||||
async redirects() {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
source: '/_not-found',
|
|
||||||
destination: '/',
|
|
||||||
permanent: false,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
},
|
|
||||||
// Ensure static files are properly served
|
|
||||||
async headers() {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
source: '/:path*',
|
|
||||||
headers: [
|
|
||||||
{
|
|
||||||
key: 'Cross-Origin-Opener-Policy',
|
|
||||||
value: 'same-origin',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'Cross-Origin-Resource-Policy',
|
|
||||||
value: 'cross-origin',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
source: '/_next/static/:path*',
|
|
||||||
headers: [
|
|
||||||
{
|
|
||||||
key: 'Cross-Origin-Resource-Policy',
|
|
||||||
value: 'cross-origin',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
source: '/_next/static/wasm/:path*',
|
|
||||||
headers: [
|
|
||||||
{
|
|
||||||
key: 'Cross-Origin-Resource-Policy',
|
|
||||||
value: 'cross-origin',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'Content-Type',
|
|
||||||
value: 'application/wasm',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default config;
|
|
||||||
3970
package-lock.json
generated
Normal file
44
package.json
|
|
@ -4,52 +4,40 @@
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "next build",
|
"build": "vite build",
|
||||||
"build:wasm": "cd cgol && wasm-pack build --release --target web",
|
"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": "bun run lint && tsc --noEmit",
|
"check": "npm run lint && tsc --noEmit",
|
||||||
"dev": "next dev --turbo",
|
"dev": "vite",
|
||||||
"format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,mdx}\" --cache",
|
"format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,mdx}\" --cache",
|
||||||
"format:write": "prettier --write \"**/*.{ts,tsx,js,jsx,mdx}\" --cache",
|
"format:write": "prettier --write \"**/*.{ts,tsx,js,jsx,mdx}\" --cache",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"lint:fix": "next lint --fix",
|
"lint:fix": "eslint . --fix",
|
||||||
"preview": "next build && next start",
|
"preview": "vite preview",
|
||||||
"start": "next start",
|
|
||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tanstack/react-query": "^5.90.21",
|
|
||||||
"anser": "^2.3.5",
|
"anser": "^2.3.5",
|
||||||
"cgol": "file:./cgol/pkg",
|
"cgol": "file:./cgol/pkg",
|
||||||
"escape-carriage": "^1.3.1",
|
"escape-carriage": "^1.3.1",
|
||||||
"next": "^16.1.6",
|
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
"react-dom": "^19.2.4",
|
"react-dom": "^19.2.4"
|
||||||
"server-only": "^0.0.1",
|
|
||||||
"superjson": "^2.2.6"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3.3.3",
|
"@tailwindcss/vite": "^4.2.1",
|
||||||
"@next/eslint-plugin-next": "^16.1.6",
|
"@types/node": "^25.3.3",
|
||||||
"@tailwindcss/postcss": "^4.2.0",
|
|
||||||
"@types/node": "^25.3.0",
|
|
||||||
"@types/react": "^19.2.14",
|
"@types/react": "^19.2.14",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"eslint": "^9",
|
"@vitejs/plugin-react": "^5.1.4",
|
||||||
"eslint-config-next": "^16.1.6",
|
"eslint": "^10",
|
||||||
"postcss": "^8.5.6",
|
|
||||||
"prettier": "^3.8.1",
|
"prettier": "^3.8.1",
|
||||||
"prettier-plugin-tailwindcss": "^0.7.2",
|
"prettier-plugin-tailwindcss": "^0.7.2",
|
||||||
"raw-loader": "^4.0.2",
|
"tailwindcss": "^4.2.1",
|
||||||
"react-doctor": "^0.0.21",
|
|
||||||
"tailwindcss": "^4.2.0",
|
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"typescript-eslint": "^8.56.0",
|
"typescript-eslint": "^8.56.1",
|
||||||
"webpack": "^5.105.2"
|
"vite": "^7.3.1",
|
||||||
|
"vite-plugin-top-level-await": "^1.6.0",
|
||||||
|
"vite-plugin-wasm": "^3.5.0"
|
||||||
},
|
},
|
||||||
"ct3aMetadata": {
|
|
||||||
"initVersion": "7.40.0"
|
|
||||||
},
|
|
||||||
"overrides": {},
|
|
||||||
"knip": {
|
"knip": {
|
||||||
"ignore": [
|
"ignore": [
|
||||||
"cgol/pkg/**"
|
"cgol/pkg/**"
|
||||||
|
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
export default {
|
|
||||||
plugins: {
|
|
||||||
"@tailwindcss/postcss": {},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 979 KiB After Width: | Height: | Size: 979 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
|
@ -1,17 +1,10 @@
|
||||||
import Link from "next/link";
|
import { BorderedBox } from "~/components/bordered-box";
|
||||||
import Image from "next/image";
|
import { FrostedBox } from "~/components/frosted-box";
|
||||||
import { BorderedBox } from "./_components/bordered-box";
|
import Header from "~/components/header";
|
||||||
import { FrostedBox } from "./_components/frosted-box";
|
import { CgolCanvas } from "~/components/cgol-canvas";
|
||||||
import Header from "./_components/header";
|
import Jet from "~/assets/Jet.txt?raw";
|
||||||
import { CgolCanvas } from "./_components/cgol-canvas";
|
|
||||||
import FirstName from "~/assets/Jet.txt";
|
|
||||||
|
|
||||||
export const metadata = {
|
export default function App() {
|
||||||
title: "Jet Pham - Software Extremist",
|
|
||||||
description: "Personal website of Jet Pham, a software extremist.",
|
|
||||||
};
|
|
||||||
|
|
||||||
export default async function Home() {
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<CgolCanvas />
|
<CgolCanvas />
|
||||||
|
|
@ -20,56 +13,57 @@ export default async function Home() {
|
||||||
<FrostedBox className="my-[2ch] w-full max-w-[66.666667%] min-w-fit md:mt-[4ch]">
|
<FrostedBox className="my-[2ch] w-full max-w-[66.666667%] min-w-fit md:mt-[4ch]">
|
||||||
<div className="flex flex-col items-center justify-center gap-[2ch] md:flex-row">
|
<div className="flex flex-col items-center justify-center gap-[2ch] md:flex-row">
|
||||||
<div className="order-1 flex flex-col items-center md:order-2">
|
<div className="order-1 flex flex-col items-center md:order-2">
|
||||||
<Header content={FirstName} />
|
<Header content={Jet} />
|
||||||
<div className="mt-[2ch]">Software Extremist</div>
|
<div className="mt-[2ch]">Software Extremist</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="order-2 shrink-0 md:order-1">
|
<div className="order-2 shrink-0 md:order-1">
|
||||||
<Image
|
<img
|
||||||
src="/jet.svg"
|
src="/jet.svg"
|
||||||
alt="Jet"
|
alt="Jet"
|
||||||
width={250}
|
width={250}
|
||||||
height={250}
|
height={250}
|
||||||
className="aspect-square w-full max-w-[250px] object-cover md:h-[263px] md:w-[175px] md:max-w-none"
|
className="aspect-square w-full max-w-[250px] object-cover md:h-[263px] md:w-[175px] md:max-w-none"
|
||||||
priority
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<BorderedBox label="Skills" className="mt-[2ch]">
|
<BorderedBox label="Contact" className="mt-[2ch]">
|
||||||
<div>Making crazy stuff</div>
|
<a href="mailto:jet@extremist.software">
|
||||||
|
jet@extremist.software
|
||||||
|
</a>
|
||||||
</BorderedBox>
|
</BorderedBox>
|
||||||
<BorderedBox label="Links">
|
<BorderedBox label="Links">
|
||||||
<ol>
|
<ol>
|
||||||
<li>
|
<li>
|
||||||
<Link
|
<a
|
||||||
|
href="https://git.extremist.software"
|
||||||
|
className="inline-flex items-center"
|
||||||
|
>
|
||||||
|
Forgejo
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
href="https://github.com/jetpham"
|
href="https://github.com/jetpham"
|
||||||
className="inline-flex items-center"
|
className="inline-flex items-center"
|
||||||
>
|
>
|
||||||
GitHub
|
GitHub
|
||||||
</Link>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<Link
|
<a
|
||||||
href="https://linkedin.com/in/jetpham"
|
href="https://x.com/jetpham5"
|
||||||
className="inline-flex items-center"
|
className="inline-flex items-center"
|
||||||
>
|
>
|
||||||
LinkedIn
|
X
|
||||||
</Link>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<Link
|
<a
|
||||||
href="https://bsky.app/profile/jetpham.com"
|
href="https://bsky.app/profile/jetpham.com"
|
||||||
className="inline-flex items-center"
|
className="inline-flex items-center"
|
||||||
>
|
>
|
||||||
Bluesky
|
Bluesky
|
||||||
</Link>
|
</a>
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<Link
|
|
||||||
href="https://x.com/jetpham5"
|
|
||||||
className="inline-flex items-center"
|
|
||||||
>
|
|
||||||
X (Twitter)
|
|
||||||
</Link>
|
|
||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
</BorderedBox>
|
</BorderedBox>
|
||||||
|
|
@ -1,62 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { useEffect, useRef, useCallback } from "react";
|
|
||||||
|
|
||||||
export function CgolCanvas() {
|
|
||||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
||||||
const initializedRef = useRef(false);
|
|
||||||
const cleanupRef = useRef<(() => void) | null>(null);
|
|
||||||
|
|
||||||
const initializeWasm = useCallback(async () => {
|
|
||||||
if (typeof window === "undefined") return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const canvas = canvasRef.current;
|
|
||||||
if (!canvas || initializedRef.current) return;
|
|
||||||
|
|
||||||
const cgolModule = await import("cgol");
|
|
||||||
|
|
||||||
// Initialize WASM module
|
|
||||||
const initFunction = cgolModule.default;
|
|
||||||
if (initFunction && typeof initFunction === "function") {
|
|
||||||
await initFunction();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start CGOL
|
|
||||||
if (typeof cgolModule.start === "function") {
|
|
||||||
cgolModule.start();
|
|
||||||
initializedRef.current = true;
|
|
||||||
|
|
||||||
const cleanupFn = (cgolModule as { cleanup?: () => void }).cleanup;
|
|
||||||
if (typeof cleanupFn === "function") {
|
|
||||||
cleanupRef.current = cleanupFn;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error: unknown) {
|
|
||||||
console.error("Failed to initialize CGOL WebAssembly module:", error);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Initialize immediately without delay
|
|
||||||
void initializeWasm();
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
// Call cleanup if available from WASM module
|
|
||||||
if (cleanupRef.current) {
|
|
||||||
cleanupRef.current();
|
|
||||||
}
|
|
||||||
// Reset initialization state on unmount
|
|
||||||
initializedRef.current = false;
|
|
||||||
};
|
|
||||||
}, [initializeWasm]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<canvas
|
|
||||||
ref={canvasRef}
|
|
||||||
id="canvas"
|
|
||||||
className="fixed top-0 left-0 -z-10 h-screen w-screen"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
import "~/styles/globals.css";
|
|
||||||
|
|
||||||
import { type Metadata, type Viewport } from "next";
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
|
||||||
title: "Jet Pham",
|
|
||||||
description: "Jet Pham's personal website",
|
|
||||||
appleWebApp: {
|
|
||||||
title: "Jet Pham",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const viewport: Viewport = {
|
|
||||||
width: "device-width",
|
|
||||||
initialScale: 1,
|
|
||||||
themeColor: "#000000",
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function RootLayout({
|
|
||||||
children,
|
|
||||||
}: Readonly<{ children: React.ReactNode }>) {
|
|
||||||
return (
|
|
||||||
<html lang="en">
|
|
||||||
<body>
|
|
||||||
{children}
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
33
src/components/cgol-canvas.tsx
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
|
||||||
|
let wasmStarted = false;
|
||||||
|
|
||||||
|
export function CgolCanvas() {
|
||||||
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (wasmStarted) return;
|
||||||
|
wasmStarted = true;
|
||||||
|
|
||||||
|
import("cgol").then(async (cgolModule) => {
|
||||||
|
if (typeof cgolModule.default === "function") {
|
||||||
|
await cgolModule.default();
|
||||||
|
}
|
||||||
|
if (typeof cgolModule.start === "function") {
|
||||||
|
cgolModule.start();
|
||||||
|
}
|
||||||
|
}).catch((error: unknown) => {
|
||||||
|
console.error("Failed to initialize CGOL WebAssembly module:", error);
|
||||||
|
wasmStarted = false;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<canvas
|
||||||
|
ref={canvasRef}
|
||||||
|
id="canvas"
|
||||||
|
className="fixed top-0 left-0 -z-10 h-screen w-screen"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
7
src/global.d.ts
vendored
|
|
@ -1,10 +1,11 @@
|
||||||
declare module "*.txt" {
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
declare module "*.txt?raw" {
|
||||||
const content: string;
|
const content: string;
|
||||||
export default content;
|
export default content;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module "*.utf8ans" {
|
declare module "*.utf8ans?raw" {
|
||||||
const content: string;
|
const content: string;
|
||||||
export default content;
|
export default content;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
10
src/main.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { StrictMode } from "react";
|
||||||
|
import { createRoot } from "react-dom/client";
|
||||||
|
import "~/styles/globals.css";
|
||||||
|
import App from "./App";
|
||||||
|
|
||||||
|
createRoot(document.getElementById("root")!).render(
|
||||||
|
<StrictMode>
|
||||||
|
<App />
|
||||||
|
</StrictMode>,
|
||||||
|
);
|
||||||
|
|
@ -23,11 +23,6 @@
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"moduleResolution": "Bundler",
|
"moduleResolution": "Bundler",
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
"plugins": [
|
|
||||||
{
|
|
||||||
"name": "next"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
/* Path Aliases */
|
/* Path Aliases */
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
|
|
@ -38,17 +33,15 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"next-env.d.ts",
|
|
||||||
"**/*.ts",
|
"**/*.ts",
|
||||||
"**/*.tsx",
|
"**/*.tsx",
|
||||||
"**/*.cjs",
|
"**/*.cjs",
|
||||||
"**/*.js",
|
"**/*.js",
|
||||||
".next/types/**/*.ts",
|
"vite.config.ts"
|
||||||
".next/dev/types/**/*.ts"
|
|
||||||
],
|
],
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"node_modules",
|
"node_modules",
|
||||||
"generated",
|
"dist",
|
||||||
"cgol/pkg"
|
"cgol/pkg"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
23
vite.config.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { defineConfig } from "vite";
|
||||||
|
import react from "@vitejs/plugin-react";
|
||||||
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
|
import wasm from "vite-plugin-wasm";
|
||||||
|
import topLevelAwait from "vite-plugin-top-level-await";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react(), tailwindcss(), wasm(), topLevelAwait()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
"~": "/src",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
headers: {
|
||||||
|
"Cross-Origin-Opener-Policy": "same-origin",
|
||||||
|
"Cross-Origin-Embedder-Policy": "require-corp",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
target: "esnext",
|
||||||
|
},
|
||||||
|
});
|
||||||