Compare commits

...

7 commits

32 changed files with 4421 additions and 1988 deletions

View file

@ -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
View file

@ -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

1226
bun.lock

File diff suppressed because it is too large Load diff

View file

@ -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"]

View file

@ -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

View file

@ -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; 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
} }
let (sum_sin, sum_cos) = hues #[inline]
.iter() fn mix(&self, hues: &[u8]) -> u8 {
.map(|&hue| (hue as f64 * 360.0 / 255.0).to_radians()) let mut s = 0.0f32;
.fold((0.0, 0.0), |acc, h_rad| { let mut c = 0.0f32;
(acc.0 + h_rad.sin(), acc.1 + h_rad.cos()) for &h in hues {
}); s += self.sin[h as usize];
let avg_hue_degrees = sum_sin.atan2(sum_cos).to_degrees().rem_euclid(360.0); c += self.cos[h as usize];
(avg_hue_degrees * 255.0 / 360.0) as u8 }
safe_hue((s.atan2(c).rem_euclid(std::f32::consts::TAU) * 256.0 / std::f32::consts::TAU) as u8)
}
} }
/// Convert HSL hue (0-255) to RGB // ── Grid ───────────────────────────────────────────────────────────
/// Using full saturation (100%) and lightness (50%)
fn hue_to_rgb(hue: u8) -> (u8, u8, u8) {
let h = hue as f64 / 255.0 * 6.0;
let x = 1.0 - (h % 2.0 - 1.0).abs();
let (r, g, b) = match h as u32 { struct Grid {
0 => (1.0, x, 0.0), w: u32,
1 => (x, 1.0, 0.0), h: u32,
2 => (0.0, 1.0, x), cells: Vec<u8>,
3 => (0.0, x, 1.0), buf: Vec<u8>,
4 => (x, 0.0, 1.0), hues: Vec<u8>,
_ => (1.0, 0.0, x),
};
((r * 255.0) as u8, (g * 255.0) as u8, (b * 255.0) as u8)
} }
#[derive(Clone, Copy, PartialEq)] impl Grid {
enum Cell { fn new(w: u32, h: u32) -> Self {
Dead, let n = (w * h) as usize;
Alive { hue: u8 }, let mut g = Self {
} w,
h,
struct Universe { cells: vec![DEAD; n],
width: u32, buf: vec![DEAD; n],
height: u32, hues: Vec::with_capacity(8),
cells: Vec<Cell>,
next_cells: Vec<Cell>,
neighbor_hues_buffer: Vec<u8>,
}
impl Universe {
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(())
} }

View file

@ -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
View file

@ -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": {

View file

@ -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";
in { version = "0.1.0";
devShells.default = pkgs.mkShell { src = pkgs.lib.cleanSource ./.;
buildInputs = [ nativeBuildInputs = [ pkgs.nodejs rustToolchain pkgs.wasm-pack pkgs.binaryen pkgs.pkg-config ];
bun buildPhase = ''
prismaEngines export HOME=$TMPDIR
] ++ devTools; cd cgol && wasm-pack build --release --target web && cd ..
npm ci
NIXPKGS_ALLOW_UNFREE = "1"; npm run build
'';
PRISMA_QUERY_ENGINE_BINARY = "${prismaEngines}/bin/query-engine"; installPhase = ''
PRISMA_SCHEMA_ENGINE_BINARY = "${prismaEngines}/bin/schema-engine"; mkdir -p $out
PRISMA_INTROSPECTION_ENGINE_BINARY = "${prismaEngines}/bin/introspection-engine"; cp -r dist/* $out/
PRISMA_ENGINES_CHECKSUM_IGNORE_MISSING = "1"; '';
}; };
in {
packages = { packages = { default = website; };
inherit bun prismaEngines; devShells.default = pkgs.mkShell {
buildInputs = with pkgs; [
default = pkgs.symlinkJoin { nodejs git curl typescript-language-server
name = "ctfjet-dev-bun"; pkg-config wasm-pack binaryen rustToolchain
paths = [ bun prismaEngines ]; ];
};
}; };
} }
); ));
} }

18
index.html Normal file
View 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>

View file

@ -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

View file

@ -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

File diff suppressed because it is too large Load diff

View file

@ -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/**"

View file

@ -1,5 +0,0 @@
export default {
plugins: {
"@tailwindcss/postcss": {},
},
};

View file

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 979 KiB

After

Width:  |  Height:  |  Size: 979 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Before After
Before After

View file

@ -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>

View file

@ -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"
/>
);
}

View file

@ -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>
);
}

View 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
View file

@ -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
View 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>,
);

View file

@ -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
View 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",
},
});