Migrate Vite SPA to Hugo (hugo-coder, dark) with WebGPU background

- Hugo static site at root: config, content (home/about/resume/blog + 5 projects),
  data/resume.yaml, custom layouts (gpu background, resume renderer, projects list),
  dark theme with canvas behind content
- gpu-bg/: extracted WebGPU/WebGL2 spiral engine as a Vite sub-package; slim bootstrap
  drops router/overlay/text renderer; fixed uniforms (scene0/dim0); self-contained engine.js
- archive/: retired Vite SPA preserved (router, overlay, data, engine/text orphaned)
- 3-stage Dockerfile (node -> hugo extended/dart-sass -> nginx), static nginx config
- DEPLOYMENT.md handover + README rewrite; pre-hugo rollback tag
This commit is contained in:
2026-08-21 03:39:42 +02:00
parent 3ad5cf55e3
commit 9ea85db5ce
74 changed files with 3170 additions and 75 deletions

View File

@@ -0,0 +1,205 @@
import shaderSource from './text.wgsl?raw'
import type { SlugData } from './slug-generator'
import type { TextLayoutResult } from './text-layout'
const TEXT_UNIFORM_FLOATS = 8 // resolution(2) + pad(2) + color(4)
const TEXT_UNIFORM_BYTES = TEXT_UNIFORM_FLOATS * 4
const QUAD_VERTICES = new Float32Array([
-1.0, -1.0,
-1.0, 1.0,
1.0, 1.0,
-1.0, -1.0,
1.0, 1.0,
1.0, -1.0
])
const MAX_GLYPHS = 4096
const INSTANCE_STRIDE = 4 * 4
export class TextRenderer {
private device!: GPUDevice
private pipeline!: GPURenderPipeline
private bindGroup!: GPUBindGroup
private uniformBuffer!: GPUBuffer
private uniformData = new Float32Array(TEXT_UNIFORM_FLOATS)
private quadBuffer!: GPUBuffer
private scaleBiasBuffer!: GPUBuffer
private glyphBandScaleBuffer!: GPUBuffer
private bandMaxTexCoordsBuffer!: GPUBuffer
private curvesTex!: GPUTexture
private bandsTex!: GPUTexture
private glyphCount = 0
private initialized = false
get ready(): boolean {
return this.initialized
}
init(device: GPUDevice, format: GPUTextureFormat, slugData: SlugData): void {
this.device = device
this.createTextures(slugData)
const module = device.createShaderModule({ code: shaderSource })
const bindGroupLayout = device.createBindGroupLayout({
entries: [
{ binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: 'uniform' } },
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'unfilterable-float' } },
{ binding: 2, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'uint' } }
]
})
const pipelineLayout = device.createPipelineLayout({ bindGroupLayouts: [bindGroupLayout] })
this.pipeline = device.createRenderPipeline({
layout: pipelineLayout,
vertex: {
module,
entryPoint: 'vs_main',
buffers: [
{
arrayStride: 2 * 4,
attributes: [{ shaderLocation: 0, offset: 0, format: 'float32x2' }]
},
{
arrayStride: INSTANCE_STRIDE,
stepMode: 'instance',
attributes: [{ shaderLocation: 1, offset: 0, format: 'float32x4' }]
},
{
arrayStride: INSTANCE_STRIDE,
stepMode: 'instance',
attributes: [{ shaderLocation: 2, offset: 0, format: 'float32x4' }]
},
{
arrayStride: INSTANCE_STRIDE,
stepMode: 'instance',
attributes: [{ shaderLocation: 3, offset: 0, format: 'uint32x4' }]
}
]
},
fragment: {
module,
entryPoint: 'fs_main',
targets: [{
format,
blend: {
color: { srcFactor: 'src-alpha', dstFactor: 'one-minus-src-alpha', operation: 'add' },
alpha: { srcFactor: 'one', dstFactor: 'one-minus-src-alpha', operation: 'add' }
}
}]
},
primitive: { topology: 'triangle-list' }
})
this.uniformBuffer = device.createBuffer({
size: TEXT_UNIFORM_BYTES,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
})
this.quadBuffer = device.createBuffer({
size: QUAD_VERTICES.byteLength,
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST
})
device.queue.writeBuffer(this.quadBuffer, 0, QUAD_VERTICES)
this.scaleBiasBuffer = device.createBuffer({
size: MAX_GLYPHS * INSTANCE_STRIDE,
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST
})
this.glyphBandScaleBuffer = device.createBuffer({
size: MAX_GLYPHS * INSTANCE_STRIDE,
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST
})
this.bandMaxTexCoordsBuffer = device.createBuffer({
size: MAX_GLYPHS * INSTANCE_STRIDE,
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST
})
this.bindGroup = device.createBindGroup({
layout: bindGroupLayout,
entries: [
{ binding: 0, resource: { buffer: this.uniformBuffer } },
{ binding: 1, resource: this.curvesTex.createView() },
{ binding: 2, resource: this.bandsTex.createView() }
]
})
this.initialized = true
}
private createTextures(slugData: SlugData): void {
this.curvesTex = this.device.createTexture({
size: { width: slugData.curvesWidth, height: slugData.curvesHeight },
format: 'rgba32float',
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST
})
this.device.queue.writeTexture(
{ texture: this.curvesTex },
slugData.curvesData.buffer as BufferSource,
{ bytesPerRow: slugData.curvesWidth * 4 * 4 },
{ width: slugData.curvesWidth, height: slugData.curvesHeight }
)
this.bandsTex = this.device.createTexture({
size: { width: slugData.bandsWidth, height: slugData.bandsHeight },
format: 'rg32uint',
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST
})
this.device.queue.writeTexture(
{ texture: this.bandsTex },
slugData.bandsData.buffer as BufferSource,
{ bytesPerRow: slugData.bandsWidth * 2 * 4 },
{ width: slugData.bandsWidth, height: slugData.bandsHeight }
)
}
updateText(layout: TextLayoutResult): void {
if (!this.initialized) return
const count = Math.min(layout.glyphCount, MAX_GLYPHS)
this.glyphCount = count
if (count === 0) return
const sb = layout.scaleBias.subarray(0, count * 4)
this.device.queue.writeBuffer(this.scaleBiasBuffer, 0, sb.buffer as BufferSource, sb.byteOffset, sb.byteLength)
const gb = layout.glyphBandScale.subarray(0, count * 4)
this.device.queue.writeBuffer(this.glyphBandScaleBuffer, 0, gb.buffer as BufferSource, gb.byteOffset, gb.byteLength)
const bm = layout.bandMaxTexCoords.subarray(0, count * 4)
this.device.queue.writeBuffer(this.bandMaxTexCoordsBuffer, 0, bm.buffer as BufferSource, bm.byteOffset, bm.byteLength)
}
renderPass(
encoder: GPUCommandEncoder,
canvasView: GPUTextureView,
resolution: Float32Array,
textColor: Float32Array
): void {
if (!this.initialized || this.glyphCount === 0) return
this.uniformData[0] = resolution[0]
this.uniformData[1] = resolution[1]
this.uniformData[4] = textColor[0]
this.uniformData[5] = textColor[1]
this.uniformData[6] = textColor[2]
this.uniformData[7] = textColor[3]
this.device.queue.writeBuffer(this.uniformBuffer, 0, this.uniformData)
const pass = encoder.beginRenderPass({
colorAttachments: [
{ view: canvasView, loadOp: 'load', storeOp: 'store' }
]
})
pass.setPipeline(this.pipeline)
pass.setBindGroup(0, this.bindGroup)
pass.setVertexBuffer(0, this.quadBuffer)
pass.setVertexBuffer(1, this.scaleBiasBuffer)
pass.setVertexBuffer(2, this.glyphBandScaleBuffer)
pass.setVertexBuffer(3, this.bandMaxTexCoordsBuffer)
pass.draw(6, this.glyphCount)
pass.end()
}
}