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:
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"]
|
||||
});
|
||||
Reference in New Issue
Block a user