LFM2-MoE-WebGPU / src /components /NeuroBackground.tsx
mlabonne's picture
Add demo files (#1)
4755edd
/**
* 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<uniform> 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<HTMLCanvasElement>(null);
const pointerRef = useRef({ x: 0, y: 0, tX: 0, tY: 0 });
const onReadyRef = useRef(onReady);
onReadyRef.current = onReady;
const rafRef = useRef<number>(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 <canvas ref={canvasRef} className="fixed inset-0 w-full h-full pointer-events-none opacity-[0.95]" />;
}