Spaces:
Running
Running
| /** | |
| * 🌿 Ivy's GPU Art Studio | |
| * WebGPU Utility Functions | |
| * | |
| * Common utilities for WebGPU initialization and shader management | |
| */ | |
| // Check if WebGPU is available | |
| async function checkWebGPUSupport() { | |
| if (!navigator.gpu) { | |
| return { supported: false, error: "WebGPU not available in this browser" }; | |
| } | |
| try { | |
| const adapter = await navigator.gpu.requestAdapter(); | |
| if (!adapter) { | |
| return { supported: false, error: "No GPU adapter found" }; | |
| } | |
| const device = await adapter.requestDevice(); | |
| return { supported: true, adapter, device }; | |
| } catch (err) { | |
| return { supported: false, error: err.message }; | |
| } | |
| } | |
| // Initialize WebGPU with canvas | |
| async function initWebGPU(canvas) { | |
| const result = await checkWebGPUSupport(); | |
| if (!result.supported) { | |
| throw new Error(result.error); | |
| } | |
| const { adapter, device } = result; | |
| // Configure canvas context | |
| const context = canvas.getContext("webgpu"); | |
| const format = navigator.gpu.getPreferredCanvasFormat(); | |
| context.configure({ | |
| device, | |
| format, | |
| alphaMode: "premultiplied" | |
| }); | |
| return { adapter, device, context, format }; | |
| } | |
| // Create a shader module from WGSL code | |
| function createShaderModule(device, code, label = "shader") { | |
| return device.createShaderModule({ | |
| label, | |
| code | |
| }); | |
| } | |
| // Create a render pipeline | |
| function createRenderPipeline( | |
| device, | |
| { | |
| shaderModule, | |
| vertexEntryPoint = "vertexMain", | |
| fragmentEntryPoint = "fragmentMain", | |
| format, | |
| topology = "triangle-list", | |
| vertexBufferLayouts = [], | |
| bindGroupLayouts = [] | |
| } | |
| ) { | |
| const pipelineLayout = bindGroupLayouts.length > 0 ? device.createPipelineLayout({ bindGroupLayouts }) : "auto"; | |
| return device.createRenderPipeline({ | |
| label: "render pipeline", | |
| layout: pipelineLayout, | |
| vertex: { | |
| module: shaderModule, | |
| entryPoint: vertexEntryPoint, | |
| buffers: vertexBufferLayouts | |
| }, | |
| fragment: { | |
| module: shaderModule, | |
| entryPoint: fragmentEntryPoint, | |
| targets: [{ format }] | |
| }, | |
| primitive: { | |
| topology | |
| } | |
| }); | |
| } | |
| // Create a compute pipeline | |
| function createComputePipeline(device, { shaderModule, entryPoint = "main", bindGroupLayouts = [] }) { | |
| const pipelineLayout = bindGroupLayouts.length > 0 ? device.createPipelineLayout({ bindGroupLayouts }) : "auto"; | |
| return device.createComputePipeline({ | |
| label: "compute pipeline", | |
| layout: pipelineLayout, | |
| compute: { | |
| module: shaderModule, | |
| entryPoint | |
| } | |
| }); | |
| } | |
| // Create a uniform buffer | |
| function createUniformBuffer(device, size, label = "uniform buffer") { | |
| return device.createBuffer({ | |
| label, | |
| size, | |
| usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST | |
| }); | |
| } | |
| // Create a storage buffer | |
| function createStorageBuffer(device, size, label = "storage buffer") { | |
| return device.createBuffer({ | |
| label, | |
| size, | |
| usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | |
| }); | |
| } | |
| // Create a vertex buffer | |
| function createVertexBuffer(device, data, label = "vertex buffer") { | |
| const buffer = device.createBuffer({ | |
| label, | |
| size: data.byteLength, | |
| usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST | |
| }); | |
| device.queue.writeBuffer(buffer, 0, data); | |
| return buffer; | |
| } | |
| // Write data to a buffer | |
| function writeBuffer(device, buffer, data, offset = 0) { | |
| device.queue.writeBuffer(buffer, offset, data); | |
| } | |
| // Create full-screen quad vertices (large triangle trick) | |
| function getFullScreenTriangleVertices() { | |
| // Single large triangle that covers clip space | |
| return new Float32Array([ | |
| -1, | |
| -1, // bottom-left | |
| 3, | |
| -1, // far right | |
| -1, | |
| 3 // far top | |
| ]); | |
| } | |
| // Resize canvas to match display size | |
| function resizeCanvasToDisplaySize(canvas, multiplier = 1) { | |
| const width = Math.floor(canvas.clientWidth * multiplier); | |
| const height = Math.floor(canvas.clientHeight * multiplier); | |
| if (canvas.width !== width || canvas.height !== height) { | |
| canvas.width = width; | |
| canvas.height = height; | |
| return true; | |
| } | |
| return false; | |
| } | |
| // Create a render pass | |
| function beginRenderPass(commandEncoder, context, clearColor = { r: 0, g: 0, b: 0, a: 1 }) { | |
| return commandEncoder.beginRenderPass({ | |
| colorAttachments: [ | |
| { | |
| view: context.getCurrentTexture().createView(), | |
| clearValue: clearColor, | |
| loadOp: "clear", | |
| storeOp: "store" | |
| } | |
| ] | |
| }); | |
| } | |
| // Animation frame helper with delta time | |
| class AnimationLoop { | |
| constructor(callback) { | |
| this.callback = callback; | |
| this.running = false; | |
| this.lastTime = 0; | |
| this.frame = this.frame.bind(this); | |
| } | |
| start() { | |
| if (!this.running) { | |
| this.running = true; | |
| this.lastTime = performance.now(); | |
| requestAnimationFrame(this.frame); | |
| } | |
| } | |
| stop() { | |
| this.running = false; | |
| } | |
| frame(currentTime) { | |
| if (!this.running) return; | |
| const deltaTime = (currentTime - this.lastTime) / 1000; // Convert to seconds | |
| this.lastTime = currentTime; | |
| this.callback(deltaTime, currentTime / 1000); | |
| requestAnimationFrame(this.frame); | |
| } | |
| } | |
| // Color utilities | |
| const ColorUtils = { | |
| // HSL to RGB conversion | |
| hslToRgb(h, s, l) { | |
| let r, g, b; | |
| if (s === 0) { | |
| r = g = b = l; | |
| } else { | |
| const hue2rgb = (p, q, t) => { | |
| if (t < 0) t += 1; | |
| if (t > 1) t -= 1; | |
| if (t < 1 / 6) return p + (q - p) * 6 * t; | |
| if (t < 1 / 2) return q; | |
| if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6; | |
| return p; | |
| }; | |
| const q = l < 0.5 ? l * (1 + s) : l + s - l * s; | |
| const p = 2 * l - q; | |
| r = hue2rgb(p, q, h + 1 / 3); | |
| g = hue2rgb(p, q, h); | |
| b = hue2rgb(p, q, h - 1 / 3); | |
| } | |
| return [r, g, b]; | |
| }, | |
| // Create a color palette | |
| createPalette(name) { | |
| const palettes = { | |
| rainbow: [ | |
| [1.0, 0.0, 0.0], | |
| [1.0, 0.5, 0.0], | |
| [1.0, 1.0, 0.0], | |
| [0.0, 1.0, 0.0], | |
| [0.0, 1.0, 1.0], | |
| [0.0, 0.0, 1.0], | |
| [0.5, 0.0, 1.0], | |
| [1.0, 0.0, 1.0] | |
| ], | |
| fire: [ | |
| [0.0, 0.0, 0.0], | |
| [0.5, 0.0, 0.0], | |
| [1.0, 0.0, 0.0], | |
| [1.0, 0.5, 0.0], | |
| [1.0, 1.0, 0.0], | |
| [1.0, 1.0, 1.0] | |
| ], | |
| ocean: [ | |
| [0.0, 0.0, 0.2], | |
| [0.0, 0.2, 0.4], | |
| [0.0, 0.4, 0.6], | |
| [0.0, 0.6, 0.8], | |
| [0.2, 0.8, 1.0], | |
| [0.6, 1.0, 1.0] | |
| ], | |
| neon: [ | |
| [0.0, 0.0, 0.0], | |
| [1.0, 0.0, 0.5], | |
| [0.0, 1.0, 1.0], | |
| [1.0, 0.0, 1.0], | |
| [0.0, 1.0, 0.5], | |
| [1.0, 1.0, 0.0] | |
| ], | |
| grayscale: [ | |
| [0.0, 0.0, 0.0], | |
| [0.2, 0.2, 0.2], | |
| [0.4, 0.4, 0.4], | |
| [0.6, 0.6, 0.6], | |
| [0.8, 0.8, 0.8], | |
| [1.0, 1.0, 1.0] | |
| ] | |
| }; | |
| return palettes[name] || palettes.rainbow; | |
| } | |
| }; | |
| // Mouse/Touch input handler | |
| class InputHandler { | |
| constructor(canvas) { | |
| this.canvas = canvas; | |
| this.mouseX = 0; | |
| this.mouseY = 0; | |
| this.prevMouseX = 0; | |
| this.prevMouseY = 0; | |
| this.deltaX = 0; | |
| this.deltaY = 0; | |
| this.isPressed = false; | |
| this.wheel = 0; | |
| this.setupEventListeners(); | |
| } | |
| setupEventListeners() { | |
| // Mouse events | |
| this.canvas.addEventListener("mousemove", e => { | |
| const rect = this.canvas.getBoundingClientRect(); | |
| this.prevMouseX = this.mouseX; | |
| this.prevMouseY = this.mouseY; | |
| this.mouseX = (e.clientX - rect.left) / rect.width; | |
| this.mouseY = 1.0 - (e.clientY - rect.top) / rect.height; // Flip Y | |
| this.deltaX = this.mouseX - this.prevMouseX; | |
| this.deltaY = this.mouseY - this.prevMouseY; | |
| }); | |
| this.canvas.addEventListener("mousedown", () => { | |
| this.isPressed = true; | |
| }); | |
| this.canvas.addEventListener("mouseup", () => { | |
| this.isPressed = false; | |
| }); | |
| this.canvas.addEventListener("mouseleave", () => { | |
| this.isPressed = false; | |
| }); | |
| this.canvas.addEventListener( | |
| "wheel", | |
| e => { | |
| e.preventDefault(); | |
| this.wheel = e.deltaY; | |
| }, | |
| { passive: false } | |
| ); | |
| // Touch events | |
| this.canvas.addEventListener( | |
| "touchstart", | |
| e => { | |
| e.preventDefault(); | |
| this.isPressed = true; | |
| this.updateTouchPosition(e.touches[0]); | |
| }, | |
| { passive: false } | |
| ); | |
| this.canvas.addEventListener( | |
| "touchmove", | |
| e => { | |
| e.preventDefault(); | |
| this.updateTouchPosition(e.touches[0]); | |
| }, | |
| { passive: false } | |
| ); | |
| this.canvas.addEventListener("touchend", () => { | |
| this.isPressed = false; | |
| }); | |
| } | |
| updateTouchPosition(touch) { | |
| const rect = this.canvas.getBoundingClientRect(); | |
| this.prevMouseX = this.mouseX; | |
| this.prevMouseY = this.mouseY; | |
| this.mouseX = (touch.clientX - rect.left) / rect.width; | |
| this.mouseY = 1.0 - (touch.clientY - rect.top) / rect.height; | |
| this.deltaX = this.mouseX - this.prevMouseX; | |
| this.deltaY = this.mouseY - this.prevMouseY; | |
| } | |
| consumeWheel() { | |
| const w = this.wheel; | |
| this.wheel = 0; | |
| return w; | |
| } | |
| // Get normalized coordinates (-1 to 1) | |
| getNormalizedCoords() { | |
| return { | |
| x: this.mouseX * 2 - 1, | |
| y: this.mouseY * 2 - 1 | |
| }; | |
| } | |
| } | |
| // Export all utilities | |
| window.WebGPUUtils = { | |
| checkWebGPUSupport, | |
| initWebGPU, | |
| createShaderModule, | |
| createRenderPipeline, | |
| createComputePipeline, | |
| createUniformBuffer, | |
| createStorageBuffer, | |
| createVertexBuffer, | |
| writeBuffer, | |
| getFullScreenTriangleVertices, | |
| resizeCanvasToDisplaySize, | |
| beginRenderPass, | |
| AnimationLoop, | |
| ColorUtils, | |
| InputHandler | |
| }; | |