/** * 🌿 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 };