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:
@@ -1,12 +1,10 @@
|
||||
node_modules
|
||||
dist
|
||||
.git
|
||||
.gitignore
|
||||
*.md
|
||||
.hermes
|
||||
archive
|
||||
public
|
||||
resources
|
||||
gpu-bg/node_modules
|
||||
gpu-bg/dist
|
||||
node_modules
|
||||
*.log
|
||||
.env
|
||||
.env.example
|
||||
nginx/
|
||||
webhook/
|
||||
scripts/
|
||||
docker-compose.yml
|
||||
.hermes/
|
||||
|
||||
10
.gitignore
vendored
10
.gitignore
vendored
@@ -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/
|
||||
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
[submodule "themes/hugo-coder"]
|
||||
path = themes/hugo-coder
|
||||
url = https://github.com/luizdepra/hugo-coder
|
||||
551
.hermes/plans/2025-07-27_120000-webgl-fallback.md
Normal file
551
.hermes/plans/2025-07-27_120000-webgl-fallback.md
Normal 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.
|
||||
307
.hermes/plans/2026-08-16_hugo-transition.md
Normal file
307
.hermes/plans/2026-08-16_hugo-transition.md
Normal 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
89
DEPLOYMENT.md
Normal 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.
|
||||
38
Dockerfile
38
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;"]
|
||||
58
README.md
58
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.
|
||||
|
||||
[](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.
|
||||
34
archive/Dockerfile
Normal file
34
archive/Dockerfile
Normal 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
93
assets/css/custom.css
Normal 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
7
content/_index.md
Normal 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
26
content/about.md
Normal 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
5
content/posts/_index.md
Normal file
@@ -0,0 +1,5 @@
|
||||
+++
|
||||
title = "Blog"
|
||||
+++
|
||||
|
||||
Posts will appear here.
|
||||
5
content/projects/_index.md
Normal file
5
content/projects/_index.md
Normal 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.
|
||||
12
content/projects/auv-simulation.md
Normal file
12
content/projects/auv-simulation.md
Normal 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.
|
||||
14
content/projects/evol-engine.md
Normal file
14
content/projects/evol-engine.md
Normal 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.
|
||||
12
content/projects/improv-gfx.md
Normal file
12
content/projects/improv-gfx.md
Normal 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.
|
||||
14
content/projects/point-cloud.md
Normal file
14
content/projects/point-cloud.md
Normal 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.
|
||||
14
content/projects/radiance-caching.md
Normal file
14
content/projects/radiance-caching.md
Normal 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
6
content/resume.md
Normal file
@@ -0,0 +1,6 @@
|
||||
+++
|
||||
title = "Resume"
|
||||
description = "Ahmed Mohamed — Graphics & Systems Engineer"
|
||||
type = "resume"
|
||||
layout = "resume"
|
||||
+++
|
||||
82
data/resume.yaml
Normal file
82
data/resume.yaml
Normal 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
7
gpu-bg/index.html
Normal 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
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
17
gpu-bg/package.json
Normal 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
127
gpu-bg/src/gl-renderer.ts
Normal 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
14
gpu-bg/src/input.ts
Normal 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
68
gpu-bg/src/main.ts
Normal 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
105
gpu-bg/src/renderer.ts
Normal 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()])
|
||||
}
|
||||
}
|
||||
110
gpu-bg/src/shaders/fullscreen.glsl
Normal file
110
gpu-bg/src/shaders/fullscreen.glsl
Normal 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);
|
||||
}
|
||||
108
gpu-bg/src/shaders/fullscreen.wgsl
Normal file
108
gpu-bg/src/shaders/fullscreen.wgsl
Normal 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
8
gpu-bg/src/types.ts
Normal 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
4
gpu-bg/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
declare module '*?raw' {
|
||||
const src: string
|
||||
export default src
|
||||
}
|
||||
17
gpu-bg/tsconfig.json
Normal file
17
gpu-bg/tsconfig.json
Normal 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
16
gpu-bg/vite.config.ts
Normal 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
75
hugo.toml
Normal 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
11
layouts/home.html
Normal 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 }}
|
||||
2
layouts/partials/gpu-background.html
Normal file
2
layouts/partials/gpu-background.html
Normal file
@@ -0,0 +1,2 @@
|
||||
<canvas id="gpu" aria-hidden="true"></canvas>
|
||||
<script src="{{ "gpu/engine.js" | relURL }}" defer></script>
|
||||
37
layouts/projects/list.html
Normal file
37
layouts/projects/list.html
Normal 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 }}
|
||||
68
layouts/resume/single.html
Normal file
68
layouts/resume/single.html
Normal 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 }}
|
||||
@@ -5,25 +5,25 @@ 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";
|
||||
}
|
||||
|
||||
1
themes/hugo-coder
Submodule
1
themes/hugo-coder
Submodule
Submodule themes/hugo-coder added at 58d956f41c
Reference in New Issue
Block a user