Created
October 11, 2025 05:08
-
-
Save vidyesh95/8b819c74b445d75e3cfccb05f5487318 to your computer and use it in GitHub Desktop.
Hybrid shader for svelte
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <script lang="ts"> | |
| import HybridShader from '$lib/components/landing/HybridShader.svelte'; | |
| import { onMount } from 'svelte'; | |
| import type { Component } from 'svelte'; | |
| let HybridShaderComp = $state<Component | null>(null); | |
| onMount(async () => { | |
| const m = await import('$lib/components/landing/HybridShader.svelte'); | |
| HybridShaderComp = m.default; | |
| }); | |
| </script> | |
| <section | |
| id="home" | |
| class="md:pt-16 relative flex flex-col items-center md:gap-4 px-4 py-8" | |
| > | |
| <!-- page decoration --> | |
| <div | |
| class="bg-black absolute inset-0 flex justify-center items-start md:items-center z-[-3] pointer-events-none" | |
| > | |
| {#if HybridShaderComp} | |
| <HybridShader | |
| class="w-full h-full object-cover" | |
| u_color={[0.3137254901960784, 0, 1]} | |
| u_background={[0,0,0,1]} | |
| u_speed={0.9} | |
| u_detail={0.4} | |
| /> | |
| {/if} | |
| </div> | |
| </section> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!-- WebGPU type declarations at file level --> | |
| <script lang="ts" module> | |
| // WebGPU type declarations | |
| declare global { | |
| interface Navigator { | |
| gpu?: GPU; | |
| } | |
| interface GPU { | |
| requestAdapter(options?: GPURequestAdapterOptions): Promise<GPUAdapter | null>; | |
| getPreferredCanvasFormat(): GPUTextureFormat; | |
| } | |
| interface GPUAdapter { | |
| requestDevice(descriptor?: GPUDeviceDescriptor): Promise<GPUDevice>; | |
| } | |
| interface GPUDevice { | |
| createBuffer(descriptor: GPUBufferDescriptor): GPUBuffer; | |
| createBindGroupLayout(descriptor: GPUBindGroupLayoutDescriptor): GPUBindGroupLayout; | |
| createBindGroup(descriptor: GPUBindGroupDescriptor): GPUBindGroup; | |
| createRenderPipeline(descriptor: GPURenderPipelineDescriptor): GPURenderPipeline; | |
| createPipelineLayout(descriptor: GPUPipelineLayoutDescriptor): GPUPipelineLayout; | |
| createShaderModule(descriptor: GPUShaderModuleDescriptor): GPUShaderModule; | |
| createCommandEncoder(descriptor?: GPUCommandEncoderDescriptor): GPUCommandEncoder; | |
| queue: GPUQueue; | |
| } | |
| interface GPUQueue { | |
| writeBuffer(buffer: GPUBuffer, bufferOffset: number, data: ArrayBufferView): void; | |
| submit(commandBuffers: GPUCommandBuffer[]): void; | |
| } | |
| interface GPUCanvasContext { | |
| configure(configuration: GPUCanvasConfiguration): void; | |
| getCurrentTexture(): GPUTexture; | |
| } | |
| interface HTMLCanvasElement { | |
| getContext(contextId: 'webgpu'): GPUCanvasContext | null; | |
| } | |
| } | |
| // WebGPU interface definitions | |
| interface GPUBuffer { | |
| destroy(): void; | |
| } | |
| interface GPUBindGroup {} | |
| interface GPURenderPipeline {} | |
| interface GPUBindGroupLayout {} | |
| interface GPUPipelineLayout {} | |
| interface GPUShaderModule {} | |
| interface GPUCommandEncoder { | |
| beginRenderPass(descriptor: GPURenderPassDescriptor): GPURenderPassEncoder; | |
| finish(): GPUCommandBuffer; | |
| } | |
| interface GPUCommandBuffer {} | |
| interface GPUTexture { | |
| createView(): GPUTextureView; | |
| } | |
| interface GPUTextureView {} | |
| interface GPURenderPassEncoder { | |
| setPipeline(pipeline: GPURenderPipeline): void; | |
| setBindGroup(index: number, bindGroup: GPUBindGroup): void; | |
| draw(vertexCount: number): void; | |
| end(): void; | |
| } | |
| // WebGPU descriptor interfaces | |
| interface GPURequestAdapterOptions {} | |
| interface GPUDeviceDescriptor {} | |
| interface GPUBufferDescriptor { | |
| size: number; | |
| usage: GPUBufferUsageFlags; | |
| } | |
| interface GPUBindGroupLayoutDescriptor { | |
| entries: GPUBindGroupLayoutEntry[]; | |
| } | |
| interface GPUBindGroupLayoutEntry { | |
| binding: number; | |
| visibility: GPUShaderStageFlags; | |
| buffer?: { type: string }; | |
| } | |
| interface GPUBindGroupDescriptor { | |
| layout: GPUBindGroupLayout; | |
| entries: GPUBindGroupEntry[]; | |
| } | |
| interface GPUBindGroupEntry { | |
| binding: number; | |
| resource: { buffer: GPUBuffer }; | |
| } | |
| interface GPUPipelineLayoutDescriptor { | |
| bindGroupLayouts: GPUBindGroupLayout[]; | |
| } | |
| interface GPUShaderModuleDescriptor { | |
| code: string; | |
| } | |
| interface GPURenderPipelineDescriptor { | |
| layout: GPUPipelineLayout; | |
| vertex: GPUVertexState; | |
| fragment?: GPUFragmentState; | |
| primitive?: GPUPrimitiveState; | |
| } | |
| interface GPUVertexState { | |
| module: GPUShaderModule; | |
| entryPoint: string; | |
| } | |
| interface GPUFragmentState { | |
| module: GPUShaderModule; | |
| entryPoint: string; | |
| targets: GPUColorTargetState[]; | |
| } | |
| interface GPUColorTargetState { | |
| format: GPUTextureFormat; | |
| } | |
| interface GPUPrimitiveState { | |
| topology: string; | |
| } | |
| interface GPUCanvasConfiguration { | |
| device: GPUDevice; | |
| format: GPUTextureFormat; | |
| alphaMode: string; | |
| } | |
| interface GPUCommandEncoderDescriptor {} | |
| interface GPURenderPassDescriptor { | |
| colorAttachments: GPURenderPassColorAttachment[]; | |
| } | |
| interface GPURenderPassColorAttachment { | |
| view: GPUTextureView; | |
| clearValue: { r: number; g: number; b: number; a: number }; | |
| loadOp: string; | |
| storeOp: string; | |
| } | |
| // Type aliases | |
| type GPUTextureFormat = string; | |
| type GPUBufferUsageFlags = number; | |
| type GPUShaderStageFlags = number; | |
| // Constants | |
| const GPUBufferUsage = { | |
| UNIFORM: 0x40, | |
| COPY_DST: 0x08 | |
| } as const; | |
| const GPUShaderStage = { | |
| FRAGMENT: 0x2 | |
| } as const; | |
| </script> | |
| <script lang="ts"> | |
| /** | |
| * Public component properties. | |
| * | |
| * @property {string} class CSS class applied to the <canvas>. Default: ''. | |
| * @property {[number, number, number]} u_color Foreground RGB in 0–1. Default: [0.3137254901960784, 0, 1]. | |
| * @property {[number, number, number, number]} u_background Background RGBA in 0–1. Default: [0, 0, 0, 1]. | |
| * @property {number} u_speed Animation speed multiplier (>=0). Default: 0.1. | |
| * @property {number} u_detail Ray-march iteration factor in [0,1]; controls cost/quality. Default: 0.4. | |
| * @property {number} maxDpr Upper bound for devicePixelRatio used for rendering resolution. Default: 2. | |
| * | |
| * Behavior: | |
| * Props influence uniforms for both the WebGPU and WebGL paths. | |
| * Values are read each frame; updates are reflected without reinitialization. | |
| */ | |
| let { | |
| class: klass = '', | |
| u_color = [0.3137254901960784, 0, 1], | |
| u_background = [0, 0, 0, 1], | |
| u_speed = 0.1, | |
| u_detail = 0.4, | |
| maxDpr = 2 | |
| } = $props(); | |
| /** Canvas reference; bound to the root <canvas>. */ | |
| let canvas: HTMLCanvasElement | undefined = $state(); | |
| // WebGPU state | |
| let device: GPUDevice | null = $state(null); | |
| let gpuContext: GPUCanvasContext | null = $state(null); | |
| let pipeline: GPURenderPipeline | null = $state(null); | |
| let uniformBuffer: GPUBuffer | null = $state(null); | |
| let bindGroup: GPUBindGroup | null = $state(null); | |
| // WebGL state | |
| let gl: WebGLRenderingContext | null = $state(null); | |
| let glProgram: WebGLProgram | null = $state(null); | |
| let positionBuf: WebGLBuffer | null = $state(null); | |
| let a_position = $state(-1); | |
| let uTime: WebGLUniformLocation | null = $state(null); | |
| let uMouse: WebGLUniformLocation | null = $state(null); | |
| let uRes: WebGLUniformLocation | null = $state(null); | |
| let uColor: WebGLUniformLocation | null = $state(null); | |
| let uBg: WebGLUniformLocation | null = $state(null); | |
| let uSpeed: WebGLUniformLocation | null = $state(null); | |
| let uDetail: WebGLUniformLocation | null = $state(null); | |
| // Common state | |
| let isWebGPU = $state(false); | |
| let start = $state(0); | |
| let raf = $state(0); | |
| let mouseX = $state(0); | |
| let mouseY = $state(0); | |
| /** | |
| * WebGPU full-screen triangle vertex shader (WGSL). | |
| * Emits clip-space positions covering the viewport with two triangles. | |
| */ | |
| const WEBGPU_VERTEX_SHADER = ` | |
| @vertex | |
| fn vs_main(@builtin(vertex_index) vertexIndex: u32) -> @builtin(position) vec4f { | |
| var pos = array<vec2f, 6>( | |
| vec2f(-1.0, -1.0), | |
| vec2f( 1.0, -1.0), | |
| vec2f(-1.0, 1.0), | |
| vec2f( 1.0, -1.0), | |
| vec2f( 1.0, 1.0), | |
| vec2f(-1.0, 1.0) | |
| ); | |
| return vec4f(pos[vertexIndex], 0.0, 1.0); | |
| } | |
| `; | |
| /** | |
| * WebGPU fragment shader (WGSL). | |
| * | |
| * Uniform layout (std140-like tightly packed float32 array written by JS): | |
| * [0]=resolution.x, [1]=resolution.y, [2]=time(s), [3]=speed, [4]=detail, | |
| * [8..10]=color.rgb, [12..15]=background.rgba, [16]=mouse.x, [17]=mouse.y. | |
| * | |
| * Performs simple ray-marching to synthesize a dynamic field and composites foreground on background. | |
| */ | |
| const WEBGPU_FRAGMENT_SHADER = ` | |
| struct Uniforms { | |
| resolution: vec2f, | |
| time: f32, | |
| speed: f32, | |
| detail: f32, | |
| padding1: f32, | |
| padding2: f32, | |
| padding3: f32, | |
| color: vec3f, | |
| padding4: f32, | |
| background: vec4f, | |
| mouse: vec2f, | |
| padding5: f32, | |
| padding6: f32, | |
| } | |
| @group(0) @binding(0) var<uniform> uniforms: Uniforms; | |
| fn rotate2D(a: f32) -> mat2x2f { | |
| let c = cos(a); | |
| let s = sin(a); | |
| return mat2x2f(c, -s, s, c); | |
| } | |
| fn map(p: vec3f) -> f32 { | |
| let t = uniforms.time * uniforms.speed; | |
| var pos = p; | |
| pos = vec3f(rotate2D(t * 0.4) * pos.xz, pos.y).xzy; | |
| pos = vec3f(rotate2D(t * 0.1) * pos.xy, pos.z); | |
| let q = pos * 2.0 + t; | |
| return length(pos + vec3f(sin(t * uniforms.speed * 0.1))) | |
| * log(length(pos) + 0.9) | |
| + cos(q.x + sin(q.z + cos(q.y))) * 0.5 - 1.0; | |
| } | |
| @fragment | |
| fn fs_main(@builtin(position) fragCoord: vec4f) -> @location(0) vec4f { | |
| let a = (fragCoord.xy - 0.6 * uniforms.resolution) / min(uniforms.resolution.x, uniforms.resolution.y); | |
| var cl = vec3f(0.0); | |
| var d = 2.5; | |
| let maxSteps = i32(1.0 + 20.0 * uniforms.detail); | |
| for (var i = 0; i < 64; i++) { | |
| if (i >= maxSteps) { break; } | |
| let p = vec3f(0.0, 0.0, 4.0) + normalize(vec3f(a, -1.0)) * d; | |
| let rz = map(p); | |
| let f = clamp((rz - map(p + 0.1)) * 0.5, -0.1, 1.0); | |
| let l = vec3f(0.1, 0.3, 0.4) + vec3f(5.0, 2.5, 3.0) * f; | |
| cl = cl * l + smoothstep(2.5, 0.0, rz) * 0.6 * l; | |
| d += min(rz, 1.0); | |
| } | |
| var color = vec4f(min(uniforms.color, cl), 1.0); | |
| color = vec4f( | |
| max(uniforms.background.r, color.r), | |
| max(uniforms.background.g, color.g), | |
| max(uniforms.background.b, color.b), | |
| 1.0 | |
| ); | |
| return color; | |
| } | |
| `; | |
| /** | |
| * WebGL vertex shader (GLSL ES 1.00). Outputs a full-screen triangle strip. | |
| */ | |
| const WEBGL_VERTEX_SHADER = ` | |
| precision highp float; | |
| attribute vec2 a_position; | |
| void main() { | |
| gl_Position = vec4(a_position, 0.0, 1.0); | |
| } | |
| `; | |
| /** | |
| * WebGL fragment shader (GLSL ES 1.00). | |
| * Mirrors the WGSL shader’s math. Uniforms: | |
| * - u_resolution(vec2 px), u_time(seconds), u_color(vec3 0–1), u_background(vec4 0–1), | |
| * u_speed(scalar), u_detail([0,1]), u_mouse(vec2 0–1 normalized window coords). | |
| */ | |
| const WEBGL_FRAGMENT_SHADER = ` | |
| precision mediump float; | |
| uniform vec2 u_resolution; | |
| uniform float u_time; | |
| uniform vec3 u_color; | |
| uniform vec4 u_background; | |
| uniform float u_speed; | |
| uniform float u_detail; | |
| uniform vec2 u_mouse; | |
| mat2 m(float a){ | |
| float c = cos(a), s = sin(a); | |
| return mat2(c,-s,s,c); | |
| } | |
| float map(vec3 p){ | |
| float t = u_time * u_speed; | |
| p.xz *= m(t * 0.4); | |
| p.xy *= m(t * 0.1); | |
| vec3 q = p * 2.0 + t; | |
| return length(p + vec3(sin((t*u_speed) * 0.1))) | |
| * log(length(p) + 0.9) | |
| + cos(q.x + sin(q.z + cos(q.y))) * 0.5 - 1.0; | |
| } | |
| void main(){ | |
| vec2 a = (gl_FragCoord.xy - 0.6 * u_resolution) / min(u_resolution.x, u_resolution.y); | |
| vec3 cl = vec3(0.0); | |
| float d = 2.5; | |
| int maxSteps = int(1.0 + 20.0 * u_detail); | |
| for (int i = 0; i < 64; ++i) { | |
| if (i >= maxSteps) break; | |
| vec3 p = vec3(0.0, 0.0, 4.0) + normalize(vec3(a, -1.0)) * d; | |
| float rz = map(p); | |
| float f = clamp((rz - map(p + 0.1)) * 0.5, -0.1, 1.0); | |
| vec3 l = vec3(0.1, 0.3, 0.4) + vec3(5.0, 2.5, 3.0) * f; | |
| cl = cl * l + smoothstep(2.5, 0.0, rz) * 0.6 * l; | |
| d += min(rz, 1.0); | |
| } | |
| vec4 color = vec4(min(u_color, cl), 1.0); | |
| color.r = max(u_background.r, color.r); | |
| color.g = max(u_background.g, color.g); | |
| color.b = max(u_background.b, color.b); | |
| gl_FragColor = color; | |
| } | |
| `; | |
| /** | |
| * Initialize WebGPU rendering pipeline and resources. | |
| * | |
| * Returns: | |
| * - Promise<boolean>: true if WebGPU successfully initialized and configured for the canvas; false otherwise. | |
| * | |
| * Behavior: | |
| * - Checks support, requests adapter/device, configures canvas context with the preferred format. | |
| * - Creates an 80-byte uniform buffer, bind group/layout, and a render pipeline using the WGSL shaders. | |
| * - Catches and logs failures; leaves state null on failure. | |
| * | |
| * Side effects: | |
| * - Mutates `device`, `gpuContext`, `pipeline`, `uniformBuffer`, `bindGroup`. | |
| * - Configures the GPUCanvasContext with `alphaMode: 'premultiplied'`. | |
| */ | |
| async function initWebGPU(): Promise<boolean> { | |
| if (!canvas || !('gpu' in navigator) || !navigator.gpu) return false; | |
| try { | |
| const adapter = await navigator.gpu.requestAdapter(); | |
| if (!adapter) return false; | |
| device = await adapter.requestDevice(); | |
| gpuContext = canvas.getContext('webgpu') as GPUCanvasContext; | |
| if (!gpuContext) return false; | |
| const format = navigator.gpu.getPreferredCanvasFormat(); | |
| gpuContext.configure({ | |
| device, | |
| format, | |
| alphaMode: 'premultiplied' | |
| }); | |
| uniformBuffer = device.createBuffer({ | |
| size: 80, | |
| usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST | |
| }); | |
| const bindGroupLayout = device.createBindGroupLayout({ | |
| entries: [{ | |
| binding: 0, | |
| visibility: GPUShaderStage.FRAGMENT, | |
| buffer: { type: 'uniform' } | |
| }] | |
| }); | |
| bindGroup = device.createBindGroup({ | |
| layout: bindGroupLayout, | |
| entries: [{ | |
| binding: 0, | |
| resource: { buffer: uniformBuffer } | |
| }] | |
| }); | |
| pipeline = device.createRenderPipeline({ | |
| layout: device.createPipelineLayout({ | |
| bindGroupLayouts: [bindGroupLayout] | |
| }), | |
| vertex: { | |
| module: device.createShaderModule({ code: WEBGPU_VERTEX_SHADER }), | |
| entryPoint: 'vs_main' | |
| }, | |
| fragment: { | |
| module: device.createShaderModule({ code: WEBGPU_FRAGMENT_SHADER }), | |
| entryPoint: 'fs_main', | |
| targets: [{ format }] | |
| }, | |
| primitive: { | |
| topology: 'triangle-list' | |
| } | |
| }); | |
| return true; | |
| } catch (error) { | |
| console.warn('WebGPU initialization failed:', error); | |
| return false; | |
| } | |
| } | |
| /** | |
| * Compile a WebGL shader from source. | |
| * | |
| * @param {number} type gl.VERTEX_SHADER or gl.FRAGMENT_SHADER. | |
| * @param {string} src GLSL ES 1.00 source. | |
| * @returns {WebGLShader|null} Compiled shader or null on error/unsupported context. | |
| * | |
| * Behavior: | |
| * - Requires `gl` to be non-null. | |
| * - Logs compile errors via `console.error` and clean up failed shader objects. | |
| */ | |
| function createShader(type: number, src: string): WebGLShader | null { | |
| if (!gl) return null; | |
| const sh = gl.createShader(type); | |
| if (!sh) return null; | |
| gl.shaderSource(sh, src); | |
| gl.compileShader(sh); | |
| if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) { | |
| console.error(gl.getShaderInfoLog(sh)); | |
| gl.deleteShader(sh); | |
| return null; | |
| } | |
| return sh; | |
| } | |
| /** | |
| * Link a WebGL program from vertex and fragment sources. | |
| * | |
| * @param {string} vsSrc Vertex shader source (GLSL ES 1.00). | |
| * @param {string} fsSrc Fragment shader source (GLSL ES 1.00). | |
| * @returns {WebGLProgram|null} Linked program or null on failure. | |
| * | |
| * Behavior: | |
| * - Compiles both shaders, links program, deletes shader objects after a link. | |
| * - Logs link errors via `console.error` and deletes the program on failure. | |
| */ | |
| function createProgram(vsSrc: string, fsSrc: string): WebGLProgram | null { | |
| if (!gl) return null; | |
| const vs = createShader(gl.VERTEX_SHADER, vsSrc); | |
| const fs = createShader(gl.FRAGMENT_SHADER, fsSrc); | |
| if (!vs || !fs) return null; | |
| const prog = gl.createProgram(); | |
| if (!prog) return null; | |
| gl.attachShader(prog, vs); | |
| gl.attachShader(prog, fs); | |
| gl.linkProgram(prog); | |
| gl.deleteShader(vs); | |
| gl.deleteShader(fs); | |
| if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) { | |
| console.error(gl.getProgramInfoLog(prog)); | |
| gl.deleteProgram(prog); | |
| return null; | |
| } | |
| return prog; | |
| } | |
| /** | |
| * Initialize WebGL rendering pipeline and buffers. | |
| * | |
| * @returns {boolean} true if WebGL is available and initialized; false otherwise. | |
| * | |
| * Behavior: | |
| * - Creates a WebGL context, compiles/link shaders, sets up a fullscreen quad VBO. | |
| * - Locates uniforms/attributes, and disables depth/cull for 2D rendering. | |
| * - Configures clear color to opaque black. | |
| * | |
| * Side effects: | |
| * - Mutates `gl`, `glProgram`, `positionBuf`, attribute/uniform locations. | |
| */ | |
| function initWebGL(): boolean { | |
| if (!canvas) return false; | |
| gl = canvas.getContext('webgl', { | |
| antialias: true, | |
| alpha: true, | |
| preserveDrawingBuffer: false, | |
| powerPreference: 'high-performance' | |
| }); | |
| if (!gl) return false; | |
| gl.clearColor(0, 0, 0, 1); | |
| glProgram = createProgram(WEBGL_VERTEX_SHADER, WEBGL_FRAGMENT_SHADER); | |
| if (!glProgram) return false; | |
| gl.useProgram(glProgram); | |
| positionBuf = gl.createBuffer(); | |
| gl.bindBuffer(gl.ARRAY_BUFFER, positionBuf); | |
| gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([ | |
| -1, -1, 1, -1, -1, 1, | |
| 1, -1, 1, 1, -1, 1 | |
| ]), gl.STATIC_DRAW); | |
| a_position = gl.getAttribLocation(glProgram, 'a_position'); | |
| gl.enableVertexAttribArray(a_position); | |
| gl.vertexAttribPointer(a_position, 2, gl.FLOAT, false, 0, 0); | |
| uTime = gl.getUniformLocation(glProgram, 'u_time'); | |
| uMouse = gl.getUniformLocation(glProgram, 'u_mouse'); | |
| uRes = gl.getUniformLocation(glProgram, 'u_resolution'); | |
| uColor = gl.getUniformLocation(glProgram, 'u_color'); | |
| uBg = gl.getUniformLocation(glProgram, 'u_background'); | |
| uSpeed = gl.getUniformLocation(glProgram, 'u_speed'); | |
| uDetail = gl.getUniformLocation(glProgram, 'u_detail'); | |
| gl.disable(gl.DEPTH_TEST); | |
| gl.disable(gl.CULL_FACE); | |
| return true; | |
| } | |
| /** | |
| * Resize the canvas to match CSS size * devicePixelRatio (clamped by `maxDpr`). | |
| * Updates WebGL viewport and `u_resolution` uniform; WebGPU uses swapchain size implicitly. | |
| */ | |
| function setSize() { | |
| if (!canvas) return; | |
| const dpr = Math.min((window.devicePixelRatio || 1), maxDpr); | |
| const rect = canvas.getBoundingClientRect(); | |
| const w = Math.max(1, Math.floor(rect.width * dpr)); | |
| const h = Math.max(1, Math.floor(rect.height * dpr)); | |
| if (canvas.width !== w || canvas.height !== h) { | |
| canvas.width = w; | |
| canvas.height = h; | |
| } | |
| if (isWebGPU) { | |
| // WebGPU doesn't need explicit viewport setting | |
| } else if (gl && uRes) { | |
| gl.viewport(0, 0, w, h); | |
| gl.uniform2f(uRes, w, h); | |
| } | |
| } | |
| /** | |
| * Render a frame via WebGPU. | |
| * | |
| * Inputs: | |
| * - Reads `u_speed`, `u_detail`, `u_color`, `u_background`, `mouseX`, `mouseY`, current time. | |
| * | |
| * Behavior: | |
| * - Packs uniforms into a Float32Array and writes to `uniformBuffer`. | |
| * - Records a single-pass render command, draws 6 vertices (two triangles). | |
| * - Submits the command buffer to `device.queue`. | |
| * | |
| * Preconditions: | |
| * - `device`, `gpuContext`, `pipeline`, `bindGroup`, `uniformBuffer`, `canvas` are non-null. | |
| */ | |
| function renderWebGPU() { | |
| if (!device || !gpuContext || !pipeline || !bindGroup || !uniformBuffer || !canvas) return; | |
| const rect = canvas.getBoundingClientRect(); | |
| const dpr = Math.min((window.devicePixelRatio || 1), maxDpr); | |
| const w = rect.width * dpr; | |
| const h = rect.height * dpr; | |
| const uniformData = new Float32Array(20); | |
| uniformData[0] = w; | |
| uniformData[1] = h; | |
| uniformData[2] = (performance.now() - start) * 0.001; | |
| uniformData[3] = u_speed; | |
| uniformData[4] = u_detail; | |
| uniformData[8] = u_color[0]; | |
| uniformData[9] = u_color[1]; | |
| uniformData[10] = u_color[2]; | |
| uniformData[12] = u_background[0]; | |
| uniformData[13] = u_background[1]; | |
| uniformData[14] = u_background[2]; | |
| uniformData[15] = u_background[3]; | |
| uniformData[16] = mouseX; | |
| uniformData[17] = mouseY; | |
| device.queue.writeBuffer(uniformBuffer, 0, uniformData); | |
| const commandEncoder = device.createCommandEncoder(); | |
| const textureView = gpuContext.getCurrentTexture().createView(); | |
| const renderPass = commandEncoder.beginRenderPass({ | |
| colorAttachments: [{ | |
| view: textureView, | |
| clearValue: { r: 0, g: 0, b: 0, a: 1 }, | |
| loadOp: 'clear', | |
| storeOp: 'store' | |
| }] | |
| }); | |
| renderPass.setPipeline(pipeline); | |
| renderPass.setBindGroup(0, bindGroup); | |
| renderPass.draw(6); | |
| renderPass.end(); | |
| device.queue.submit([commandEncoder.finish()]); | |
| } | |
| /** | |
| * Render a frame via WebGL. | |
| * | |
| * Behavior: | |
| * - Computes elapsed time in seconds, binds program, clears color buffer. | |
| * - Updates all active uniforms, and draws 6 vertices forming two triangles. | |
| * | |
| * Preconditions: | |
| * - `gl` and `glProgram` are initialized by `initWebGL()`. | |
| */ | |
| function renderWebGL() { | |
| if (!gl || !glProgram) return; | |
| const sec = (performance.now() - start) * 0.001; | |
| gl.useProgram(glProgram); | |
| gl.clear(gl.COLOR_BUFFER_BIT); | |
| if (uTime) gl.uniform1f(uTime, sec); | |
| if (uMouse) gl.uniform2f(uMouse, mouseX, mouseY); | |
| if (uColor) gl.uniform3f(uColor, u_color[0], u_color[1], u_color[2]); | |
| if (uBg) gl.uniform4f(uBg, u_background[0], u_background[1], u_background[2], u_background[3]); | |
| if (uSpeed) gl.uniform1f(uSpeed, u_speed); | |
| if (uDetail) gl.uniform1f(uDetail, u_detail); | |
| gl.drawArrays(gl.TRIANGLES, 0, 6); | |
| } | |
| /** | |
| * Main animation loop. | |
| * | |
| * Behavior: | |
| * - Lazily sets `start` time. | |
| * - Renders one frame using the active backend (WebGPU/WebGL). | |
| * - Schedules the next frame via `requestAnimationFrame`, storing the id in `raf`. | |
| */ | |
| function loop() { | |
| if (!start) start = performance.now(); | |
| if (isWebGPU) { | |
| renderWebGPU(); | |
| } else { | |
| renderWebGL(); | |
| } | |
| raf = requestAnimationFrame(loop); | |
| } | |
| /** | |
| * Initialize and setup.Lifecycle effect. | |
| * | |
| * Behavior: | |
| * - On mount: attempts WebGPU, falls back to WebGL; logs which path is active. | |
| * - Registers `mousemove` and `resize` listeners; normalizes mouse to [0,1] in canvas space. | |
| * - Calls `setSize()` and starts the animation `loop()`. | |
| * - On destroy: cancels RAF, removes listeners, releases GPU/WebGL resources. | |
| * | |
| * Side effects: | |
| * - Mutates `isWebGPU`, scheduling, and GPU/WebGL global state. | |
| */ | |
| $effect(() => { | |
| if (typeof window === 'undefined' || !canvas) return; | |
| const init = async () => { | |
| // Try WebGPU first | |
| if (await initWebGPU()) { | |
| isWebGPU = true; | |
| console.log('Using WebGPU renderer'); | |
| } else if (initWebGL()) { | |
| isWebGPU = false; | |
| console.log('Using WebGL renderer (WebGPU fallback)'); | |
| } else { | |
| console.error('Neither WebGPU nor WebGL are supported'); | |
| return; | |
| } | |
| /** | |
| * Mouse-move handler. | |
| * | |
| * @param {MouseEvent} ev Pointer event. | |
| * Behavior: updates `mouseX`/`mouseY` to normalized [0,1] coords in canvas space (Y flipped). | |
| */ | |
| const onMove = (ev: MouseEvent) => { | |
| if (!canvas) return; | |
| const r = canvas.getBoundingClientRect(); | |
| mouseX = (ev.clientX - r.left) / r.width; | |
| mouseY = 1.0 - (ev.clientY - r.top) / r.height; | |
| }; | |
| /** Resize handler; recomputes backing store size and GL viewport/uniforms. */ | |
| const onResize = () => setSize(); | |
| if (canvas) { | |
| canvas.addEventListener('mousemove', onMove, { passive: true }); | |
| } | |
| window.addEventListener('resize', onResize, { passive: true }); | |
| setSize(); | |
| raf = requestAnimationFrame(loop); | |
| return () => { | |
| cancelAnimationFrame(raf); | |
| window.removeEventListener('resize', onResize); | |
| if (canvas) { | |
| canvas.removeEventListener('mousemove', onMove); | |
| } | |
| // Cleanup WebGPU | |
| if (uniformBuffer) { | |
| uniformBuffer.destroy(); | |
| } | |
| // Cleanup WebGL | |
| if (gl && glProgram) { | |
| gl.deleteProgram(glProgram); | |
| } | |
| if (gl && positionBuf) { | |
| gl.deleteBuffer(positionBuf); | |
| } | |
| }; | |
| }; | |
| let cleanup: (() => void) | undefined; | |
| init().then(fn => cleanup = fn); | |
| return () => cleanup?.(); | |
| }); | |
| </script> | |
| <canvas bind:this={canvas} class={klass}></canvas> | |
| <style> | |
| :global(canvas){ display:block; } | |
| </style> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment