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
This commit is contained in:
2026-08-21 03:39:42 +02:00
parent 3ad5cf55e3
commit 9ea85db5ce
74 changed files with 3170 additions and 75 deletions

View File

@@ -1,12 +1,10 @@
node_modules
dist
.git .git
.gitignore .hermes
*.md archive
.env public
.env.example resources
nginx/ gpu-bg/node_modules
webhook/ gpu-bg/dist
scripts/ node_modules
docker-compose.yml *.log
.hermes/ .env

10
.gitignore vendored
View File

@@ -20,3 +20,13 @@ resume.tex
# CI/CD — webhook secret + deployment env # CI/CD — webhook secret + deployment env
.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/

3
.gitmodules vendored Normal file
View File

@@ -0,0 +1,3 @@
[submodule "themes/hugo-coder"]
path = themes/hugo-coder
url = https://github.com/luizdepra/hugo-coder

View File

@@ -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<boolean>
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<vec4<f32>, 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<boolean> {
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<void> {
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
<div id="fallback">
<div>
<h2>Graphics not available</h2>
<p>Your browser doesn't support WebGPU or WebGL. Open this page in a recent Chrome, Edge, or Firefox.</p>
</div>
</div>
```
**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.

View File

@@ -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 # <canvas> + <script src=/gpu/engine.js>
│ ├── resume/
│ │ └── single.html # data-driven resume renderer
│ └── projects/
│ └── list.html # shows period + description + tags (if needed)
├── static/
│ ├── css/custom.css # canvas-behind-content + dark glass styling
│ └── gpu/ # engine build output (gitignored)
│ └── engine.js (+ hashed assets)
├── gpu-bg/ # engine sub-package (Vite + TS)
│ ├── package.json
│ ├── tsconfig.json
│ ├── vite.config.ts
│ ├── index.html # local dev harness only
│ └── src/
│ ├── main.ts # slim bootstrap (replaces app.ts + main.ts)
│ ├── renderer.ts # WebGPU (copied, unmodified)
│ ├── gl-renderer.ts # WebGL2 fallback (copied, unmodified)
│ ├── types.ts # RendererLike (copied)
│ ├── input.ts # pointer tracking (copied)
│ └── shaders/
│ ├── fullscreen.wgsl # UNCHANGED
│ └── fullscreen.glsl # UNCHANGED
├── archive/ # old SPA (per decision 12)
│ ├── index.html
│ ├── src/ # app.ts, router.ts, ui/overlay.ts, data/, engine/text/*
│ ├── package.json / package-lock.json / tsconfig.json / vite.config.ts
├── themes/hugo-coder/ # git submodule
├── Dockerfile # 3-stage: node → hugo(ext) → nginx
├── docker-compose.yml # UNCHANGED
├── nginx.default.conf # drop SPA fallback → plain static
├── DEPLOYMENT.md # handover (section 9 below)
├── .gitmodules
└── .gitignore # + public/, static/gpu/, resources/
```
---
## 3. Engine extraction — what survives vs. is archived
**Survives into `gpu-bg/` (copied unmodified):**
- `engine/renderer.ts` (WebGPU) — `UiState` interface lives here
- `engine/gl-renderer.ts` (WebGL2 fallback)
- `engine/types.ts` (`RendererLike`)
- `engine/shaders/fullscreen.wgsl` + `fullscreen.glsl` — **shader math stays byte-identical**
- `ui/input.ts` (pointer tracking; tiny, feeds `state.mouse`)
**New file — slim bootstrap `gpu-bg/src/main.ts` (replaces app.ts + main.ts):**
- Look up `#gpu` canvas; if absent, no-op (partial only included on homepage).
- `init()` WebGPU → fallback WebGL2; only if both fail, hide canvas (no separate #fallback UI — Hugo content still shows).
- `resize()` + DPR cap 2 (reuse logic from app.ts).
- `requestAnimationFrame` loop: feed `resolution`, `time`, `mouse`, and **fixed uniforms**: `scene = 0`, `dim = 0.0`, zeroed `stemMultipliers` (32 floats).
- No router, no overlay, no scene switching. The `scene`/`dim`/`stemMultipliers` plumbing is collapsed to constants at the bootstrap boundary — the renderer and shader remain untouched.
**Archived into `archive/` (deleted from active tree):**
- `app.ts`, `main.ts` (superseded by slim bootstrap)
- `router.ts` (Hugo does routing)
- `ui/overlay.ts` (Hugo renders content)
- `data/**` (all content → Hugo; see section 4)
- `engine/text/**` (text-renderer, text-layout, slug-generator — confirmed orphaned: only self-references, nothing imports them)
- root `index.html`, `package.json`, `package-lock.json`, `tsconfig.json`, `vite.config.ts` (move to `archive/`; new engine gets its own under `gpu-bg/`)
**Note on shaders:** the WGSL/GLSL scene-palette `if/else` chain will now always resolve to `scene0` (emerald). Do NOT simplify the shader during this transition — it risks introducing bugs in working math. Flag as an optional later cleanup.
---
## 4. Content migration map
| Source | Destination | Notes |
|--------|-------------|-------|
| `pages.ts` home entry + `scenes/hero.ts` | `content/_index.md` + `params.info` | tagline → `params.info = "Creative Developer | Game Developer | Software Engineer"`; welcome body → `_index.md` |
| `scenes/about.ts` | `content/about.md` | Convert HTML `<p>/<ul>/<li>/<a>` → Markdown. Content: aliases, TUM masters, graphics/XR/games/AI background, Forza/Turn10, Siemens AI, hobbies list, emails + discord |
| `scenes/contact.ts` | — (drop) | It's a placeholder (`hello@example.com`); real contact lives in about + social config |
| `scenes/projects.ts` (intro) | `content/projects/_index.md` | Keep the "not an exhaustive list…" intro text |
| `projects/index.ts` categories | taxonomy config | `categories` = programming, hardware, visuals, music (empty ones become invisible terms until populated) |
| `projects/radiance-caching.ts` | `content/projects/radiance-caching.md` | frontmatter below |
| `projects/point-cloud.ts` | `content/projects/point-cloud.md` | " |
| `projects/evol-engine.ts` | `content/projects/evol-engine.md` | " |
| `projects/auv-simulation.ts` | `content/projects/auv-simulation.md` | " |
| `projects/improv-gfx.ts` | `content/projects/improv-gfx.md` | " |
| `resume.ts` | `data/resume.yaml` + `layouts/resume/single.html` | full structure preserved |
| `scenes/resume.ts` (title/subtitle) | `content/resume.md` frontmatter | title "Resume" |
**Project frontmatter shape:**
```toml
+++
title = "<name>"
description = "<tagline>"
date = "<parseable date>" # for Hugo ordering
period = "<human range>" # e.g. "12/2025 — 06/2026"; source dates are ranges/years
tags = ["<tech...>"] # tech[] → tags
categories = ["programming"] # all 5 currently live in "programming"
+++
<markdown body from content>
```
**Date handling:** source `date` values are ranges (`12/2025 — 06/2026`) or bare years (`2024`, `2021`). Hugo needs a real date for list ordering — set `date` to the range end (or a sane parseable date) and preserve the human string in a custom `period` frontmatter field, displayed by the projects list template. `tech` → `tags`, category → `categories`.
**Resume name divergence (intentional):** site identity = "Djosen" (decision 5), but `data/resume.yaml` keeps `name: "Ahmed Mohamed"` — a CV must carry the real name. Flag if you want otherwise.
---
## 5. Theme setup (hugo-coder, dark)
- `git submodule add https://github.com/luizdepra/hugo-coder themes/hugo-coder`
- **Requires Hugo extended** (theme compiles SCSS via Hugo Pipes).
- `config.toml` key points:
- `baseURL = "https://djosen.one/"`
- `title = "Djosen"`, `theme = "hugo-coder"`
- `params.author = "Djosen"`, `params.info = "<tagline>"`, `params.description`, `params.keywords`
- `params.colorScheme = "dark"` (pin dark; user wants dark overall)
- `params.avatarURL = ""` (no avatar for now — leave off; add later)
- `params.social`: github (`github.com/inedited`), linkedin (`linkedin.com/in/inedited/`), email (`ineditedofficial@gmail.com`)
- `menu.main`: Home `/`, About `/about`, Projects `/projects`, Resume `/resume`, Blog `/posts`
**Custom layouts (the only real theme surgery):**
1. `layouts/partials/gpu-background.html` — `<canvas id="gpu" aria-hidden="true"></canvas>` + `<script src="{{ "gpu/engine.js" | relURL }}" defer></script>`.
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 `<canvas id="gpu">` + `/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/<slug>`, `/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.

89
DEPLOYMENT.md Normal file
View File

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

View File

@@ -1,34 +1,24 @@
# ---- stage 1: build ---- FROM node:22-alpine AS engine
FROM node:22-alpine AS build WORKDIR /gpu
WORKDIR /app COPY gpu-bg/package.json gpu-bg/package-lock.json* ./
# Install deps (cache layer)
COPY package.json package-lock.json* ./
RUN npm ci RUN npm ci
COPY gpu-bg/ ./
# 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 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 FROM nginx:alpine-slim AS serve
# curl needed for the healthcheck in docker-compose
RUN apk add --no-cache curl RUN apk add --no-cache curl
COPY --from=site /site/public /usr/share/nginx/html
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 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 COPY nginx.default.conf /etc/nginx/conf.d/default.conf
RUN nginx -t RUN nginx -t
EXPOSE 8080 EXPOSE 8080
CMD ["nginx", "-g", "daemon off;"] CMD ["nginx", "-g", "daemon off;"]

View File

@@ -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) [![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 ## Dev
**Hugo server** (site content):
```bash ```bash
npm install hugo server
npm run dev # vite dev server (opens browser) ```
npm run typecheck # tsc --noEmit
npm run build # typecheck + vite build → dist/ **Engine** (background):
npm run preview # serve built dist/
```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 ## Docker
```bash ```bash
# Build docker compose 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 up -d docker compose up -d
``` ```
Site at `http://localhost:8080`. Site at `http://127.0.0.1:8080`.
### Smoke test ## Deploy
```bash Server-side deployment (Gitea Actions / webhook) is documented in [DEPLOYMENT.md](DEPLOYMENT.md) — including the mandatory recursive-submodule checkout for the Hugo build.
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.

34
archive/Dockerfile Normal file
View File

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

93
assets/css/custom.css Normal file
View File

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

7
content/_index.md Normal file
View File

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

26
content/about.md Normal file
View File

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

5
content/posts/_index.md Normal file
View File

@@ -0,0 +1,5 @@
+++
title = "Blog"
+++
Posts will appear here.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

6
content/resume.md Normal file
View File

@@ -0,0 +1,6 @@
+++
title = "Resume"
description = "Ahmed Mohamed — Graphics & Systems Engineer"
type = "resume"
layout = "resume"
+++

82
data/resume.yaml Normal file
View File

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

7
gpu-bg/index.html Normal file
View File

@@ -0,0 +1,7 @@
<!doctype html>
<html lang="en">
<head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>gpu-bg dev</title>
<style>html,body{margin:0;height:100%;background:#000}canvas{display:block;position:fixed;inset:0;width:100vw;height:100vh}</style>
</head>
<body><canvas id="gpu"></canvas><script type="module" src="/src/main.ts"></script></body>
</html>

1028
gpu-bg/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

17
gpu-bg/package.json Normal file
View File

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

127
gpu-bg/src/gl-renderer.ts Normal file
View File

@@ -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<boolean> {
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)
}
}

14
gpu-bg/src/input.ts Normal file
View File

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

68
gpu-bg/src/main.ts Normal file
View File

@@ -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<void> {
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()
}

105
gpu-bg/src/renderer.ts Normal file
View File

@@ -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<boolean> {
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()])
}
}

View File

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

View File

@@ -0,0 +1,108 @@
struct Uniforms {
resolution: vec2<f32>,
time: f32,
scene: f32,
mouse: vec2<f32>,
date: vec4<f32>,
sampleRate: vec4<f32>,
stemMultipliers: array<vec4<f32>, 8>,
};
@group(0) @binding(0) var<uniform> u: Uniforms;
struct VsOut {
@builtin(position) pos: vec4<f32>,
@location(0) uv: vec2<f32>,
};
@vertex
fn vs(@builtin(vertex_index) i: u32) -> VsOut {
let p = array<vec2<f32>, 3>(
vec2<f32>(-1.0, -3.0),
vec2<f32>(-1.0, 1.0),
vec2<f32>(3.0, 1.0),
);
let clip = p[i];
var out: VsOut;
out.pos = vec4<f32>(clip, 0.0, 1.0);
out.uv = clip;
return out;
}
@fragment
fn fs(in: VsOut) -> @location(0) vec4<f32> {
let res = u.resolution;
let fragCoord = (in.uv * vec2<f32>(0.5) + vec2<f32>(0.5)) * res;
var uv = (fragCoord - 0.5 * res) / res.y;
let slowTime = u.time * 0.2;
let bgGrey = vec3<f32>(0.11, 0.11, 0.12);
let cardGrey = vec3<f32>(0.30, 0.32, 0.36);
let coreWhite = vec3<f32>(0.90, 0.95, 0.90);
// ---------- palette keyed by scene ----------
let scene0 = vec3<f32>(0.20, 0.75, 0.20); // HERO – emerald
let scene1 = vec3<f32>(0.85, 0.55, 0.20); // ABOUT – amber
let scene2 = vec3<f32>(0.25, 0.60, 0.95); // PROJECTS – cobalt
let scene3 = vec3<f32>(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<f32>(rChannel, gChannel, bChannel);
let coreNeon = vec3<f32>(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<f32>(0.0), dim);
return vec4<f32>(finalColor * vignette, 1.0);
}

8
gpu-bg/src/types.ts Normal file
View File

@@ -0,0 +1,8 @@
import type { UiState } from './renderer'
export interface RendererLike {
readonly ready: boolean
init(canvas: HTMLCanvasElement): Promise<boolean>
render(state: UiState): void
resize(): void
}

4
gpu-bg/src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1,4 @@
declare module '*?raw' {
const src: string
export default src
}

17
gpu-bg/tsconfig.json Normal file
View File

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

16
gpu-bg/vite.config.ts Normal file
View File

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

75
hugo.toml Normal file
View File

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

11
layouts/home.html Normal file
View File

@@ -0,0 +1,11 @@
{{ define "content" }}
{{ partial "gpu-background.html" . }}
{{ partial "home.html" . }}
{{ if .Content }}
<section class="container centered">
<div class="about home-content">
{{ .Content }}
</div>
</section>
{{ end }}
{{ end }}

View File

@@ -0,0 +1,2 @@
<canvas id="gpu" aria-hidden="true"></canvas>
<script src="{{ "gpu/engine.js" | relURL }}" defer></script>

View File

@@ -0,0 +1,37 @@
{{ define "title" }}
{{ .Title }} · {{ .Site.Title }}
{{ end }}
{{ define "content" }}
<section class="container list">
<header>
<h1 class="title">
<a class="title-link" href="{{ .Permalink | safeURL }}">
{{ .Title }}
</a>
</h1>
</header>
{{ .Content }}
{{ range .Pages.ByDate.Reverse }}
<div class="project-card">
{{ if .Params.period }}
<p class="project-period">{{ .Params.period }}</p>
{{ end }}
<h2 class="project-name">
<a class="title-link" href="{{ .RelPermalink }}">{{ .Title }}</a>
</h2>
{{ if .Params.description }}
<p class="project-desc">{{ .Params.description }}</p>
{{ end }}
{{ if .Params.tags }}
<p class="project-tags">
{{ range .Params.tags }}
<span class="pill">{{ . }}</span>
{{ end }}
</p>
{{ end }}
</div>
{{ end }}
</section>
{{ end }}

View File

@@ -0,0 +1,68 @@
{{ define "title" }}
{{ .Title }} · {{ .Site.Title }}
{{ end }}
{{ define "content" }}
{{ with hugo.Data.resume }}
<section class="container page">
<article class="resume">
<header>
<h1 class="title">{{ .name }}</h1>
<p class="resume-contact">
{{ range $i, $c := .contact }}
{{ if $i }}<span class="contact-sep"> · </span>{{ end }}
<a href="{{ $c.href | safeURL }}">{{ $c.label }}</a>
{{ end }}
</p>
<p class="resume-meta">{{ .location }}</p>
</header>
<section>
<h2>Summary</h2>
<p>{{ .summary }}</p>
</section>
<section>
<h2>Work Experience</h2>
{{ range .work }}
<div class="resume-card">
<p class="resume-period">{{ .date }}</p>
<h3>{{ .role }}</h3>
<p class="resume-meta">{{ .company }} — {{ .location }}</p>
<ul>
{{ range .bullets }}
<li>{{ . }}</li>
{{ end }}
</ul>
</div>
{{ end }}
</section>
<section>
<h2>Education</h2>
{{ range .education }}
<div class="resume-card">
<p class="resume-period">{{ .date }}</p>
<h3>{{ .role }}</h3>
<p class="resume-meta">{{ .company }} — {{ .location }}</p>
{{ if .thesis }}
<p class="resume-meta resume-thesis"><em>{{ .thesis }}</em></p>
{{ end }}
</div>
{{ end }}
</section>
<section>
<h2>Skills</h2>
{{ range .skills }}
<h3>{{ .label }}</h3>
<p>
{{ range .skills }}
<span class="pill">{{ . }}</span>
{{ end }}
</p>
{{ end }}
</section>
</article>
</section>
{{ end }}
{{ end }}

View File

@@ -5,26 +5,26 @@ server {
root /usr/share/nginx/html; root /usr/share/nginx/html;
index index.html; index index.html;
# Security headers
add_header X-Content-Type-Options "nosniff" always; add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always; add_header X-Frame-Options "DENY" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Enable compression
gzip on; gzip on;
gzip_types text/css application/javascript text/javascript application/wasm image/svg+xml; gzip_types text/css application/javascript text/javascript application/wasm image/svg+xml;
gzip_min_length 512; gzip_min_length 512;
# Immutable cache for hashed assets # fingerprint-hashed Hugo assets (css/js) and the engine bundle: immutable
location /assets/ { location /css/ { expires 1y; add_header Cache-Control "public, immutable"; }
expires 1y; location /js/ { expires 1y; add_header Cache-Control "public, immutable"; }
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 / { location / {
try_files $uri $uri/ /index.html; try_files $uri $uri/ =404;
expires -1; expires -1;
add_header Cache-Control "no-cache"; add_header Cache-Control "no-cache";
} }
} }

1
themes/hugo-coder Submodule

Submodule themes/hugo-coder added at 58d956f41c