/** * WebGPU (wgsl) implementation of https://codepen.io/ksenia-k/pen/vYwgrWv */ import { useRef, useEffect } from "react"; const SHADER_SOURCE = /* wgsl */ ` struct Uniforms { time: f32, ratio: f32, pointer: vec2f, scroll_progress: f32, } @group(0) @binding(0) var u: Uniforms; struct VsOut { @builtin(position) pos: vec4f, @location(0) uv: vec2f, } @vertex fn vs(@builtin(vertex_index) i: u32) -> VsOut { let x = f32(i32(i & 1u) * 2 - 1); let y = f32(i32(i >> 1u) * 2 - 1); var o: VsOut; o.pos = vec4f(x, y, 0.0, 1.0); o.uv = vec2f(x, y) * 0.5 + 0.5; return o; } fn rotate2(v: vec2f, th: f32) -> vec2f { let c = cos(th); let s = sin(th); return vec2f(c * v.x + s * v.y, -s * v.x + c * v.y); } fn neuro_shape(uv_in: vec2f, t: f32, p: f32) -> f32 { var uv = uv_in; var sine_acc = vec2f(0.0); var res = vec2f(0.0); var scale: f32 = 8.0; for (var j = 0; j < 15; j++) { uv = rotate2(uv, 1.0); sine_acc = rotate2(sine_acc, 1.0); let layer = uv * scale + f32(j) + sine_acc - t; sine_acc += sin(layer) + 2.4 * p; res += (0.5 + 0.5 * cos(layer)) / scale; scale *= 1.2; } return res.x + res.y; } @fragment fn fs(@location(0) vUv: vec2f) -> @location(0) vec4f { var uv = 0.5 * vUv; uv.x *= u.ratio; var pointer = vUv - u.pointer; pointer.x *= u.ratio; let p = 0.5 * pow(1.0 - clamp(length(pointer), 0.0, 1.0), 2.0); let t = 0.001 * u.time; var noise = neuro_shape(uv, t, p); noise = 1.2 * pow(noise, 3.0); noise += pow(noise, 10.0); noise = max(0.0, noise - 0.5); noise *= (1.0 - length(vUv - 0.5)); let sp = u.scroll_progress; var color = normalize(vec3f(0.2, 0.5 + 0.4 * cos(3.0 * sp), 0.5 + 0.5 * sin(3.0 * sp))); color = color * noise; return vec4f(color, noise); } `; const UNIFORM_SIZE = 32; interface NeuroBackgroundProps { onReady?: () => void; } export function NeuroBackground({ onReady }: NeuroBackgroundProps) { const canvasRef = useRef(null); const pointerRef = useRef({ x: 0, y: 0, tX: 0, tY: 0 }); const onReadyRef = useRef(onReady); onReadyRef.current = onReady; const rafRef = useRef(0); useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; let destroyed = false; async function init() { if (!navigator.gpu) return; const adapter = await navigator.gpu.requestAdapter(); if (!adapter || destroyed) return; const device = await adapter.requestDevice(); if (destroyed) { device.destroy(); return; } const ctx = canvas!.getContext("webgpu") as GPUCanvasContext | null; if (!ctx) return; const format = navigator.gpu.getPreferredCanvasFormat(); ctx.configure({ device, format, alphaMode: "premultiplied" }); const module = device.createShaderModule({ code: SHADER_SOURCE }); const pipeline = device.createRenderPipeline({ layout: "auto", vertex: { module, entryPoint: "vs" }, fragment: { module, entryPoint: "fs", 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-strip", stripIndexFormat: "uint32" }, }); const uniformBuf = device.createBuffer({ size: UNIFORM_SIZE, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, }); const bindGroup = device.createBindGroup({ layout: pipeline.getBindGroupLayout(0), entries: [{ binding: 0, resource: { buffer: uniformBuf } }], }); const uniformData = new Float32Array(UNIFORM_SIZE / 4); const dpr = Math.min(window.devicePixelRatio, 2); function resize() { if (!canvas || destroyed) return; canvas.width = window.innerWidth * dpr; canvas.height = window.innerHeight * dpr; } resize(); window.addEventListener("resize", resize); let firstFrame = true; function render() { if (destroyed) return; const ptr = pointerRef.current; ptr.x += (ptr.tX - ptr.x) * 0.2; ptr.y += (ptr.tY - ptr.y) * 0.2; uniformData[0] = performance.now(); uniformData[1] = canvas!.width / canvas!.height; uniformData[2] = ptr.x / window.innerWidth; uniformData[3] = 1 - ptr.y / window.innerHeight; uniformData[4] = 0; device.queue.writeBuffer(uniformBuf, 0, uniformData); const encoder = device.createCommandEncoder(); const pass = encoder.beginRenderPass({ colorAttachments: [ { view: ctx!.getCurrentTexture().createView(), loadOp: "clear", storeOp: "store", clearValue: { r: 0, g: 0, b: 0, a: 0 }, }, ], }); pass.setPipeline(pipeline); pass.setBindGroup(0, bindGroup); pass.draw(4); pass.end(); device.queue.submit([encoder.finish()]); if (firstFrame) { firstFrame = false; onReadyRef.current?.(); } rafRef.current = requestAnimationFrame(render); } rafRef.current = requestAnimationFrame(render); return { device, resize }; } let cleanup: (() => void) | undefined; init().then((resources) => { if (destroyed) { resources?.device.destroy(); return; } function onPointerMove(e: PointerEvent) { pointerRef.current.tX = e.clientX; pointerRef.current.tY = e.clientY; } function onTouchMove(e: TouchEvent) { pointerRef.current.tX = e.targetTouches[0].clientX; pointerRef.current.tY = e.targetTouches[0].clientY; } function onClick(e: MouseEvent) { pointerRef.current.tX = e.clientX; pointerRef.current.tY = e.clientY; } window.addEventListener("pointermove", onPointerMove); window.addEventListener("touchmove", onTouchMove); window.addEventListener("click", onClick); cleanup = () => { window.removeEventListener("resize", resources!.resize); window.removeEventListener("pointermove", onPointerMove); window.removeEventListener("touchmove", onTouchMove); window.removeEventListener("click", onClick); resources?.device.destroy(); }; }); return () => { destroyed = true; cancelAnimationFrame(rafRef.current); cleanup?.(); }; }, []); return ; }