ivy-gpu-art-studio / js /webgpu-utils.js
Elysia-Suite's picture
Upload 23 files
e5d943e verified
/**
* 🌿 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
};