From 9ea85db5cef237f13b9302715e4daa68735f9a06 Mon Sep 17 00:00:00 2001 From: djoser Date: Fri, 21 Aug 2026 03:39:42 +0200 Subject: [PATCH] Migrate Vite SPA to Hugo (hugo-coder, dark) with WebGPU background - Hugo static site at root: config, content (home/about/resume/blog + 5 projects), data/resume.yaml, custom layouts (gpu background, resume renderer, projects list), dark theme with canvas behind content - gpu-bg/: extracted WebGPU/WebGL2 spiral engine as a Vite sub-package; slim bootstrap drops router/overlay/text renderer; fixed uniforms (scene0/dim0); self-contained engine.js - archive/: retired Vite SPA preserved (router, overlay, data, engine/text orphaned) - 3-stage Dockerfile (node -> hugo extended/dart-sass -> nginx), static nginx config - DEPLOYMENT.md handover + README rewrite; pre-hugo rollback tag --- .dockerignore | 20 +- .gitignore | 10 + .gitmodules | 3 + .../plans/2025-07-27_120000-webgl-fallback.md | 551 +++++++++ .hermes/plans/2026-08-16_hugo-transition.md | 307 +++++ DEPLOYMENT.md | 89 ++ Dockerfile | 40 +- README.md | 58 +- archive/Dockerfile | 34 + index.html => archive/index.html | 0 .../package-lock.json | 0 package.json => archive/package.json | 0 {src => archive/src}/app.ts | 0 {src => archive/src}/data/pages.ts | 0 .../src}/data/projects/auv-simulation.ts | 0 .../src}/data/projects/evol-engine.ts | 0 .../src}/data/projects/improv-gfx.ts | 0 {src => archive/src}/data/projects/index.ts | 0 .../src}/data/projects/point-cloud.ts | 0 .../src}/data/projects/radiance-caching.ts | 0 {src => archive/src}/data/resume.ts | 0 {src => archive/src}/data/scenes/about.ts | 0 {src => archive/src}/data/scenes/contact.ts | 0 {src => archive/src}/data/scenes/hero.ts | 0 {src => archive/src}/data/scenes/index.ts | 0 {src => archive/src}/data/scenes/projects.ts | 0 {src => archive/src}/data/scenes/resume.ts | 0 {src => archive/src}/engine/gl-renderer.ts | 0 {src => archive/src}/engine/renderer.ts | 0 .../src}/engine/shaders/fullscreen.glsl | 0 .../src}/engine/shaders/fullscreen.wgsl | 0 .../src}/engine/text/slug-generator.ts | 0 .../src}/engine/text/text-layout.ts | 0 .../src}/engine/text/text-renderer.ts | 0 {src => archive/src}/engine/text/text.wgsl | 0 {src => archive/src}/engine/types.ts | 0 {src => archive/src}/main.ts | 0 {src => archive/src}/router.ts | 0 {src => archive/src}/ui/input.ts | 0 {src => archive/src}/ui/overlay.ts | 0 tsconfig.json => archive/tsconfig.json | 0 vite.config.ts => archive/vite.config.ts | 0 assets/css/custom.css | 93 ++ content/_index.md | 7 + content/about.md | 26 + content/posts/_index.md | 5 + content/projects/_index.md | 5 + content/projects/auv-simulation.md | 12 + content/projects/evol-engine.md | 14 + content/projects/improv-gfx.md | 12 + content/projects/point-cloud.md | 14 + content/projects/radiance-caching.md | 14 + content/resume.md | 6 + data/resume.yaml | 82 ++ gpu-bg/index.html | 7 + gpu-bg/package-lock.json | 1028 +++++++++++++++++ gpu-bg/package.json | 17 + gpu-bg/src/gl-renderer.ts | 127 ++ gpu-bg/src/input.ts | 14 + gpu-bg/src/main.ts | 68 ++ gpu-bg/src/renderer.ts | 105 ++ gpu-bg/src/shaders/fullscreen.glsl | 110 ++ gpu-bg/src/shaders/fullscreen.wgsl | 108 ++ gpu-bg/src/types.ts | 8 + gpu-bg/src/vite-env.d.ts | 4 + gpu-bg/tsconfig.json | 17 + gpu-bg/vite.config.ts | 16 + hugo.toml | 75 ++ layouts/home.html | 11 + layouts/partials/gpu-background.html | 2 + layouts/projects/list.html | 37 + layouts/resume/single.html | 68 ++ nginx.default.conf | 20 +- themes/hugo-coder | 1 + 74 files changed, 3170 insertions(+), 75 deletions(-) create mode 100644 .gitmodules create mode 100644 .hermes/plans/2025-07-27_120000-webgl-fallback.md create mode 100644 .hermes/plans/2026-08-16_hugo-transition.md create mode 100644 DEPLOYMENT.md create mode 100644 archive/Dockerfile rename index.html => archive/index.html (100%) rename package-lock.json => archive/package-lock.json (100%) rename package.json => archive/package.json (100%) rename {src => archive/src}/app.ts (100%) rename {src => archive/src}/data/pages.ts (100%) rename {src => archive/src}/data/projects/auv-simulation.ts (100%) rename {src => archive/src}/data/projects/evol-engine.ts (100%) rename {src => archive/src}/data/projects/improv-gfx.ts (100%) rename {src => archive/src}/data/projects/index.ts (100%) rename {src => archive/src}/data/projects/point-cloud.ts (100%) rename {src => archive/src}/data/projects/radiance-caching.ts (100%) rename {src => archive/src}/data/resume.ts (100%) rename {src => archive/src}/data/scenes/about.ts (100%) rename {src => archive/src}/data/scenes/contact.ts (100%) rename {src => archive/src}/data/scenes/hero.ts (100%) rename {src => archive/src}/data/scenes/index.ts (100%) rename {src => archive/src}/data/scenes/projects.ts (100%) rename {src => archive/src}/data/scenes/resume.ts (100%) rename {src => archive/src}/engine/gl-renderer.ts (100%) rename {src => archive/src}/engine/renderer.ts (100%) rename {src => archive/src}/engine/shaders/fullscreen.glsl (100%) rename {src => archive/src}/engine/shaders/fullscreen.wgsl (100%) rename {src => archive/src}/engine/text/slug-generator.ts (100%) rename {src => archive/src}/engine/text/text-layout.ts (100%) rename {src => archive/src}/engine/text/text-renderer.ts (100%) rename {src => archive/src}/engine/text/text.wgsl (100%) rename {src => archive/src}/engine/types.ts (100%) rename {src => archive/src}/main.ts (100%) rename {src => archive/src}/router.ts (100%) rename {src => archive/src}/ui/input.ts (100%) rename {src => archive/src}/ui/overlay.ts (100%) rename tsconfig.json => archive/tsconfig.json (100%) rename vite.config.ts => archive/vite.config.ts (100%) create mode 100644 assets/css/custom.css create mode 100644 content/_index.md create mode 100644 content/about.md create mode 100644 content/posts/_index.md create mode 100644 content/projects/_index.md create mode 100644 content/projects/auv-simulation.md create mode 100644 content/projects/evol-engine.md create mode 100644 content/projects/improv-gfx.md create mode 100644 content/projects/point-cloud.md create mode 100644 content/projects/radiance-caching.md create mode 100644 content/resume.md create mode 100644 data/resume.yaml create mode 100644 gpu-bg/index.html create mode 100644 gpu-bg/package-lock.json create mode 100644 gpu-bg/package.json create mode 100644 gpu-bg/src/gl-renderer.ts create mode 100644 gpu-bg/src/input.ts create mode 100644 gpu-bg/src/main.ts create mode 100644 gpu-bg/src/renderer.ts create mode 100644 gpu-bg/src/shaders/fullscreen.glsl create mode 100644 gpu-bg/src/shaders/fullscreen.wgsl create mode 100644 gpu-bg/src/types.ts create mode 100644 gpu-bg/src/vite-env.d.ts create mode 100644 gpu-bg/tsconfig.json create mode 100644 gpu-bg/vite.config.ts create mode 100644 hugo.toml create mode 100644 layouts/home.html create mode 100644 layouts/partials/gpu-background.html create mode 100644 layouts/projects/list.html create mode 100644 layouts/resume/single.html create mode 160000 themes/hugo-coder diff --git a/.dockerignore b/.dockerignore index aa4495d..86c8618 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,12 +1,10 @@ -node_modules -dist .git -.gitignore -*.md -.env -.env.example -nginx/ -webhook/ -scripts/ -docker-compose.yml -.hermes/ +.hermes +archive +public +resources +gpu-bg/node_modules +gpu-bg/dist +node_modules +*.log +.env \ No newline at end of file diff --git a/.gitignore b/.gitignore index 30bf6c3..8f75413 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,13 @@ resume.tex # CI/CD — webhook secret + deployment env .env + +# Hugo build output + cache +public/ +resources/ +.hugo_build.lock + +# WebGPU engine bundle (compiled from gpu-bg/) +static/gpu/ +gpu-bg/dist/ +gpu-bg/node_modules/ \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..8dcbbd8 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "themes/hugo-coder"] + path = themes/hugo-coder + url = https://github.com/luizdepra/hugo-coder diff --git a/.hermes/plans/2025-07-27_120000-webgl-fallback.md b/.hermes/plans/2025-07-27_120000-webgl-fallback.md new file mode 100644 index 0000000..e6e0574 --- /dev/null +++ b/.hermes/plans/2025-07-27_120000-webgl-fallback.md @@ -0,0 +1,551 @@ +# WebGL Fallback Renderer Implementation Plan + +> **For Hermes:** Use `opencode` CLI to implement this plan task-by-task. Each task is self-contained and should be verified before moving to the next. + +**Goal:** Add a WebGL2 fallback renderer so devices without WebGPU still get an animated procedural background behind the DOM overlay, instead of a static error message. + +**Architecture:** A new `GlRenderer` class mirrors the existing `Renderer` interface (init, render, resize, ready). `app.ts` tries WebGPU first; if that fails, it falls back to WebGL2. Only if both fail does the `#fallback` static message appear. The DOM overlay (nav, content, projects, resume) runs identically regardless of which GPU backend is active — no overlay code changes. + +**Tech Stack:** TypeScript, WebGL2 (GLSL 300 ES), UBO for uniform passing, Vite `?raw` imports for shader source. + +--- + +## Current state recap + +| File | Role | Change needed? | +|------|------|:---:| +| `src/engine/renderer.ts` | WebGPU-only renderer; checks `navigator.gpu`, returns false if unavailable | Extract interface | +| `src/engine/shaders/fullscreen.wgsl` | WGSL fullscreen-triangle fragment shader (spiral + dim) | None (reference for GLSL port) | +| `src/app.ts` | `App.start()` calls `renderer.init()`, returns false on failure → site dead | Try GL on GPU fail | +| `src/main.ts` | Shows `#fallback` div + hides canvas when `app.start()` returns false | Adjust: keep canvas visible for GL path; only show fallback when both fail | +| `index.html` | `#fallback` div (`display:none`), `#gpu` canvas, `#overlay` div | Remove or update fallback message text | + +--- + +## Task 1: Define shared renderer interface + +**Objective:** Extract a structural interface so both `Renderer` and `GlRenderer` are interchangeable in `app.ts`. + +**Files:** +- Create: `src/engine/types.ts` + +**Step 1: Create the interface file** + +```ts +// src/engine/types.ts +import type { UiState } from './renderer' + +export interface RendererLike { + readonly ready: boolean + init(canvas: HTMLCanvasElement): Promise + render(state: UiState): void + resize(): void +} +``` + +Note: `UiState` is already exported from `renderer.ts`. Keep the import there; this avoids circular deps because `types.ts` only imports a type. + +**Step 2: Verify** + +```bash +npm run typecheck +``` +Expected: PASS (no usages yet, just the file existing). + +--- + +## Task 2: Make `Renderer` conform to the interface + +**Objective:** Ensure `Renderer` already satisfies `RendererLike` (it does — just verify typecheck passes when assigned). + +**Files:** +- Modify: `src/engine/renderer.ts` + +**Step 1: Import the interface and add a type assertion test** + +No code changes needed to `Renderer` itself — it already has `ready`, `init`, `render`, `resize`. Just verify: + +```bash +npm run typecheck +``` +Expected: PASS. + +--- + +## Task 3: Create GLSL fullscreen shader + +**Objective:** Port the WGSL spiral shader to GLSL 300 ES (WebGL2). Same visual output: spiral arms, mouse distortion, scene-based palette, dim factor. + +**Files:** +- Create: `src/engine/shaders/fullscreen.glsl` + +**Shader content (vertex + fragment in one file, split on marker comments):** + +```glsl +//---VERTEX--- +#version 300 es + +const vec2 POSITIONS[3] = vec2[]( + vec2(-1.0, -1.0), + vec2(-1.0, 3.0), + vec2( 3.0, -1.0) +); + +out vec2 vUv; + +void main() { + vec2 pos = POSITIONS[gl_VertexID]; + gl_Position = vec4(pos, 0.0, 1.0); + vUv = pos; +} + +//---FRAGMENT--- +#version 300 es +precision highp float; + +layout(std140) uniform Uniforms { + vec2 resolution; + float time; + float scene; + vec2 mouse; + // 8 bytes padding (vec2 → vec4 alignment in std140) + vec4 date; + vec4 sampleRate; + vec4 stemMultipliers[8]; +}; + +in vec2 vUv; +out vec4 outColor; + +void main() { + vec2 fragCoord = (vUv * 0.5 + 0.5) * resolution; + vec2 uv = (fragCoord - 0.5 * resolution) / resolution.y; + + float slowTime = time * 0.2; + + vec3 bgGrey = vec3(0.11, 0.11, 0.12); + vec3 cardGrey = vec3(0.30, 0.32, 0.36); + vec3 coreWhite = vec3(0.90, 0.95, 0.90); + + // palette keyed by scene + vec3 scene0 = vec3(0.20, 0.75, 0.20); // HERO – emerald + vec3 scene1 = vec3(0.85, 0.55, 0.20); // ABOUT – amber + vec3 scene2 = vec3(0.25, 0.60, 0.95); // PROJECTS – cobalt + vec3 scene3 = vec3(0.90, 0.35, 0.55); // CONTACT – rose + + int sFloor = int(clamp(floor(scene), 0.0, 3.0)); + int sCeil = int(clamp(ceil(scene), 0.0, 3.0)); + + vec3 c0; + if (sFloor == 0) c0 = scene0; + else if (sFloor == 1) c0 = scene1; + else if (sFloor == 2) c0 = scene2; + else c0 = scene3; + + vec3 c1; + if (sCeil == 0) c1 = scene0; + else if (sCeil == 1) c1 = scene1; + else if (sCeil == 2) c1 = scene2; + else c1 = scene3; + + float tBlend = scene - floor(scene); + float smoothT = tBlend * tBlend * (3.0 - 2.0 * tBlend); + vec3 sceneAccent = mix(c0, c1, smoothT); + + // mouse-reactive distortion + vec2 mousePos = (mouse - 0.5 * resolution) / resolution.y; + float distMouse = length(uv - mousePos); + float influence = exp(-distMouse * 7.0); + uv += (mousePos - uv) * influence * 0.12; + + float angle = atan(uv.y, uv.x); + float radius = length(uv); + + float baseSpiral = angle + (radius * 8.0) - slowTime; + float ripple = sin(radius * 15.0 - (time * 0.5)) * 0.08; + float distortedSpiral = baseSpiral + ripple; + + float armCount = 2.0; + float rChannel = smoothstep(0.05, 0.55, abs(sin((distortedSpiral + 0.04) * armCount))); + float gChannel = smoothstep(0.05, 0.55, abs(sin(distortedSpiral * armCount))); + float bChannel = smoothstep(0.05, 0.55, abs(sin((distortedSpiral - 0.04) * armCount))); + vec3 patternChannels = vec3(rChannel, gChannel, bChannel); + + vec3 coreNeon = vec3(0.02 / (patternChannels + 0.025)); + vec3 spiralColor = mix(cardGrey, sceneAccent, patternChannels.g); + vec3 primarySpiral = coreNeon * spiralColor; + + float ringWave = sin(radius * 12.0 - (time * 1.5)); + float sharpRing = smoothstep(0.85, 0.95, ringWave); + float ringMask = sharpRing * smoothstep(0.7, 0.1, radius); + vec3 secondaryRings = cardGrey * ringMask * 0.4; + + float vignette = smoothstep(0.95, 0.4, radius); + + vec3 finalColor = bgGrey + secondaryRings; + finalColor = mix(finalColor, primarySpiral, 0.85); + + float centerLight = 0.015 / (radius + 0.02); + finalColor += centerLight * coreWhite; + + // soft cursor glow + finalColor += exp(-distMouse * 4.0) * 0.03 * sceneAccent; + + // dim when not on the home page (date.x controls it) + float dim = date.x; + finalColor = mix(finalColor, vec3(0.0), dim); + + outColor = vec4(finalColor * vignette, 1.0); +} +``` + +**Key porting notes:** +- `select(a, b, cond)` → `if/else` chains (clearer than mix-with-bool-cast in GLSL) +- `array, 8>` → `vec4 stemMultipliers[8]` +- `@builtin(position)` → `gl_Position` +- `@location(0)` → `out vec2 vUv` / `in vec2 vUv` +- Uniform buffer layout with `std140` matches the Float32Array layout exactly (vec2 + float + float = 16 bytes, then 8 bytes padding, then vec4 — same as the TS array d[6],d[7] being zero) +- `atan2(y, x)` → GLSL `atan(y, x)` (same signature) + +**Step 2: Verify the file exists** + +```bash +wc -l src/engine/shaders/fullscreen.glsl +``` +Expected: ~108 lines. + +--- + +## Task 4: Create `GlRenderer` class + +**Objective:** Implement the WebGL2 renderer that mirrors `Renderer`'s lifecycle. + +**Files:** +- Create: `src/engine/gl-renderer.ts` + +**Step 1: Write the GlRenderer class** + +```ts +import glslSource from './shaders/fullscreen.glsl?raw' +import type { UiState } from './renderer' +import type { RendererLike } from './types' + +const UNIFORM_FLOATS = 48 +const UNIFORM_BYTES = UNIFORM_FLOATS * 4 + +function compileShader(gl: WebGL2RenderingContext, type: number, source: string): WebGLShader | null { + const shader = gl.createShader(type) + if (!shader) return null + gl.shaderSource(shader, source) + gl.compileShader(shader) + if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { + console.error('GL shader compile error:', gl.getShaderInfoLog(shader)) + gl.deleteShader(shader) + return null + } + return shader +} + +function linkProgram(gl: WebGL2RenderingContext, vs: WebGLShader, fs: WebGLShader): WebGLProgram | null { + const program = gl.createProgram() + if (!program) return null + gl.attachShader(program, vs) + gl.attachShader(program, fs) + gl.linkProgram(program) + if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { + console.error('GL program link error:', gl.getProgramInfoLog(program)) + gl.deleteProgram(program) + return null + } + return program +} + +function splitShaderSource(source: string): { vs: string; fs: string } { + const parts = source.split('//---FRAGMENT---') + if (parts.length !== 2) throw new Error('GLSL source must contain //---VERTEX--- and //---FRAGMENT--- markers') + const vs = parts[0].replace('//---VERTEX---', '').trim() + const fs = parts[1].trim() + return { vs, fs } +} + +export class GlRenderer implements RendererLike { + private gl: WebGL2RenderingContext | null = null + private program: WebGLProgram | null = null + private ubo: WebGLBuffer | null = null + private uniformData = new Float32Array(UNIFORM_FLOATS) + private _ready = false + + get ready(): boolean { + return this._ready + } + + async init(canvas: HTMLCanvasElement): Promise { + const gl = canvas.getContext('webgl2', { + alpha: false, + antialias: false, + depth: false, + stencil: false, + preserveDrawingBuffer: false, + }) + if (!gl) return false + + this.gl = gl + + const { vs: vsSrc, fs: fsSrc } = splitShaderSource(glslSource) + + const vs = compileShader(gl, gl.VERTEX_SHADER, vsSrc) + if (!vs) return false + + const fs = compileShader(gl, gl.FRAGMENT_SHADER, fsSrc) + if (!fs) { + gl.deleteShader(vs) + return false + } + + const program = linkProgram(gl, vs, fs) + gl.deleteShader(vs) + gl.deleteShader(fs) + if (!program) return false + + this.program = program + + // Create UBO + const ubo = gl.createBuffer() + if (!ubo) return false + gl.bindBuffer(gl.UNIFORM_BUFFER, ubo) + gl.bufferData(gl.UNIFORM_BUFFER, UNIFORM_BYTES, gl.DYNAMIC_DRAW) + + const blockIndex = gl.getUniformBlockIndex(program, 'Uniforms') + if (blockIndex === gl.INVALID_INDEX) { + console.error('Uniform block "Uniforms" not found in GLSL program') + return false + } + gl.uniformBlockBinding(program, blockIndex, 0) + gl.bindBufferBase(gl.UNIFORM_BUFFER, 0, ubo) + + this.ubo = ubo + this._ready = true + return true + } + + resize(): void { + if (!this.gl) return + this.gl.viewport(0, 0, this.gl.canvas.width, this.gl.canvas.height) + } + + render(state: UiState): void { + const gl = this.gl + if (!gl || !this.ubo || !this._ready) return + + const d = this.uniformData + d[0] = state.resolution[0] + d[1] = state.resolution[1] + d[2] = state.time + d[3] = state.scene + d[4] = state.mouse[0] + d[5] = state.mouse[1] + // d[6], d[7]: padding — left as zero + d[8] = state.dim + // d[12]-d[15]: sampleRate — left as zero + d.set(state.stemMultipliers, 16) + + gl.bindBuffer(gl.UNIFORM_BUFFER, this.ubo) + gl.bufferSubData(gl.UNIFORM_BUFFER, 0, this.uniformData) + + gl.useProgram(this.program) + gl.drawArrays(gl.TRIANGLES, 0, 3) + } +} +``` + +**Step 2: Verify** + +```bash +npm run typecheck +``` +Expected: PASS. The GLSL import via `?raw` is a string; the `RendererLike` import is a type; all WebGL2 types are in the standard DOM lib. + +--- + +## Task 5: Wire fallback logic in `app.ts` + +**Objective:** `App.start()` tries WebGPU → falls back to WebGL → only returns false if both fail. + +**Files:** +- Modify: `src/app.ts` + +**Changes:** + +1. Add imports at top: +```ts +import { Renderer, UiState } from './engine/renderer' +import { GlRenderer } from './engine/gl-renderer' +import type { RendererLike } from './engine/types' +``` + +2. Change renderer field type: +```ts +private renderer: RendererLike = new Renderer() +``` + +3. In `start()`, change the init block: +```ts +let ok = await this.renderer.init(this.canvas) +if (!ok) { + this.renderer = new GlRenderer() + ok = await this.renderer.init(this.canvas) +} +if (!ok) return false +``` + +That's it. The rest of `app.ts` (resize, loop, render calls) works unchanged because both renderers satisfy `RendererLike`. + +**Step 2: Verify** + +```bash +npm run typecheck +``` +Expected: PASS. + +--- + +## Task 6: Adjust `main.ts` fallback behavior + +**Objective:** Only show the static `#fallback` div when BOTH WebGPU and WebGL are unavailable. When WebGL succeeds, the canvas is already visible and rendering — no action needed. + +**Files:** +- Modify: `src/main.ts` + +**Current code (lines 1-16):** +```ts +import { App } from './app' + +const canvas = document.getElementById('gpu') as HTMLCanvasElement +const fallback = document.getElementById('fallback') as HTMLElement + +async function boot(): Promise { + const app = new App(canvas) + const ok = await app.start() + if (!ok) { + fallback.style.display = 'flex' + canvas.style.display = 'none' + return + } +} + +void boot() +``` + +**New code:** No structural change needed — the logic is already correct. When `app.start()` returns false (both GPU and GL failed), the fallback div shows. When it returns true (either backend works), the canvas is visible and rendering. No changes required. + +**Step 2: Update the fallback message in `index.html`** + +Change the `#fallback` content in `index.html` to mention WebGL too: + +```html +
+
+

Graphics not available

+

Your browser doesn't support WebGPU or WebGL. Open this page in a recent Chrome, Edge, or Firefox.

+
+
+``` + +**Step 3: Verify** + +```bash +npm run typecheck +``` +Expected: PASS (no TS changes, just HTML). + +--- + +## Task 7: Build and final check + +**Objective:** Full build to verify everything compiles and bundles correctly. + +**Files:** None (verification only) + +**Step 1: Build** + +```bash +npm run build +``` +Expected: PASS. The `?raw` import for `.glsl` should be handled by Vite just like `.wgsl`. + +If Vite complains about `.glsl` extension, add to `vite.config.ts`: +```ts +export default defineConfig({ + server: { open: true }, + build: { target: 'esnext' }, + assetsInclude: ['**/*.glsl'], +}) +``` + +**Step 2: Check dist size** + +```bash +du -sh dist/ +``` +The GLSL source (~3KB raw) will be inlined as a string in the JS bundle. Acceptable overhead. + +--- + +## Task 8: Manual smoke test (dev server) + +**Objective:** Verify the site works in both paths. + +**Files:** None (verification only) + +**Step 1: Start dev server** + +```bash +npm run dev +``` + +**Step 2: Test WebGPU path** + +Open in Chrome/Edge with WebGPU enabled → should see the normal WebGPU spiral + DOM overlay. + +**Step 3: Test WebGL fallback path** + +Open in Firefox (no WebGPU) → should see the WebGL spiral + fully functional DOM overlay. + +Alternatively, temporarily disable WebGPU in Chrome (`chrome://flags/#enable-unsafe-webgpu` → Disabled) and reload. + +**Step 4: Verify DOM overlay on both paths** + +- Click through nav tabs (Home, About, Projects, Resume) +- Open a project detail, click back +- Scroll resume +- Confirm mouse movement affects the background (cursor glow) + +--- + +## Summary of changes + +| File | Action | Lines | +|------|--------|-------| +| `src/engine/types.ts` | Create | ~6 | +| `src/engine/shaders/fullscreen.glsl` | Create | ~108 | +| `src/engine/gl-renderer.ts` | Create | ~110 | +| `src/app.ts` | Modify: add GL fallback init | +8, ~4 changed | +| `src/main.ts` | No changes needed | 0 | +| `index.html` | Update fallback message text | ~2 lines changed | +| `vite.config.ts` | Possibly add `assetsInclude` for `.glsl` | +1 line | + +Total: ~230 new lines, ~6 lines modified. No DOM overlay code touched. No existing shader touched. + +--- + +## Risks and mitigations + +1. **Vite doesn't handle `.glsl` with `?raw`** — Mitigation: add `assetsInclude: ['**/*.glsl']` to vite config. Vite treats included assets as raw strings by default when imported with `?raw`. + +2. **GLSL precision differences** — The WGSL shader uses `f32` (IEEE 754 binary32). WebGL fragment shaders default to `mediump` which may not have enough precision for the spiral math. Mitigation: use `precision highp float;` at the top of the fragment shader. All modern mobile and desktop GPUs support `highp` in fragment shaders. + +3. **UBO layout mismatch** — If the GLSL compiler pads the struct differently than expected, the uniform data will be misaligned. Mitigation: explicitly use `layout(std140)` which guarantees the exact layout we've matched. The TS array layout was designed to match std140 already (d[6],d[7] = 8 bytes of padding before `date`). + +4. **`noUnusedLocals` / `noUnusedParameters`** — If any GL renderer methods or params trigger TS unused warnings. Mitigation: all method implementations use their params; the `_ready` naming convention is already established in the project. + +5. **Context loss handling** — The WebGL2 context can be lost. Mitigation: WebGL2 has `webglcontextlost` / `webglcontextrestored` events. For now, the current codebase doesn't handle WebGPU device loss recovery (beyond logging), so feature parity is sufficient. Can add recovery in a follow-up. diff --git a/.hermes/plans/2026-08-16_hugo-transition.md b/.hermes/plans/2026-08-16_hugo-transition.md new file mode 100644 index 0000000..4ccf67c --- /dev/null +++ b/.hermes/plans/2026-08-16_hugo-transition.md @@ -0,0 +1,307 @@ +# Hugo Transition Plan — WebGPU Portfolio + +> **For Hermes:** implement this plan via `opencode` CLI, task-by-task, small verifiable chunks wrapped with `timeout`. Verify each task (typecheck/build/hugo/curl) before moving on. After any timeout, inspect `git status` before continuing. + +**Created:** 2026-08-16 +**Goal:** Replace the Vite/WebGPU single-page app with a Hugo static site (hugo-coder theme, dark), keeping the WebGPU spiral visuals as a full-page homepage background behind the theme. Everything else — content, nav, routing, resume, projects, blog — becomes Hugo. + +--- + +## 1. Locked decisions (recap) + +| # | Decision | Choice | +|---|----------|--------| +| 1 | Engine scope | Minimal background shader only (spiral + mouse + accent/dim). Drop text-renderer, router, DOM overlay, TS data files | +| 2 | Engine build | Keep Vite + TS as a sub-package; emit a static JS bundle into Hugo `static/` | +| 3 | Repo layout | In-place monorepo; Hugo at root, engine under `gpu-bg/` | +| 4 | Homepage canvas | Full-page background, dark theme overall | +| 5 | Identity | "Djosen" as site title + displayed author (full alias); resume keeps real name "Ahmed Mohamed" | +| 6 | Resume | Structured `data/resume.yaml` + custom Hugo layout | +| 7 | Projects | `categories` taxonomy (programming/hardware/visuals/music), tech → `tags` | +| 8 | Menu + blog | Home, About, Projects, Resume, Blog — blog enabled | +| 9 | Accent | hugo-coder default blue accent; background stays emerald (two-tone, revisit after render) | +| 10 | baseURL | `https://djosen.one/` | +| 11 | Theme install | git submodule at `themes/hugo-coder` | +| 12 | Old SPA code | Archive (tag + `archive/` folder), do not delete | +| 13 | CI/CD | Deploy workflow lives server-side; produce a handover doc to adjust it | + +--- + +## 2. Target architecture + +``` +webgpu/ (repo root = Hugo site) +├── config.toml # hugo-coder config (TOML) +├── content/ +│ ├── _index.md # homepage welcome text +│ ├── about.md +│ ├── resume.md # layout = "resume", renders data/resume.yaml +│ ├── projects/ +│ │ ├── radiance-caching.md +│ │ ├── point-cloud.md +│ │ ├── evol-engine.md +│ │ ├── auv-simulation.md +│ │ └── improv-gfx.md +│ └── posts/ +│ └── _index.md # blog landing (empty starter) +├── data/ +│ └── resume.yaml +├── layouts/ +│ ├── index.html # homepage override → includes gpu-background partial +│ ├── partials/ +│ │ └── gpu-background.html # + `. +2. `layouts/index.html` — override homepage: include the partial; canvas is `position: fixed; inset: 0; z-index: -1`; content container gets transparent/dark glass background so the spiral shows through with readable text. +3. `static/css/custom.css` — the canvas + glass styling above; loaded via the theme's head hook (`layouts/partials/head.html` override, or hugo-coder's `custom_css` param if present — **verify exact hook during implementation**). +4. `layouts/resume/single.html` — render `site.Data.resume` (summary, work, education, skills) matching the current card/pill visual but in hugo-coder's palette. +5. `layouts/projects/list.html` — only if the default list doesn't show `period` + `description` + `tags` acceptably. + +--- + +## 6. Build orchestration + +**Engine (`gpu-bg/`):** `vite build` → a single self-contained bundle (WGSL/GLSL inlined via `?raw`) emitted to `gpu-bg/dist/`, which the Hugo build copies into `static/gpu/`. Configure `vite.config.ts` build `outDir` + `assetsInlineLimit` so shaders inline; `assetsInclude: ['**/*.glsl', '**/*.wgsl']`. + +**Dev workflow:** +- `cd gpu-bg && npm run build` (engine), then `hugo server` from root (watches content + the built bundle in `static/`). +- Optional single `npm`/`Makefile` orchestration script: `build:engine && hugo server`. + +**Dockerfile (3-stage):** +```dockerfile +FROM node:22-alpine AS engine +WORKDIR /gpu +COPY gpu-bg/package.json gpu-bg/package-lock.json* ./ +RUN npm ci +COPY gpu-bg/ ./ +RUN npm run build + +FROM gohugoio/hugo:ext-alpine AS site # pin a version tag +WORKDIR /site +COPY . . +COPY --from=engine /gpu/dist ./static/gpu/ +RUN hugo --minify + +FROM nginx:alpine-slim AS serve +COPY --from=site /site/public /usr/share/nginx/html +COPY nginx.default.conf /etc/nginx/conf.d/default.conf +RUN nginx -t +EXPOSE 8080 +CMD ["nginx", "-g", "daemon off;"] +``` + +**Critical:** the build context must contain the initialized `themes/hugo-coder` submodule. Any clone/deploy step must use `--recursive` or run `git submodule update --init --recursive` before `docker build`. + +**nginx.default.conf changes:** replace the SPA fallback (`try_files $uri $uri/ /index.html`) with plain static `try_files $uri $uri/ =404` (Hugo emits real `.html` per page; 404s are now legitimate). Keep security headers + gzip. Adjust immutable-cache rule to Hugo's output dirs (`/css/`, `/js/`, `/gpu/`) with fingerprint-aware caching (Hugo fingerprints via filename hash). + +--- + +## 7. Task breakdown (for opencode) + +Each task = one opencode run, self-contained, with an explicit verify step. + +0. **Safety tag** — `git tag pre-hugo` at current HEAD; confirm clean tree. +1. **Toolchain** — install/extend Hugo (extended) locally; `git submodule add` hugo-coder; init submodule. +2. **Archive SPA** — move `src/`, `index.html`, root `package.json`/`package-lock.json`/`tsconfig.json`/`vite.config.ts` → `archive/`; update `.gitignore` (`public/`, `static/gpu/`, `resources/`, `gpu-bg/dist/`, `gpu-bg/node_modules/`). +3. **Engine sub-package** — create `gpu-bg/` with copied renderer/gl-renderer/types/input/shaders + slim `main.ts` + `package.json`/`tsconfig.json`/`vite.config.ts`. Verify: `npm run typecheck` in `gpu-bg/`. +4. **Engine build** — `npm run build`; verify `dist/` bundle exists and shaders are inlined. +5. **Hugo scaffold** — `config.toml`; `content/_index.md`, `about.md`, `resume.md`, `projects/_index.md`, `posts/_index.md`; `data/resume.yaml`. Verify: `hugo` builds, homepage/about render. +6. **Projects migration** — 5 × `content/projects/*.md` with frontmatter. Verify: `hugo` builds; `/projects` + each project URL render. +7. **Custom layouts** — gpu-background partial, index.html override + custom.css, resume layout, projects list tweak. Verify: `hugo` builds; homepage contains `` + `/gpu/engine.js` link. +8. **Docker + nginx** — rewrite Dockerfile (3-stage), update nginx.default.conf, `.gitignore`. Verify: `docker build` succeeds; `docker compose up -d`; `curl -sf http://127.0.0.1:8080/`, `/about`, `/projects`, `/resume`, `/posts`, `/gpu/engine.js` all 200; a bogus path → 404. +9. **Docs** — write `DEPLOYMENT.md` (section 9 content) to repo; update `README.md` dev/Docker sections. +10. **Final verification** — `npm run typecheck` + `npm run build` (engine), `hugo`, `docker compose` smoke test, and a manual browser check of WebGPU render + WebGL fallback + dark theme + canvas-behind-content. + +--- + +## 8. Verification checklist (acceptance) + +- `hugo` build exits 0; `public/` populated with real `.html` per page (no SPA fallback). +- Homepage renders the WebGPU spiral behind hugo-coder content; text readable (dark theme). +- WebGL2 fallback works when WebGPU is unavailable (manual: disable WebGPU in a browser). +- `/about`, `/projects`, `/projects/`, `/resume`, `/posts`, `/` all return 200; unknown path → 404. +- `/gpu/engine.js` returns 200 (hashed, cacheable). +- Resume renders from `data/resume.yaml` with real name + full work/education/skills. +- Projects list groups under the `programming` category; tags shown. +- Docker image builds from a recursive clone; container healthcheck passes. +- `git status` clean of build artifacts (public/, static/gpu/, gpu-bg/dist/, node_modules/ all ignored). + +--- + +## 9. DEPLOYMENT HANDOVER (copy verbatim into repo as `DEPLOYMENT.md`) + +> How the site now works, and how to adjust the server-side deploy workflow (Gitea Actions + webhook) for the new build. + +### 9.1 How it works now (unchanged parts) + +Serving topology is **unchanged**. The host's system nginx (Certbot-managed TLS at `djosen.one`) still proxies `location /` → `127.0.0.1:8080`, where the Docker `web` container listens. Nothing in the system nginx (`nginx/system-default.conf` / `nginx/reverse.conf`) or Certbot setup needs to change. + +What changes is **what's inside the container**: instead of a Vite SPA (single `index.html` + JS), the container now serves a Hugo static site (`public/` — one real `.html` per page). + +### 9.2 What changed in the build + +- **Build chain is now two steps:** (1) `gpu-bg/` Vite build → static JS bundle, (2) `hugo --minify` → `public/`. The Dockerfile does both in stages (`node` → `hugo` extended → `nginx`). +- **Hugo extended is required** (the theme compiles SCSS). The Docker stage uses a Hugo-extended image (pinned tag). +- **The hugo-coder theme is a git submodule** at `themes/hugo-coder`. +- **nginx in the container no longer does SPA fallback** — plain static serving with fingerprint-aware cache rules. + +### 9.3 How to adjust the server-side deploy workflow + +The deploy workflow (Gitea Actions `deploy.yml` / webhook script) currently clones and `docker build`s. Two adjustments are mandatory: + +1. **Clone with submodules.** The hugo-coder submodule must be present before `docker build`. Change the checkout step to recursive: + - Gitea Actions: `checkout@v3` with `submodules: recursive` (or add `git submodule update --init --recursive`). + - Plain webhook/script: `git clone --recursive ...` or `git pull && git submodule update --init --recursive`. + Without this, `hugo` fails with "theme not found" and the build breaks. + +2. **No other secrets/hosts change.** Registry creds, SSH/webhook target, and the `docker compose up -d` deploy command are all the same. The image just gets bigger (Hugo extended + node build layers are discarded; final nginx stage stays small). + +### 9.4 How to test the deployment + +**Local (before pushing):** +```bash +git submodule update --init --recursive +cd gpu-bg && npm install && npm run build && cd .. +hugo # builds public/ +docker compose build # 3-stage build +docker compose up -d # maps 127.0.0.1:8080:8080 +``` + +**Smoke test (after `up -d`):** +```bash +curl -sf http://127.0.0.1:8080/ | grep -q 'id="gpu"' && echo "canvas OK" +curl -sf http://127.0.0.1:8080/gpu/engine.js -o /dev/null && echo "engine bundle OK" +curl -sf http://127.0.0.1:8080/about/ -o /dev/null && echo "about OK" +curl -sf http://127.0.0.1:8080/projects/ -o /dev/null && echo "projects OK" +curl -sf http://127.0.0.1:8080/resume/ -o /dev/null && echo "resume OK" +curl -sf http://127.0.0.1:8080/posts/ -o /dev/null && echo "posts OK" +curl -s http://127.0.0.1:8080/nonexistent -o /dev/null -w "%{http_code}\n" # expect 404 +``` + +**Browser check (manual, WebGPU is a real-GPU feature):** +- Homepage: spiral animates behind content; dark theme; text readable. +- Disable WebGPU (or use a non-WebGPU browser) → WebGL2 fallback still animates. +- No-WebGL browser → static dark page (content still readable; canvas simply doesn't render). + +**After deploy (remote):** +- `curl -s https://djosen.one/health` → `ok` (system nginx health endpoint, unchanged). +- Verify `/`, `/about/`, `/projects/`, `/resume/`, `/posts/` over HTTPS. +- Confirm the spiral renders in a real browser (the one thing `curl` can't check). + +### 9.5 Rollback + +The previous site is preserved as the git tag `pre-hugo` and the `archive/` folder. To roll back: `git checkout pre-hugo` and redeploy with the old single-stage `Dockerfile` (also recoverable from git history). No system nginx / DNS changes were made, so rollback is purely a container-image swap. diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md new file mode 100644 index 0000000..a73854c --- /dev/null +++ b/DEPLOYMENT.md @@ -0,0 +1,89 @@ +# Deployment Handover + +How the migrated Hugo site is served, and how to adjust the server-side deploy workflow (Gitea Actions + webhook) for the new two-stage build. + +## Serving topology (unchanged) + +The site is served as before. The host's system nginx (Certbot-managed TLS at `djosen.one`) proxies `location /` → `127.0.0.1:8080`, where the Docker `web` container listens. **Nothing changed** in the system nginx (`nginx/system-default.conf` / `nginx/reverse.conf`) or the Certbot/DNS setup during this migration. + +What changed is **what's inside the container**: instead of a Vite SPA (a single `index.html` plus JavaScript), the container now serves a Hugo static site — `public/` with one real `.html` page per path. + +## What changed in the build + +The build is now **two steps**: + +1. **Engine** — `gpu-bg/` Vite/Typescript build → a static JS bundle (`gpu-bg/dist/`). +2. **Site** — `hugo --minify` → `public/`. + +The Dockerfile runs both in stages (`node` → `hugo` → `nginx`). + +Key details for the server build: + +- **Hugo image:** the `site` stage uses `hugomods/hugo:debian-dart-sass-go-0.165.0` — an **extended Hugo** build including dart-sass. This is required because hugo-coder compiles the theme's SCSS via Hugo Pipes (`toCSS`), and Hugo 0.129+ dropped libsass in favor of dart-sass. This replaced the deprecated `klakegg/hugo` image. +- **Theme submodule:** hugo-coder lives as a **git submodule** at `themes/hugo-coder`. +- **Static serving only:** the container nginx no longer does SPA fallback (`try_files ... /index.html`). It serves plain static files — each page is a real `.html`, and a 404 for a missing path is legitimate and intentional. + +## Mandatory server-side change (the #1 gotcha) + +**Your clone/checkout must initialize submodules before building.** The `themes/hugo-coder` submodule must be pulled in before `docker build`; otherwise `hugo` fails with `theme not found` and the build breaks. + +- **Gitea Actions `deploy.yml`:** make the checkout recursive. With `actions/checkout`, set: + + ```yaml + - uses: actions/checkout@v3 + with: + submodules: recursive + ``` + +- **Plain webhook / shell:** use a recursive clone, or run the update before building: + + ```bash + git clone --recursive ... + # or, on an existing clone: + git pull + git submodule update --init --recursive + ``` + +Nothing else changes server-side: registry credentials, the SSH/webhook target, and the `docker compose up -d` deploy command are all the same. The image is slightly larger to build (node and Hugo-extended layers are discarded; the final nginx stage stays small). + +## Testing + +### Local (before pushing) + +```bash +git submodule update --init --recursive +cd gpu-bg && npm install && npm run build && cd .. +hugo # builds public/ +docker compose build # 3-stage build +docker compose up -d # maps 127.0.0.1:8080:8080 +``` + +### Smoke test (after `up -d`) + +```bash +curl -sf http://127.0.0.1:8080/ | grep -q 'id="gpu"' && echo "canvas OK" +curl -sf http://127.0.0.1:8080/gpu/engine.js -o /dev/null && echo "engine bundle OK" +curl -sf http://127.0.0.1:8080/about/ -o /dev/null && echo "about OK" +curl -sf http://127.0.0.1:8080/projects/ -o /dev/null && echo "projects OK" +curl -sf http://127.0.0.1:8080/resume/ -o /dev/null && echo "resume OK" +curl -sf http://127.0.0.1:8080/posts/ -o /dev/null && echo "posts OK" +curl -s http://127.0.0.1:8080/nonexistent -o /dev/null -w "%{http_code}\n" # expect 404 +``` + +### Browser check (manual — WebGPU is a real-GPU feature) + +- Homepage: emerald spiral animates behind the dark glass panel; text readable. +- With WebGPU disabled, the WebGL2 fallback still animates. +- With no WebGL at all, the page still shows its content; the canvas simply doesn't render. + +### After deploy (remote) + +- `curl -s https://djosen.one/health` → `ok` (system nginx health endpoint, unchanged). +- Verify `/`, `/about/`, `/projects/`, `/resume/`, `/posts/` over HTTPS. +- Confirm the spiral actually renders in a real browser — the one thing `curl` cannot check. + +## Rollback + +The previous site is preserved in the git tag `pre-hugo` and the `archive/` folder. To roll back: `git checkout pre-hugo` and redeploy with the old single-stage Dockerfile (also recoverable from git history). + +No system nginx or DNS changes were made in this migration, so rollback is purely a container-image swap. \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 272a0bd..02eee4f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,34 +1,24 @@ -# ---- stage 1: build ---- -FROM node:22-alpine AS build -WORKDIR /app - -# Install deps (cache layer) -COPY package.json package-lock.json* ./ +FROM node:22-alpine AS engine +WORKDIR /gpu +COPY gpu-bg/package.json gpu-bg/package-lock.json* ./ RUN npm ci - -# Copy source and build -COPY tsconfig.json vite.config.ts index.html ./ -COPY src/ src/ - -# Run tests first — gates the build -RUN npm test - +COPY gpu-bg/ ./ RUN npm run build -# ---- stage 2: serve ---- +# hugomods/hugo is the maintained successor to the deprecated klakegg/hugo image. +# The "dart-sass" variant is extended Hugo + dart-sass (required: hugo-coder compiles +# SCSS via toCSS, and Hugo 0.129+ dropped libsass in favor of dart-sass). +FROM hugomods/hugo:debian-dart-sass-go-0.165.0 AS site +WORKDIR /site +COPY . . +COPY --from=engine /gpu/dist ./static/gpu/ +RUN hugo --minify + FROM nginx:alpine-slim AS serve - -# curl needed for the healthcheck in docker-compose RUN apk add --no-cache curl - -COPY --from=build /app/dist /usr/share/nginx/html - -# Wipe any default server blocks that could conflict with our config. +COPY --from=site /site/public /usr/share/nginx/html RUN rm -f /etc/nginx/conf.d/default.conf - -# Deploy our custom nginx config and validate it at build time COPY nginx.default.conf /etc/nginx/conf.d/default.conf RUN nginx -t - EXPOSE 8080 -CMD ["nginx", "-g", "daemon off;"] +CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/README.md b/README.md index 1c4b070..5e57df6 100644 --- a/README.md +++ b/README.md @@ -1,46 +1,46 @@ -# WebGPU Portfolio +# Djosen — personal site + +Hugo static site (hugo-coder theme, dark) with a WebGPU spiral background on the homepage. [![Build & Deploy](https://git.djosen.one/djoser/webgpu-portfolio/actions/workflows/deploy.yml/badge.svg?branch=master)](https://git.djosen.one/djoser/webgpu-portfolio/actions) -Generative-art portfolio site. WebGPU canvas background, DOM overlay UI. +## Structure + +- **Hugo site (repo root):** `content/` (pages, projects, posts), `data/resume.yaml`, `layouts/` (custom homepage/resume templates), `themes/hugo-coder` (git submodule), `static/gpu/` (the compiled engine bundle). +- **`gpu-bg/`** — the WebGPU/WebGL2 background engine (`Vite` + `Typescript`), built to a static JS bundle. +- **`archive/`** — the retired Vite SPA, preserved for reference. ## Dev +**Hugo server** (site content): + ```bash -npm install -npm run dev # vite dev server (opens browser) -npm run typecheck # tsc --noEmit -npm run build # typecheck + vite build → dist/ -npm run preview # serve built dist/ +hugo server +``` + +**Engine** (background): + +```bash +cd gpu-bg && npm run dev +``` + +**Full loop** — build the engine, copy the bundle into the Hugo site, then serve: + +```bash +cd gpu-bg && npm install && npm run build +cp gpu-bg/dist/engine.js static/gpu/engine.js +hugo server ``` ## Docker ```bash -# Build -docker build -t webgpu-portfolio . - -# Run (foreground) -docker run --rm -p 8080:80 webgpu-portfolio - -# Run (detached) -docker run -d -p 8080:80 --name portfolio webgpu-portfolio - -# Compose +docker compose build docker compose up -d ``` -Site at `http://localhost:8080`. +Site at `http://127.0.0.1:8080`. -### Smoke test +## Deploy -```bash -docker run -d -p 8080:80 --name portfolio-test webgpu-portfolio -curl -sSf http://localhost:8080/ > /dev/null && echo "OK" -curl -sSf http://localhost:8080/assets/ | head -1 # should 200 (hashed JS) -docker stop portfolio-test -``` - -### Image size - -~6 MB (nginx:alpine-slim + static dist). Multi-stage build — Node build layer is discarded. +Server-side deployment (Gitea Actions / webhook) is documented in [DEPLOYMENT.md](DEPLOYMENT.md) — including the mandatory recursive-submodule checkout for the Hugo build. \ No newline at end of file diff --git a/archive/Dockerfile b/archive/Dockerfile new file mode 100644 index 0000000..272a0bd --- /dev/null +++ b/archive/Dockerfile @@ -0,0 +1,34 @@ +# ---- stage 1: build ---- +FROM node:22-alpine AS build +WORKDIR /app + +# Install deps (cache layer) +COPY package.json package-lock.json* ./ +RUN npm ci + +# Copy source and build +COPY tsconfig.json vite.config.ts index.html ./ +COPY src/ src/ + +# Run tests first — gates the build +RUN npm test + +RUN npm run build + +# ---- stage 2: serve ---- +FROM nginx:alpine-slim AS serve + +# curl needed for the healthcheck in docker-compose +RUN apk add --no-cache curl + +COPY --from=build /app/dist /usr/share/nginx/html + +# Wipe any default server blocks that could conflict with our config. +RUN rm -f /etc/nginx/conf.d/default.conf + +# Deploy our custom nginx config and validate it at build time +COPY nginx.default.conf /etc/nginx/conf.d/default.conf +RUN nginx -t + +EXPOSE 8080 +CMD ["nginx", "-g", "daemon off;"] diff --git a/index.html b/archive/index.html similarity index 100% rename from index.html rename to archive/index.html diff --git a/package-lock.json b/archive/package-lock.json similarity index 100% rename from package-lock.json rename to archive/package-lock.json diff --git a/package.json b/archive/package.json similarity index 100% rename from package.json rename to archive/package.json diff --git a/src/app.ts b/archive/src/app.ts similarity index 100% rename from src/app.ts rename to archive/src/app.ts diff --git a/src/data/pages.ts b/archive/src/data/pages.ts similarity index 100% rename from src/data/pages.ts rename to archive/src/data/pages.ts diff --git a/src/data/projects/auv-simulation.ts b/archive/src/data/projects/auv-simulation.ts similarity index 100% rename from src/data/projects/auv-simulation.ts rename to archive/src/data/projects/auv-simulation.ts diff --git a/src/data/projects/evol-engine.ts b/archive/src/data/projects/evol-engine.ts similarity index 100% rename from src/data/projects/evol-engine.ts rename to archive/src/data/projects/evol-engine.ts diff --git a/src/data/projects/improv-gfx.ts b/archive/src/data/projects/improv-gfx.ts similarity index 100% rename from src/data/projects/improv-gfx.ts rename to archive/src/data/projects/improv-gfx.ts diff --git a/src/data/projects/index.ts b/archive/src/data/projects/index.ts similarity index 100% rename from src/data/projects/index.ts rename to archive/src/data/projects/index.ts diff --git a/src/data/projects/point-cloud.ts b/archive/src/data/projects/point-cloud.ts similarity index 100% rename from src/data/projects/point-cloud.ts rename to archive/src/data/projects/point-cloud.ts diff --git a/src/data/projects/radiance-caching.ts b/archive/src/data/projects/radiance-caching.ts similarity index 100% rename from src/data/projects/radiance-caching.ts rename to archive/src/data/projects/radiance-caching.ts diff --git a/src/data/resume.ts b/archive/src/data/resume.ts similarity index 100% rename from src/data/resume.ts rename to archive/src/data/resume.ts diff --git a/src/data/scenes/about.ts b/archive/src/data/scenes/about.ts similarity index 100% rename from src/data/scenes/about.ts rename to archive/src/data/scenes/about.ts diff --git a/src/data/scenes/contact.ts b/archive/src/data/scenes/contact.ts similarity index 100% rename from src/data/scenes/contact.ts rename to archive/src/data/scenes/contact.ts diff --git a/src/data/scenes/hero.ts b/archive/src/data/scenes/hero.ts similarity index 100% rename from src/data/scenes/hero.ts rename to archive/src/data/scenes/hero.ts diff --git a/src/data/scenes/index.ts b/archive/src/data/scenes/index.ts similarity index 100% rename from src/data/scenes/index.ts rename to archive/src/data/scenes/index.ts diff --git a/src/data/scenes/projects.ts b/archive/src/data/scenes/projects.ts similarity index 100% rename from src/data/scenes/projects.ts rename to archive/src/data/scenes/projects.ts diff --git a/src/data/scenes/resume.ts b/archive/src/data/scenes/resume.ts similarity index 100% rename from src/data/scenes/resume.ts rename to archive/src/data/scenes/resume.ts diff --git a/src/engine/gl-renderer.ts b/archive/src/engine/gl-renderer.ts similarity index 100% rename from src/engine/gl-renderer.ts rename to archive/src/engine/gl-renderer.ts diff --git a/src/engine/renderer.ts b/archive/src/engine/renderer.ts similarity index 100% rename from src/engine/renderer.ts rename to archive/src/engine/renderer.ts diff --git a/src/engine/shaders/fullscreen.glsl b/archive/src/engine/shaders/fullscreen.glsl similarity index 100% rename from src/engine/shaders/fullscreen.glsl rename to archive/src/engine/shaders/fullscreen.glsl diff --git a/src/engine/shaders/fullscreen.wgsl b/archive/src/engine/shaders/fullscreen.wgsl similarity index 100% rename from src/engine/shaders/fullscreen.wgsl rename to archive/src/engine/shaders/fullscreen.wgsl diff --git a/src/engine/text/slug-generator.ts b/archive/src/engine/text/slug-generator.ts similarity index 100% rename from src/engine/text/slug-generator.ts rename to archive/src/engine/text/slug-generator.ts diff --git a/src/engine/text/text-layout.ts b/archive/src/engine/text/text-layout.ts similarity index 100% rename from src/engine/text/text-layout.ts rename to archive/src/engine/text/text-layout.ts diff --git a/src/engine/text/text-renderer.ts b/archive/src/engine/text/text-renderer.ts similarity index 100% rename from src/engine/text/text-renderer.ts rename to archive/src/engine/text/text-renderer.ts diff --git a/src/engine/text/text.wgsl b/archive/src/engine/text/text.wgsl similarity index 100% rename from src/engine/text/text.wgsl rename to archive/src/engine/text/text.wgsl diff --git a/src/engine/types.ts b/archive/src/engine/types.ts similarity index 100% rename from src/engine/types.ts rename to archive/src/engine/types.ts diff --git a/src/main.ts b/archive/src/main.ts similarity index 100% rename from src/main.ts rename to archive/src/main.ts diff --git a/src/router.ts b/archive/src/router.ts similarity index 100% rename from src/router.ts rename to archive/src/router.ts diff --git a/src/ui/input.ts b/archive/src/ui/input.ts similarity index 100% rename from src/ui/input.ts rename to archive/src/ui/input.ts diff --git a/src/ui/overlay.ts b/archive/src/ui/overlay.ts similarity index 100% rename from src/ui/overlay.ts rename to archive/src/ui/overlay.ts diff --git a/tsconfig.json b/archive/tsconfig.json similarity index 100% rename from tsconfig.json rename to archive/tsconfig.json diff --git a/vite.config.ts b/archive/vite.config.ts similarity index 100% rename from vite.config.ts rename to archive/vite.config.ts diff --git a/assets/css/custom.css b/assets/css/custom.css new file mode 100644 index 0000000..3b1e666 --- /dev/null +++ b/assets/css/custom.css @@ -0,0 +1,93 @@ +/* ---- WebGPU background layer ---- */ +/* The canvas is rendered INSIDE main.wrapper (home partial), so it is a sibling + of the nav and sections. As a positioned (fixed) element it stacks ABOVE the + static-positioned nav/content. Give it z-index:-1 so it paints behind all + in-flow content on the page. */ +#gpu { + position: fixed; + inset: 0; + width: 100vw; + height: 100vh; + z-index: -1; + overflow: hidden; +} + +/* The nav has no opaque background in vanilla hugo-coder, so the shader + shows straight through behind the links. Same dark-glass as the home panel. + (This is a backdrop for readability, not a re-stack — the nav now paints + above the z-index:-1 canvas because it is normal in-flow content.) */ +.navigation { + background: rgba(33, 33, 33, 0.82); + -webkit-backdrop-filter: blur(6px); + backdrop-filter: blur(6px); + border-bottom: 1px solid rgba(255, 255, 255, 0.08); +} + +/* ---- Homepage: dark glass panels for readability over the background ---- */ +.centered .about { + background: rgba(33, 33, 33, 0.72); + -webkit-backdrop-filter: blur(6px); + backdrop-filter: blur(6px); + border: 1px solid rgba(255, 255, 255, 0.06); + border-radius: 1.1rem; + padding: 2.4rem 2.8rem; + box-shadow: 0 1rem 2.5rem rgba(0, 0, 0, 0.35); +} + +/* Light headings stay readable over the bright spiral center */ +.centered .about h1, +.centered .about h2 { + text-shadow: 0 1px 3px rgba(0, 0, 0, 0.55); +} + +/* ---- Resume cards & pills ---- */ +.resume-card { + background: rgba(33, 33, 33, 0.6); + -webkit-backdrop-filter: blur(5px); + backdrop-filter: blur(5px); + border: 1px solid rgba(255, 255, 255, 0.06); + border-radius: 0.8rem; + padding: 1.2rem 1.4rem; + margin: 1rem 0; +} + +.resume-meta { + color: #9e9e9e; + font-size: 0.85em; +} + +.resume-period { + color: #9e9e9e; + font-size: 0.85em; + font-weight: 500; + margin: 0 0 0.5rem; +} + +.pill { + display: inline-block; + background: rgba(33, 33, 33, 0.72); + color: #dadada; + border: 1px solid rgba(66, 66, 66, 0.9); + border-radius: 999px; + padding: 0.15rem 0.7rem; + margin: 0.2rem 0.25rem; + font-size: 0.8em; + white-space: nowrap; +} + +/* ---- Projects cards ---- */ +.project-card { + background: rgba(33, 33, 33, 0.6); + -webkit-backdrop-filter: blur(5px); + backdrop-filter: blur(5px); + border: 1px solid rgba(255, 255, 255, 0.06); + border-radius: 0.8rem; + padding: 1.2rem 1.4rem; + margin: 1rem 0; +} + +.project-period { + color: #9e9e9e; + font-size: 0.85em; + font-weight: 500; +} \ No newline at end of file diff --git a/content/_index.md b/content/_index.md new file mode 100644 index 0000000..0674e12 --- /dev/null +++ b/content/_index.md @@ -0,0 +1,7 @@ ++++ +title = "Djosen" ++++ + +Welcome to my personal website. There's a lot going on in my life that I'll be sharing here over time, professional and not. + +Everything is still under construction; I'll be adding updates as I go. \ No newline at end of file diff --git a/content/about.md b/content/about.md new file mode 100644 index 0000000..90be221 --- /dev/null +++ b/content/about.md @@ -0,0 +1,26 @@ ++++ +title = "About" +description = "Who I am and what I do" ++++ + +My name is Ahmed Alaa Ahmed Mohamed (aka Djoser / Djosen / Inedited). I'm a passionate creative software engineer and game developer. + +I hold a master's degree in games engineering from TUM, with a strong background in graphics programming, real-time rendering, XR development, game development, and AI engineering. + +I have experience working on AAA titles (Forza Motorsport 8) with Turn10 Games, as well as indie games and other personal projects. If it involves computers and is fun, I'm in. + +Recently I've been getting into AI Engineering during my work at Siemens, and have developed workflows with Nvidia and Meta XR-AI for industrial operations. + +Besides my professional work, I do some other fun stuff: + +- Video games +- Music production and sound design +- Audio reactive visualizations +- Shader programming +- Sim racing +- Hardware tinkering +- Making more video games and whatnot + +You can reach me at [ineditedofficial@gmail.com](mailto:ineditedofficial@gmail.com) or [djoser@djosen.one](mailto:djoser@djosen.one) or "inedited" on discord. + +It's a little hard to try to dumb everything about me in one page so just reach out to me, let's have a chat! \ No newline at end of file diff --git a/content/posts/_index.md b/content/posts/_index.md new file mode 100644 index 0000000..4348345 --- /dev/null +++ b/content/posts/_index.md @@ -0,0 +1,5 @@ ++++ +title = "Blog" ++++ + +Posts will appear here. \ No newline at end of file diff --git a/content/projects/_index.md b/content/projects/_index.md new file mode 100644 index 0000000..6d201df --- /dev/null +++ b/content/projects/_index.md @@ -0,0 +1,5 @@ ++++ +title = "Projects" ++++ + +This is not an exhaustive list or an indepth, it's more of an attempt to try to document the fact that I build a lot of cool shit that I can't always talk about anywhere else. Some of these are personal projects, some are professional, some are just experiments and prototypes. I will try to keep this updated as I go. \ No newline at end of file diff --git a/content/projects/auv-simulation.md b/content/projects/auv-simulation.md new file mode 100644 index 0000000..67c26c5 --- /dev/null +++ b/content/projects/auv-simulation.md @@ -0,0 +1,12 @@ ++++ +title = "AUV Simulation & Interfacing" +description = "Full physics simulation + ROS + Qt ground control" +date = "2021-01-01" +period = "2021" +tags = ["C++", "ROS", "Gazebo", "Qt5"] +categories = ["programming"] ++++ + +Full AUV physics simulation in Gazebo with ROS nodes using GStreamer to pipeline media from AUV/simulation to an external monitoring interface via a Qt application for the ground control team. + +Designed the simulator, developed the Gazebo plugins and the C++ ROS nodes. \ No newline at end of file diff --git a/content/projects/evol-engine.md b/content/projects/evol-engine.md new file mode 100644 index 0000000..f687a40 --- /dev/null +++ b/content/projects/evol-engine.md @@ -0,0 +1,14 @@ ++++ +title = "Evol Game Engine" +description = "Graduation Project — Vulkan-powered engine" +date = "2021-07-01" +period = "07/2021" +tags = ["C", "C++", "Vulkan", "OpenGL", "ImGUI"] +categories = ["programming"] ++++ + +Developed a high-performance, Vulkan-powered game engine with real-time physics simulation, applicable to vehicle simulation and digital twin technology. + +Built a modular scene editor for real-time rendering and physics-based simulation. + +Implemented the Vulkan renderer for the engine core and an OpenGL PBR renderer for the editor. \ No newline at end of file diff --git a/content/projects/improv-gfx.md b/content/projects/improv-gfx.md new file mode 100644 index 0000000..282c628 --- /dev/null +++ b/content/projects/improv-gfx.md @@ -0,0 +1,12 @@ ++++ +title = "ImprovGFX" +description = "From-scratch rasterizer with OpenCL GPU acceleration" +date = "2021-01-01" +period = "2021" +tags = ["C++", "OpenCL"] +categories = ["programming"] ++++ + +A renderer/rasterizer built from scratch to render .obj models and .tga textures, with OpenCL providing GPU acceleration. + +Built a clean API for constructing scenes and shaders. \ No newline at end of file diff --git a/content/projects/point-cloud.md b/content/projects/point-cloud.md new file mode 100644 index 0000000..754b67e --- /dev/null +++ b/content/projects/point-cloud.md @@ -0,0 +1,14 @@ ++++ +title = "GPU-Driven Point Cloud Renderer" +description = "TUM — 140M+ points at ~100 FPS" +date = "2024-01-01" +period = "2024" +tags = ["C++", "Vulkan", "GLSL"] +categories = ["programming"] ++++ + +Implemented a real-time GPU-driven renderer for large point clouds (140M+ points at ~100 FPS) using a custom 5-stage compute shader pipeline in Vulkan, based on Schütz et al. (2021, 2022). + +Created a software rasterization pipeline using atomic uint64 depth-color packing and compute shaders, enabling fine-grained depth testing without a hardware depth buffer. + +Implemented batch-based frustum culling, Morton-code spatial sorting, and screen-coverage LOD selection via 10-bit quantized position precision. Used Vulkan subgroup extensions for warp-level optimizations. \ No newline at end of file diff --git a/content/projects/radiance-caching.md b/content/projects/radiance-caching.md new file mode 100644 index 0000000..bc1d241 --- /dev/null +++ b/content/projects/radiance-caching.md @@ -0,0 +1,14 @@ ++++ +title = "Radiance Caching for Real-Time Volumetric Rendering" +description = "Master's Thesis — TUM" +date = "2025-12-01" +period = "12/2025 — 06/2026" +tags = ["C++", "CUDA", "Vulkan", "OptiX"] +categories = ["programming"] ++++ + +Researched and implemented a real-time radiance cache for volumetric path tracing using 3D Gaussian Splatting and a fused MLP as the cache representation. + +Developed an online differentiable optimization pipeline in CUDA that adapts the Gaussian cache to dynamic lighting and transfer function changes during rendering. + +Built a spatial-hash-based fused MLP for radiance caching on volumetric heterogeneous media. Supervised by Prof. Westermann. \ No newline at end of file diff --git a/content/resume.md b/content/resume.md new file mode 100644 index 0000000..4337321 --- /dev/null +++ b/content/resume.md @@ -0,0 +1,6 @@ ++++ +title = "Resume" +description = "Ahmed Mohamed — Graphics & Systems Engineer" +type = "resume" +layout = "resume" ++++ \ No newline at end of file diff --git a/data/resume.yaml b/data/resume.yaml new file mode 100644 index 0000000..d483356 --- /dev/null +++ b/data/resume.yaml @@ -0,0 +1,82 @@ +name: "Ahmed Mohamed" +location: "München, Germany" +summary: "Experienced software developer with a strong background in graphics programming, real-time rendering, XR development, Game development and AI engineering. Experienced in developing high-performance applications and specializing in low-level optimizations." +contact: + - label: "ineditedofficial@gmail.com" + href: "mailto:ineditedofficial@gmail.com" + - label: "linkedin.com/in/inedited/" + href: "https://linkedin.com/in/inedited/" + - label: "github.com/inedited" + href: "https://github.com/inedited" + +education: + - role: "Master of Science in Informatics: Games Engineering" + company: "Technical University of Munich (TUM)" + location: "Munich, Germany" + date: "04/2024 — 06/2026" + thesis: "Thesis: Real-time volume path tracing using neural radiance caching and 3DGS Radiance cache." + bullets: [] + + - role: "Bachelor of Science in Software Systems and Computer Engineering" + company: "Ain Shams University" + location: "Cairo, Egypt" + date: "07/2016 — 07/2021" + thesis: "Thesis: 3D game engine in C." + bullets: [] + + - role: "Dual Bachelor of Science in Software Systems and Computer Engineering" + company: "University of East London" + location: "London, United Kingdom" + date: "07/2018 — 07/2021" + bullets: [] + +work: + - role: "Software Developer (Working Student)" + company: "Siemens AG" + location: "Munich, Germany" + date: "04/2025 — Present" + bullets: + - "Led full cycle development across Physical AI, simulation, XR, and robotics integration projects within the Industrial Metaverse Lab." + - "Worked on technical collaboration projects with NVIDIA, Meta, and Amazon AWS for Industrial AI applications." + - "Built XR applications for Meta Quest, Meta RayBan, and Apple Vision Pro, implementing real time XR AI assisted solutions for industrial environments." + - "Worked on robotics simulation and training using NVIDIA IsaacSim, developed ROS integrations, Teleoperation protocols, and deployed trained models to physical robots." + - "Deployed and fine-tuned LLM models for industrial applications and internal tooling." + + - role: "Software Developer" + company: "The Forge ConfettiFX" + location: "California (Remote), USA" + date: "03/2022 — 08/2023" + bullets: + - "Contributed to real-time rendering engine for Forza Motorsport 8 with Turn10 studio, optimizing rendering performance and rewriting shadow rendering system and asset pipelines." + - "Improved The Forge rendering framework visibility buffer implementation, optimizing the graphics pipeline for real time rendering and testing across different consoles." + - "Developed and optimized Linux ports for The Forge rendering framework and optimized the framework for Steam Deck." + - "Redesigned the CI/CD testing pipeline for a Linux-based rendering framework, reducing testing time." + + - role: "Software Developer" + company: "SciChart" + location: "London (Remote), United Kingdom" + date: "01/2022 — 01/2025" + bullets: + - "Developed and optimized the SciChart 3D graphics engine for high performance data visualization, simulation, telemetry, and real time analytics." + - "Ported the 3D graphics engine and WPF Desktop application to Linux." + - "Worked on the development of JavaScript/WebGL version of the 3D graphics engine on top of the C++ core engine." + - "Implemented a Vulkan rendering backend, increasing performance on large scale datasets by about 20% across multiple platforms." + + - role: "Software Developer" + company: "412 Labs" + location: "Cairo, Egypt" + date: "03/2021 — 12/2021" + bullets: + - "Developed VR applications using Unity for different client projects for architectural visualization and internal designs." + - "Taught a class of 20 students on VR development fundamentals using Unity." + - "Built and deployed multiple full stack solutions for 412 Labs projects and clients." + +skills: + - label: "Languages" + skills: ["C", "C++", "C#", "Python", "Swift", "TypeScript", "Kotlin", "GLSL", "HLSL"] + + - label: "Graphics & HPC" + skills: ["Vulkan", "OpenGL", "DirectX", "CUDA", "WebGpu", "Volume Rendering", "Path Tracing"] + + - label: "Other" + skills: ["CI/CD", "AI Agents", "LLM", "ROS", "Unity", "Unreal Engine", "Git", "Docker"] \ No newline at end of file diff --git a/gpu-bg/index.html b/gpu-bg/index.html new file mode 100644 index 0000000..ff4d3d1 --- /dev/null +++ b/gpu-bg/index.html @@ -0,0 +1,7 @@ + + + gpu-bg dev + + + + diff --git a/gpu-bg/package-lock.json b/gpu-bg/package-lock.json new file mode 100644 index 0000000..db88539 --- /dev/null +++ b/gpu-bg/package-lock.json @@ -0,0 +1,1028 @@ +{ + "name": "gpu-bg", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "gpu-bg", + "version": "0.0.0", + "devDependencies": { + "@webgpu/types": "^0.1.40", + "typescript": "^5.4.5", + "vite": "^5.2.11" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.4.tgz", + "integrity": "sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.4.tgz", + "integrity": "sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.4.tgz", + "integrity": "sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.4.tgz", + "integrity": "sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.4.tgz", + "integrity": "sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.4.tgz", + "integrity": "sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.4.tgz", + "integrity": "sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.4.tgz", + "integrity": "sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.4.tgz", + "integrity": "sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.4.tgz", + "integrity": "sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.4.tgz", + "integrity": "sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.4.tgz", + "integrity": "sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.4.tgz", + "integrity": "sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.4.tgz", + "integrity": "sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.4.tgz", + "integrity": "sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.4.tgz", + "integrity": "sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.4.tgz", + "integrity": "sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.4.tgz", + "integrity": "sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.4.tgz", + "integrity": "sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.4.tgz", + "integrity": "sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.4.tgz", + "integrity": "sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.4.tgz", + "integrity": "sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.4.tgz", + "integrity": "sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.4.tgz", + "integrity": "sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.4.tgz", + "integrity": "sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webgpu/types": { + "version": "0.1.72", + "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.72.tgz", + "integrity": "sha512-0cF7RFM2edNoiIS1ODJp0/Gzv4/xSXhwoR0YCza+OWpJWtn4wmo9DvK91aLlH9+uUnwIriP7ZiC3WitmyhuzBw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.4.tgz", + "integrity": "sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.62.4", + "@rollup/rollup-android-arm64": "4.62.4", + "@rollup/rollup-darwin-arm64": "4.62.4", + "@rollup/rollup-darwin-x64": "4.62.4", + "@rollup/rollup-freebsd-arm64": "4.62.4", + "@rollup/rollup-freebsd-x64": "4.62.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.4", + "@rollup/rollup-linux-arm-musleabihf": "4.62.4", + "@rollup/rollup-linux-arm64-gnu": "4.62.4", + "@rollup/rollup-linux-arm64-musl": "4.62.4", + "@rollup/rollup-linux-loong64-gnu": "4.62.4", + "@rollup/rollup-linux-loong64-musl": "4.62.4", + "@rollup/rollup-linux-ppc64-gnu": "4.62.4", + "@rollup/rollup-linux-ppc64-musl": "4.62.4", + "@rollup/rollup-linux-riscv64-gnu": "4.62.4", + "@rollup/rollup-linux-riscv64-musl": "4.62.4", + "@rollup/rollup-linux-s390x-gnu": "4.62.4", + "@rollup/rollup-linux-x64-gnu": "4.62.4", + "@rollup/rollup-linux-x64-musl": "4.62.4", + "@rollup/rollup-openbsd-x64": "4.62.4", + "@rollup/rollup-openharmony-arm64": "4.62.4", + "@rollup/rollup-win32-arm64-msvc": "4.62.4", + "@rollup/rollup-win32-ia32-msvc": "4.62.4", + "@rollup/rollup-win32-x64-gnu": "4.62.4", + "@rollup/rollup-win32-x64-msvc": "4.62.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + } + } +} diff --git a/gpu-bg/package.json b/gpu-bg/package.json new file mode 100644 index 0000000..1908df5 --- /dev/null +++ b/gpu-bg/package.json @@ -0,0 +1,17 @@ +{ + "name": "gpu-bg", + "private": true, + "type": "module", + "version": "0.0.0", + "scripts": { + "dev": "vite", + "build": "tsc --noEmit && vite build", + "typecheck": "tsc --noEmit", + "test": "tsc --noEmit" + }, + "devDependencies": { + "@webgpu/types": "^0.1.40", + "typescript": "^5.4.5", + "vite": "^5.2.11" + } +} diff --git a/gpu-bg/src/gl-renderer.ts b/gpu-bg/src/gl-renderer.ts new file mode 100644 index 0000000..795cea7 --- /dev/null +++ b/gpu-bg/src/gl-renderer.ts @@ -0,0 +1,127 @@ +import glslSource from './shaders/fullscreen.glsl?raw' +import type { UiState } from './renderer' +import type { RendererLike } from './types' + +const UNIFORM_FLOATS = 48 +const UNIFORM_BYTES = UNIFORM_FLOATS * 4 + +function compileShader(gl: WebGL2RenderingContext, type: number, source: string): WebGLShader | null { + const shader = gl.createShader(type) + if (!shader) return null + gl.shaderSource(shader, source) + gl.compileShader(shader) + if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { + console.error('GL shader compile error:', gl.getShaderInfoLog(shader)) + gl.deleteShader(shader) + return null + } + return shader +} + +function linkProgram(gl: WebGL2RenderingContext, vs: WebGLShader, fs: WebGLShader): WebGLProgram | null { + const program = gl.createProgram() + if (!program) return null + gl.attachShader(program, vs) + gl.attachShader(program, fs) + gl.linkProgram(program) + if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { + console.error('GL program link error:', gl.getProgramInfoLog(program)) + gl.deleteProgram(program) + return null + } + return program +} + +function splitShaderSource(source: string): { vs: string; fs: string } { + const parts = source.split('//---FRAGMENT---') + if (parts.length !== 2) throw new Error('GLSL source must contain //---VERTEX--- and //---FRAGMENT--- markers') + const vs = parts[0].replace('//---VERTEX---', '').trim() + const fs = parts[1].trim() + return { vs, fs } +} + +export class GlRenderer implements RendererLike { + private gl: WebGL2RenderingContext | null = null + private program: WebGLProgram | null = null + private ubo: WebGLBuffer | null = null + private uniformData = new Float32Array(UNIFORM_FLOATS) + private _ready = false + + get ready(): boolean { + return this._ready + } + + async init(canvas: HTMLCanvasElement): Promise { + const gl = canvas.getContext('webgl2', { + alpha: false, + antialias: false, + depth: false, + stencil: false, + preserveDrawingBuffer: false, + }) + if (!gl) return false + + this.gl = gl + + const { vs: vsSrc, fs: fsSrc } = splitShaderSource(glslSource) + + const vs = compileShader(gl, gl.VERTEX_SHADER, vsSrc) + if (!vs) return false + + const fs = compileShader(gl, gl.FRAGMENT_SHADER, fsSrc) + if (!fs) { + gl.deleteShader(vs) + return false + } + + const program = linkProgram(gl, vs, fs) + gl.deleteShader(vs) + gl.deleteShader(fs) + if (!program) return false + + this.program = program + + const ubo = gl.createBuffer() + if (!ubo) return false + gl.bindBuffer(gl.UNIFORM_BUFFER, ubo) + gl.bufferData(gl.UNIFORM_BUFFER, UNIFORM_BYTES, gl.DYNAMIC_DRAW) + + const blockIndex = gl.getUniformBlockIndex(program, 'Uniforms') + if (blockIndex === gl.INVALID_INDEX) { + console.error('Uniform block "Uniforms" not found in GLSL program') + return false + } + gl.uniformBlockBinding(program, blockIndex, 0) + gl.bindBufferBase(gl.UNIFORM_BUFFER, 0, ubo) + + this.ubo = ubo + this._ready = true + return true + } + + resize(): void { + if (!this.gl) return + this.gl.viewport(0, 0, this.gl.canvas.width, this.gl.canvas.height) + } + + render(state: UiState): void { + const gl = this.gl + if (!gl || !this.ubo || !this._ready) return + + const d = this.uniformData + d[0] = state.resolution[0] + d[1] = state.resolution[1] + d[2] = state.time + d[3] = state.scene + d[4] = state.mouse[0] + d[5] = state.mouse[1] + d[8] = state.dim + d.set(state.stemMultipliers, 16) + + gl.bindBuffer(gl.UNIFORM_BUFFER, this.ubo) + gl.bufferSubData(gl.UNIFORM_BUFFER, 0, this.uniformData) + + gl.useProgram(this.program) + gl.drawArrays(gl.TRIANGLES, 0, 3) + } +} diff --git a/gpu-bg/src/input.ts b/gpu-bg/src/input.ts new file mode 100644 index 0000000..92179b3 --- /dev/null +++ b/gpu-bg/src/input.ts @@ -0,0 +1,14 @@ +// Window-level pointer position capture. Discrete clicks and hover are +// handled by the DOM overlay elements directly; this class only feeds the +// continuous pointer position that the background shader consumes. +export class Input { + x = 0 + y = 0 + + constructor() { + window.addEventListener('pointermove', (e) => { + this.x = e.clientX + this.y = e.clientY + }) + } +} \ No newline at end of file diff --git a/gpu-bg/src/main.ts b/gpu-bg/src/main.ts new file mode 100644 index 0000000..3aa7044 --- /dev/null +++ b/gpu-bg/src/main.ts @@ -0,0 +1,68 @@ +import { Renderer, UiState } from './renderer' +import { GlRenderer } from './gl-renderer' +import type { RendererLike } from './types' +import { Input } from './input' + +const DPR_CAP = 2 + +const canvas = document.getElementById('gpu') as HTMLCanvasElement | null +if (canvas) void boot(canvas) + +async function boot(canvas: HTMLCanvasElement): Promise { + const input = new Input() + + const dpr = 1 + const res = new Float32Array(2) + const mouse = new Float32Array(2) + const stemMultipliers = new Float32Array(32) + + let renderer: RendererLike = new Renderer() + let ok = await renderer.init(canvas) + if (!ok) { + renderer = new GlRenderer() + ok = await renderer.init(canvas) + } + if (!ok) { + canvas.style.display = 'none' + return + } + + const startTime = performance.now() + + const resize = (): void => { + const d = Math.min(window.devicePixelRatio || 1, DPR_CAP) + const w = Math.floor(window.innerWidth * d) + const h = Math.floor(window.innerHeight * d) + if (canvas.width !== w || canvas.height !== h) { + canvas.width = w + canvas.height = h + res[0] = w + res[1] = h + if (renderer.ready) renderer.resize() + } + } + + resize() + + const loop = (): void => { + requestAnimationFrame(loop) + const time = (performance.now() - startTime) / 1000 + + resize() + + mouse[0] = input.x * Math.min(window.devicePixelRatio || 1, DPR_CAP) + mouse[1] = res[1] - input.y * Math.min(window.devicePixelRatio || 1, DPR_CAP) + + const state: UiState = { + resolution: res, + time, + scene: 0, + mouse, + dim: 0.0, + stemMultipliers, + } + renderer.render(state) + } + + loop() +} diff --git a/gpu-bg/src/renderer.ts b/gpu-bg/src/renderer.ts new file mode 100644 index 0000000..61a9f36 --- /dev/null +++ b/gpu-bg/src/renderer.ts @@ -0,0 +1,105 @@ +import shaderSource from './shaders/fullscreen.wgsl?raw' + +const UNIFORM_FLOATS = 48 // res(2), time, scene, mouse(2), pad(2), date(4), sampleRate(4), stemMultipliers(32) +const UNIFORM_BYTES = UNIFORM_FLOATS * 4 + +export interface UiState { + resolution: Float32Array + time: number + scene: number + mouse: Float32Array + dim: number + stemMultipliers: Float32Array +} + +export class Renderer { + private device!: GPUDevice + private context!: GPUCanvasContext + private format!: GPUTextureFormat + private pipeline!: GPURenderPipeline + private uniformBuffer!: GPUBuffer + private bindGroup!: GPUBindGroup + private uniformData = new Float32Array(UNIFORM_FLOATS) + private configured = false + + get ready(): boolean { + return this.configured + } + + async init(canvas: HTMLCanvasElement): Promise { + if (!navigator.gpu) return false + const adapter = await navigator.gpu.requestAdapter() + if (!adapter) return false + this.device = await adapter.requestDevice() + this.context = canvas.getContext('webgpu') as GPUCanvasContext + this.format = navigator.gpu.getPreferredCanvasFormat() + this.configure() + + const module = this.device.createShaderModule({ code: shaderSource }) + this.pipeline = this.device.createRenderPipeline({ + layout: 'auto', + vertex: { module, entryPoint: 'vs' }, + fragment: { module, entryPoint: 'fs', targets: [{ format: this.format }] }, + primitive: { topology: 'triangle-list' } + }) + + this.uniformBuffer = this.device.createBuffer({ + size: UNIFORM_BYTES, + usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST + }) + this.bindGroup = this.device.createBindGroup({ + layout: this.pipeline.getBindGroupLayout(0), + entries: [{ binding: 0, resource: { buffer: this.uniformBuffer } }] + }) + + this.device.lost.then((info) => { console.error('WebGPU device lost:', info); this.configured = false }) + this.device.onuncapturederror = (ev: GPUUncapturedErrorEvent) => { + console.error('WebGPU uncaptured error:', ev.error) + } + this.configured = true + return true + } + + configure(): void { + this.context.configure({ + device: this.device, + format: this.format, + alphaMode: 'opaque' + }) + } + + resize(): void { + this.configure() + } + + render(state: UiState): void { + if (!this.configured) return + const d = this.uniformData + d[0] = state.resolution[0] + d[1] = state.resolution[1] + d[2] = state.time + d[3] = state.scene + d[4] = state.mouse[0] + d[5] = state.mouse[1] + // d[6], d[7]: padding — left as zero + d[8] = state.dim // u.date.x — dim factor for non-home scenes + // d[12]-d[15]: sampleRate — left as zero + d.set(state.stemMultipliers, 16) + this.device.queue.writeBuffer(this.uniformBuffer, 0, this.uniformData) + + const encoder = this.device.createCommandEncoder() + const view = this.context.getCurrentTexture().createView() + + const bgPass = encoder.beginRenderPass({ + colorAttachments: [ + { view, clearValue: { r: 0, g: 0, b: 0, a: 1 }, loadOp: 'clear', storeOp: 'store' } + ] + }) + bgPass.setPipeline(this.pipeline) + bgPass.setBindGroup(0, this.bindGroup) + bgPass.draw(3) + bgPass.end() + + this.device.queue.submit([encoder.finish()]) + } +} \ No newline at end of file diff --git a/gpu-bg/src/shaders/fullscreen.glsl b/gpu-bg/src/shaders/fullscreen.glsl new file mode 100644 index 0000000..983bf86 --- /dev/null +++ b/gpu-bg/src/shaders/fullscreen.glsl @@ -0,0 +1,110 @@ +//---VERTEX--- +#version 300 es + +const vec2 POSITIONS[3] = vec2[]( + vec2(-1.0, -1.0), + vec2(-1.0, 3.0), + vec2( 3.0, -1.0) +); + +out vec2 vUv; + +void main() { + vec2 pos = POSITIONS[gl_VertexID]; + gl_Position = vec4(pos, 0.0, 1.0); + vUv = pos; +} + +//---FRAGMENT--- +#version 300 es +precision highp float; + +layout(std140) uniform Uniforms { + vec2 resolution; + float time; + float scene; + vec2 mouse; + vec4 date; + vec4 sampleRate; + vec4 stemMultipliers[8]; +}; + +in vec2 vUv; +out vec4 outColor; + +void main() { + vec2 fragCoord = (vUv * 0.5 + 0.5) * resolution; + vec2 uv = (fragCoord - 0.5 * resolution) / resolution.y; + + float slowTime = time * 0.2; + + vec3 bgGrey = vec3(0.11, 0.11, 0.12); + vec3 cardGrey = vec3(0.30, 0.32, 0.36); + vec3 coreWhite = vec3(0.90, 0.95, 0.90); + + vec3 scene0 = vec3(0.20, 0.75, 0.20); + vec3 scene1 = vec3(0.85, 0.55, 0.20); + vec3 scene2 = vec3(0.25, 0.60, 0.95); + vec3 scene3 = vec3(0.90, 0.35, 0.55); + + int sFloor = int(clamp(floor(scene), 0.0, 3.0)); + int sCeil = int(clamp(ceil(scene), 0.0, 3.0)); + + vec3 c0; + if (sFloor == 0) c0 = scene0; + else if (sFloor == 1) c0 = scene1; + else if (sFloor == 2) c0 = scene2; + else c0 = scene3; + + vec3 c1; + if (sCeil == 0) c1 = scene0; + else if (sCeil == 1) c1 = scene1; + else if (sCeil == 2) c1 = scene2; + else c1 = scene3; + + float tBlend = scene - floor(scene); + float smoothT = tBlend * tBlend * (3.0 - 2.0 * tBlend); + vec3 sceneAccent = mix(c0, c1, smoothT); + + vec2 mousePos = (mouse - 0.5 * resolution) / resolution.y; + float distMouse = length(uv - mousePos); + float influence = exp(-distMouse * 7.0); + uv += (mousePos - uv) * influence * 0.12; + + float angle = atan(uv.y, uv.x); + float radius = length(uv); + + float baseSpiral = angle + (radius * 8.0) - slowTime; + float ripple = sin(radius * 15.0 - (time * 0.5)) * 0.08; + float distortedSpiral = baseSpiral + ripple; + + float armCount = 2.0; + float rChannel = smoothstep(0.05, 0.55, abs(sin((distortedSpiral + 0.04) * armCount))); + float gChannel = smoothstep(0.05, 0.55, abs(sin(distortedSpiral * armCount))); + float bChannel = smoothstep(0.05, 0.55, abs(sin((distortedSpiral - 0.04) * armCount))); + vec3 patternChannels = vec3(rChannel, gChannel, bChannel); + + vec3 coreNeon = vec3(0.02 / (patternChannels + 0.025)); + vec3 spiralColor = mix(cardGrey, sceneAccent, patternChannels.g); + vec3 primarySpiral = coreNeon * spiralColor; + + float ringWave = sin(radius * 12.0 - (time * 1.5)); + float sharpRing = smoothstep(0.85, 0.95, ringWave); + float ringMask = sharpRing * smoothstep(0.7, 0.1, radius); + vec3 secondaryRings = cardGrey * ringMask * 0.4; + + float vignette = smoothstep(0.95, 0.4, radius); + + vec3 finalColor = bgGrey + secondaryRings; + finalColor = mix(finalColor, primarySpiral, 0.85); + + float centerLight = 0.015 / (radius + 0.02); + finalColor += centerLight * coreWhite; + + finalColor += exp(-distMouse * 4.0) * 0.03 * sceneAccent; + + float dim = date.x; + finalColor = mix(finalColor, vec3(0.0), dim); + + outColor = vec4(finalColor * vignette, 1.0); +} diff --git a/gpu-bg/src/shaders/fullscreen.wgsl b/gpu-bg/src/shaders/fullscreen.wgsl new file mode 100644 index 0000000..8c15fb1 --- /dev/null +++ b/gpu-bg/src/shaders/fullscreen.wgsl @@ -0,0 +1,108 @@ +struct Uniforms { + resolution: vec2, + time: f32, + scene: f32, + mouse: vec2, + date: vec4, + sampleRate: vec4, + stemMultipliers: array, 8>, +}; + +@group(0) @binding(0) var u: Uniforms; + +struct VsOut { + @builtin(position) pos: vec4, + @location(0) uv: vec2, +}; + +@vertex +fn vs(@builtin(vertex_index) i: u32) -> VsOut { + let p = array, 3>( + vec2(-1.0, -3.0), + vec2(-1.0, 1.0), + vec2(3.0, 1.0), + ); + let clip = p[i]; + var out: VsOut; + out.pos = vec4(clip, 0.0, 1.0); + out.uv = clip; + return out; +} + +@fragment +fn fs(in: VsOut) -> @location(0) vec4 { + let res = u.resolution; + let fragCoord = (in.uv * vec2(0.5) + vec2(0.5)) * res; + + var uv = (fragCoord - 0.5 * res) / res.y; + + let slowTime = u.time * 0.2; + + let bgGrey = vec3(0.11, 0.11, 0.12); + let cardGrey = vec3(0.30, 0.32, 0.36); + let coreWhite = vec3(0.90, 0.95, 0.90); + + // ---------- palette keyed by scene ---------- + let scene0 = vec3(0.20, 0.75, 0.20); // HERO – emerald + let scene1 = vec3(0.85, 0.55, 0.20); // ABOUT – amber + let scene2 = vec3(0.25, 0.60, 0.95); // PROJECTS – cobalt + let scene3 = vec3(0.90, 0.35, 0.55); // CONTACT – rose + + let sFloor = i32(clamp(floor(u.scene), 0.0, 3.0)); + let sCeil = i32(clamp(ceil(u.scene), 0.0, 3.0)); + var c0 = scene0; + c0 = select(c0, scene1, sFloor == 1); + c0 = select(c0, scene2, sFloor == 2); + c0 = select(c0, scene3, sFloor == 3); + var c1 = scene0; + c1 = select(c1, scene1, sCeil == 1); + c1 = select(c1, scene2, sCeil == 2); + c1 = select(c1, scene3, sCeil == 3); + let tBlend = u.scene - floor(u.scene); + let sceneAccent = mix(c0, c1, tBlend * tBlend * (3.0 - 2.0 * tBlend)); + + // ---------- mouse-reactive distortion ---------- + let mousePos = (u.mouse - 0.5 * res) / res.y; + let distMouse = length(uv - mousePos); + let influence = exp(-distMouse * 7.0); + uv += (mousePos - uv) * influence * 0.12; + + let angle = atan2(uv.y, uv.x); + let radius = length(uv); + + let baseSpiral = angle + (radius * 8.0) - slowTime; + let ripple = sin(radius * 15.0 - (u.time * 0.5)) * 0.08; + let distortedSpiral = baseSpiral + ripple; + + let armCount = 2.0; + let rChannel = smoothstep(0.05, 0.55, abs(sin((distortedSpiral + 0.04) * armCount))); + let gChannel = smoothstep(0.05, 0.55, abs(sin(distortedSpiral * armCount))); + let bChannel = smoothstep(0.05, 0.55, abs(sin((distortedSpiral - 0.04) * armCount))); + let patternChannels = vec3(rChannel, gChannel, bChannel); + + let coreNeon = vec3(0.02 / (patternChannels + 0.025)); + let spiralColor = mix(cardGrey, sceneAccent, patternChannels.g); + let primarySpiral = coreNeon * spiralColor; + + let ringWave = sin(radius * 12.0 - (u.time * 1.5)); + let sharpRing = smoothstep(0.85, 0.95, ringWave); + let ringMask = sharpRing * smoothstep(0.7, 0.1, radius); + let secondaryRings = cardGrey * ringMask * 0.4; + + let vignette = smoothstep(0.95, 0.4, radius); + + var finalColor = bgGrey + secondaryRings; + finalColor = mix(finalColor, primarySpiral, 0.85); + + let centerLight = 0.015 / (radius + 0.02); + finalColor += centerLight * coreWhite; + + // soft cursor glow + finalColor += exp(-distMouse * 4.0) * 0.03 * sceneAccent; + + // ---------- dim when not on the home page (u.date.x controls it) ---------- + let dim = u.date.x; + finalColor = mix(finalColor, vec3(0.0), dim); + + return vec4(finalColor * vignette, 1.0); +} diff --git a/gpu-bg/src/types.ts b/gpu-bg/src/types.ts new file mode 100644 index 0000000..6bf16da --- /dev/null +++ b/gpu-bg/src/types.ts @@ -0,0 +1,8 @@ +import type { UiState } from './renderer' + +export interface RendererLike { + readonly ready: boolean + init(canvas: HTMLCanvasElement): Promise + render(state: UiState): void + resize(): void +} diff --git a/gpu-bg/src/vite-env.d.ts b/gpu-bg/src/vite-env.d.ts new file mode 100644 index 0000000..a96e866 --- /dev/null +++ b/gpu-bg/src/vite-env.d.ts @@ -0,0 +1,4 @@ +declare module '*?raw' { + const src: string + export default src +} \ No newline at end of file diff --git a/gpu-bg/tsconfig.json b/gpu-bg/tsconfig.json new file mode 100644 index 0000000..8d769d4 --- /dev/null +++ b/gpu-bg/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "strict": true, + "types": ["@webgpu/types"] + }, + "include": ["src"] +} diff --git a/gpu-bg/vite.config.ts b/gpu-bg/vite.config.ts new file mode 100644 index 0000000..64926eb --- /dev/null +++ b/gpu-bg/vite.config.ts @@ -0,0 +1,16 @@ +import { defineConfig } from "vite"; +export default defineConfig({ + server: { open: true }, + build: { + target: "esnext", + outDir: "dist", + assetsInlineLimit: 100000000, + rollupOptions: { + output: { + entryFileNames: "engine.js", + assetFileNames: "engine_[name].[hash][extname]" + } + } + }, + assetsInclude: ["**/*.glsl", "**/*.wgsl"] +}); diff --git a/hugo.toml b/hugo.toml new file mode 100644 index 0000000..b6dfc3d --- /dev/null +++ b/hugo.toml @@ -0,0 +1,75 @@ +baseURL = "https://djosen.one/" +title = "Djosen" +theme = "hugo-coder" +locale = "en" +enableEmoji = true + +[pagination] +pagerSize = 6 + +[markup.highlight] +noClasses = false + +[markup.goldmark.renderer] +unsafe = true + +[markup.goldmark.extensions.typographer] +disable = true + +[taxonomies] +category = "categories" +series = "series" +tag = "tags" +author = "authors" + +[params] +author = "Djosen" +description = "Djosen's personal website" +keywords = "graphics,rendering,webgpu,gamedev,software engineering" +info = ["Creative Developer | Game Developer | Software Engineer"] +colorScheme = "dark" +hideColorSchemeToggle = true +customCSS = ["css/custom.css"] + +[[params.social]] +name = "GitHub" +icon = "fa-brands fa-github fa-2x" +weight = 1 +url = "https://github.com/inedited" + +[[params.social]] +name = "LinkedIn" +icon = "fa-brands fa-linkedin fa-2x" +weight = 2 +url = "https://linkedin.com/in/inedited/" + +[[params.social]] +name = "Email" +icon = "fa-solid fa-envelope fa-2x" +weight = 3 +url = "mailto:ineditedofficial@gmail.com" + +[[menu.main]] +name = "Home" +weight = 1 +url = "/" + +[[menu.main]] +name = "About" +weight = 2 +url = "about/" + +[[menu.main]] +name = "Projects" +weight = 3 +url = "projects/" + +[[menu.main]] +name = "Resume" +weight = 4 +url = "resume/" + +[[menu.main]] +name = "Blog" +weight = 5 +url = "posts/" \ No newline at end of file diff --git a/layouts/home.html b/layouts/home.html new file mode 100644 index 0000000..c135b90 --- /dev/null +++ b/layouts/home.html @@ -0,0 +1,11 @@ +{{ define "content" }} + {{ partial "gpu-background.html" . }} + {{ partial "home.html" . }} + {{ if .Content }} +
+
+ {{ .Content }} +
+
+ {{ end }} +{{ end }} \ No newline at end of file diff --git a/layouts/partials/gpu-background.html b/layouts/partials/gpu-background.html new file mode 100644 index 0000000..90a0f29 --- /dev/null +++ b/layouts/partials/gpu-background.html @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/layouts/projects/list.html b/layouts/projects/list.html new file mode 100644 index 0000000..6e3a80a --- /dev/null +++ b/layouts/projects/list.html @@ -0,0 +1,37 @@ +{{ define "title" }} + {{ .Title }} · {{ .Site.Title }} +{{ end }} +{{ define "content" }} +
+
+

+ + {{ .Title }} + +

+
+ + {{ .Content }} + + {{ range .Pages.ByDate.Reverse }} +
+ {{ if .Params.period }} +

{{ .Params.period }}

+ {{ end }} +

+ {{ .Title }} +

+ {{ if .Params.description }} +

{{ .Params.description }}

+ {{ end }} + {{ if .Params.tags }} +

+ {{ range .Params.tags }} + {{ . }} + {{ end }} +

+ {{ end }} +
+ {{ end }} +
+{{ end }} \ No newline at end of file diff --git a/layouts/resume/single.html b/layouts/resume/single.html new file mode 100644 index 0000000..a5e876b --- /dev/null +++ b/layouts/resume/single.html @@ -0,0 +1,68 @@ +{{ define "title" }} + {{ .Title }} · {{ .Site.Title }} +{{ end }} +{{ define "content" }} +{{ with hugo.Data.resume }} +
+
+
+

{{ .name }}

+

+ {{ range $i, $c := .contact }} + {{ if $i }} · {{ end }} + {{ $c.label }} + {{ end }} +

+

{{ .location }}

+
+ +
+

Summary

+

{{ .summary }}

+
+ +
+

Work Experience

+ {{ range .work }} +
+

{{ .date }}

+

{{ .role }}

+

{{ .company }} — {{ .location }}

+
    + {{ range .bullets }} +
  • {{ . }}
  • + {{ end }} +
+
+ {{ end }} +
+ +
+

Education

+ {{ range .education }} +
+

{{ .date }}

+

{{ .role }}

+

{{ .company }} — {{ .location }}

+ {{ if .thesis }} +

{{ .thesis }}

+ {{ end }} +
+ {{ end }} +
+ +
+

Skills

+ {{ range .skills }} +

{{ .label }}

+

+ {{ range .skills }} + {{ . }} + {{ end }} +

+ {{ end }} +
+
+
+{{ end }} +{{ end }} \ No newline at end of file diff --git a/nginx.default.conf b/nginx.default.conf index e5c59fb..00fd330 100644 --- a/nginx.default.conf +++ b/nginx.default.conf @@ -5,26 +5,26 @@ server { root /usr/share/nginx/html; index index.html; - # Security headers add_header X-Content-Type-Options "nosniff" always; add_header X-Frame-Options "DENY" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; - # Enable compression gzip on; gzip_types text/css application/javascript text/javascript application/wasm image/svg+xml; gzip_min_length 512; - # Immutable cache for hashed assets - location /assets/ { - expires 1y; - add_header Cache-Control "public, immutable"; - } + # fingerprint-hashed Hugo assets (css/js) and the engine bundle: immutable + location /css/ { expires 1y; add_header Cache-Control "public, immutable"; } + location /js/ { expires 1y; add_header Cache-Control "public, immutable"; } + location /gpu/ { expires 1y; add_header Cache-Control "public, immutable"; } - # Everything else → index.html (SPA fallback) + # static fonts/favicons + location /fonts/ { expires 1y; add_header Cache-Control "public, immutable"; } + + # everything else: plain static — real files, 404 is legitimate (no SPA fallback) location / { - try_files $uri $uri/ /index.html; + try_files $uri $uri/ =404; expires -1; add_header Cache-Control "no-cache"; } -} +} \ No newline at end of file diff --git a/themes/hugo-coder b/themes/hugo-coder new file mode 160000 index 0000000..58d956f --- /dev/null +++ b/themes/hugo-coder @@ -0,0 +1 @@ +Subproject commit 58d956f41c056a12e15d5b6ee27c54fc98cd6e49