From c39bdac3712696df56e146481145f546c8fdce99 Mon Sep 17 00:00:00 2001 From: Shane Cranor Date: Fri, 18 Apr 2025 21:52:55 -0500 Subject: [PATCH 01/47] use webgl --- index.js | 80 ++++++--- workerWebGL.js | 460 +++++++++++++++++++++++-------------------------- 2 files changed, 270 insertions(+), 270 deletions(-) diff --git a/index.js b/index.js index 0748a28..8cdcb9f 100644 --- a/index.js +++ b/index.js @@ -1,30 +1,51 @@ import { createInfo, modifyInfo } from "/qbist.js" import { loadStateFromParam } from "/qbistListeners.js" + +// Get UI elements +const loadingOverlay = document.getElementById("loadingOverlay") +const loadingBar = document.getElementById("loadingBar") +loadingOverlay.style.display = "none" + +// Keep track of which canvases have been transferred +const transferredCanvases = new WeakSet() + function drawQbist(canvas, info, oversampling = 0) { return new Promise((resolve, reject) => { if (typeof Worker === "undefined") { reject(new Error("Web Workers are not supported in this browser")) return } - const ctx = canvas.getContext("2d") - const width = canvas.width - const height = canvas.height + + // Check if canvas was already transferred + if (transferredCanvases.has(canvas)) { + // For already transferred canvases, just send the new info to update + const worker = canvas.worker + worker.postMessage({ + type: "update", + info: info, + }) + resolve() + return + } // Create the worker instance - const worker = new Worker("worker.js", { type: "module" }) + const worker = new Worker("workerWebGL.js", { type: "module" }) + + // Store worker reference on the canvas + canvas.worker = worker + + // Create an OffscreenCanvas for WebGL rendering + const offscreen = canvas.transferControlToOffscreen() + + // Mark this canvas as transferred + transferredCanvases.add(canvas) + // Listen for messages from the worker worker.addEventListener("message", (e) => { - const { command } = e.data - if (command === "progress") { - const { progress } = e.data - loadingOverlay.style.display = "flex" - loadingBar.style.width = `${progress}%` - } else if (command === "rendered") { - const { imageData } = e.data - const data = new Uint8ClampedArray(imageData) - const imgData = new ImageData(data, width, height) - ctx.putImageData(imgData, 0, 0) - worker.terminate() // Clean up the worker + if (e.data.command === "rendered") { + if (!e.data.keepAlive) { + worker.terminate() // Clean up the worker + } loadingOverlay.style.display = "none" resolve() // Resolve the Promise when rendering is complete } @@ -37,15 +58,24 @@ function drawQbist(canvas, info, oversampling = 0) { reject(err) }) - // Prepare and send the payload to the worker - const payload = { - command: "render", - info, - width, - height, - oversampling, + // Show loading overlay for main canvas only + if (canvas.id === "mainPattern") { + loadingOverlay.style.display = "flex" + loadingBar.style.width = "100%" } - worker.postMessage(payload) + + // Initialize the WebGL worker with the canvas and formula + worker.postMessage( + { + type: "init", + canvas: offscreen, + width: canvas.width, + height: canvas.height, + info: info, + keepAlive: true, // Keep the worker alive for future updates + }, + [offscreen] + ) }) } @@ -110,7 +140,3 @@ export async function downloadImage(outputWidth, outputHeight, oversampling) { link.click() document.body.removeChild(link) } - -const loadingOverlay = document.getElementById("loadingOverlay") -const loadingBar = document.getElementById("loadingBar") -loadingOverlay.style.display = "none" diff --git a/workerWebGL.js b/workerWebGL.js index e25d2c7..d0eea3a 100644 --- a/workerWebGL.js +++ b/workerWebGL.js @@ -1,132 +1,217 @@ // worker.js -// Listen for the canvas from the main thread. +// Global variables and context +let gl = null +let program = null +let mainFormula = null +let uResolutionLoc, + uTimeLoc, + uTransformSequenceLoc, + uSourceLoc, + uControlLoc, + uDestLoc +let isSingleRender = false +let keepAlive = false + +// Shader sources +const vertexShaderSource = `#version 300 es + in vec2 aPosition; + out vec2 vUV; + void main() { + vUV = aPosition * 0.5 + 0.5; + gl_Position = vec4(aPosition, 0.0, 1.0); + }` + +const fragmentShaderSource = `#version 300 es + precision highp float; + precision highp int; + in vec2 vUV; + uniform ivec2 uResolution; + uniform float uTime; + uniform int uTransformSequence[36]; + uniform int uSource[36]; + uniform int uControl[36]; + uniform int uDest[36]; + out vec4 outColor; + + #define OVERSAMPLING 2 + const int MAX_TRANSFORMS = 36; + const int NUM_REGISTERS = 6; + + void main() { + vec2 pixelCoord = vUV * vec2(uResolution); + vec3 accum = vec3(0.0); + int samples = OVERSAMPLING * OVERSAMPLING; + for (int oy = 0; oy < OVERSAMPLING; oy++) { + for (int ox = 0; ox < OVERSAMPLING; ox++) { + vec2 subPixel = (pixelCoord * float(OVERSAMPLING) + vec2(float(ox), float(oy))) / + (vec2(uResolution) * float(OVERSAMPLING)); + vec3 r[NUM_REGISTERS]; + for (int i = 0; i < NUM_REGISTERS; i++) { + r[i] = (vec3(subPixel, float(i) / float(NUM_REGISTERS)) + vec3(0,0,uTime * 0.1)) * 1.0; + } + for (int i = 0; i < MAX_TRANSFORMS; i++) { + int t = uTransformSequence[i]; + int sr = uSource[i]; + int cr = uControl[i]; + int dr = uDest[i]; + vec3 src = r[sr]; + vec3 ctrl = r[cr]; + if (t == 0) { + float scalarProd = dot(src, ctrl); + r[dr] = src * scalarProd; + } else if (t == 1) { + r[dr] = mod(src + ctrl, 1.0); + } else if (t == 2) { + r[dr] = mod(src - ctrl, 1.0); + } else if (t == 3) { + r[dr] = vec3(src.y, src.z, src.x); + } else if (t == 4) { + r[dr] = vec3(src.z, src.x, src.y); + } else if (t == 5) { + r[dr] = src * ctrl; + } else if (t == 6) { + r[dr] = vec3(0.5 + 0.5*sin(20.0*src.x*ctrl.x), + 0.5 + 0.5*sin(20.0*src.y*ctrl.y), + 0.5 + 0.5*sin(20.0*src.z*ctrl.z)); + } else if (t == 7) { + float sum = ctrl.x + ctrl.y + ctrl.z; + r[dr] = sum > 0.5 ? src : ctrl; + } else if (t == 8) { + r[dr] = vec3(1.0) - src; + } + } + accum += r[0]; + } + } + vec3 color = accum / float(samples); + outColor = vec4(color, 1.0); + }` + +// WebGL setup functions +function createShader(gl, type, source) { + const shader = gl.createShader(type) + gl.shaderSource(shader, source) + gl.compileShader(shader) + if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { + console.error("Shader compile error:", gl.getShaderInfoLog(shader)) + gl.deleteShader(shader) + return null + } + return shader +} + +function createProgram(gl, vsSource, fsSource) { + const vertexShader = createShader(gl, gl.VERTEX_SHADER, vsSource) + const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fsSource) + const program = gl.createProgram() + gl.attachShader(program, vertexShader) + gl.attachShader(program, fragmentShader) + gl.linkProgram(program) + if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { + console.error("Program link error:", gl.getProgramInfoLog(program)) + gl.deleteProgram(program) + return null + } + return program +} + +// Formula generation functions +function randomInt(min, max) { + return Math.floor(Math.random() * (max - min)) + min +} + +function createInfo() { + const info = { + transformSequence: [], + source: [], + control: [], + dest: [], + } + for (let k = 0; k < 36; k++) { + info.transformSequence.push(randomInt(0, 9)) + info.source.push(randomInt(0, 6)) + info.control.push(randomInt(0, 6)) + info.dest.push(randomInt(0, 6)) + } + return info +} + +function uploadFormula(formula) { + if (!gl || !program) return + gl.uniform1iv( + uTransformSequenceLoc, + new Int32Array(formula.transformSequence) + ) + gl.uniform1iv(uSourceLoc, new Int32Array(formula.source)) + gl.uniform1iv(uControlLoc, new Int32Array(formula.control)) + gl.uniform1iv(uDestLoc, new Int32Array(formula.dest)) +} + +function loadStateFromParam(stateBase64) { + try { + const stateJSON = atob(stateBase64) + const stateObj = JSON.parse(stateJSON) + if ( + stateObj.transformSequence && + stateObj.source && + stateObj.control && + stateObj.dest + ) { + return stateObj + } + console.error("Invalid pattern state") + return null + } catch (e) { + console.error("Error loading state:", e) + return null + } +} + +function render(time) { + if (!gl || !program) return + const t = time * 0.001 + gl.uniform1f(uTimeLoc, t) + + gl.viewport(0, 0, gl.canvas.width, gl.canvas.height) + gl.clearColor(0, 0, 0, 1) + gl.clear(gl.COLOR_BUFFER_BIT) + gl.drawArrays(gl.TRIANGLES, 0, 6) + + if (isSingleRender && !keepAlive) { + self.postMessage({ command: "rendered", keepAlive: false }) + } else { + self.postMessage({ command: "rendered", keepAlive: true }) + requestAnimationFrame(render) + } +} + +// Message handler self.addEventListener("message", (event) => { + if (event.data.type === "update") { + mainFormula = event.data.info + uploadFormula(mainFormula) + requestAnimationFrame(render) + return + } + if (!event.data.canvas) { console.error("No canvas provided in the message.") return } + const canvas = event.data.canvas - const gl = canvas.getContext("webgl2", { antialias: true }) + gl = canvas.getContext("webgl2", { antialias: true }) if (!gl) { console.error("WebGL2 is not available in this worker.") return } - // --- Shader Sources --- - - // Vertex shader: simple pass-through that calculates UV coordinates. - const vertexShaderSource = `#version 300 es - in vec2 aPosition; - out vec2 vUV; - void main() { - vUV = aPosition * 0.5 + 0.5; - gl_Position = vec4(aPosition, 0.0, 1.0); - }` - - // Fragment shader: implements the Qbist algorithm with a uniform time value (uTime) - // to animate the image. It uses fixed oversampling for anti-aliasing. - const fragmentShaderSource = `#version 300 es - precision highp float; - precision highp int; - in vec2 vUV; - uniform ivec2 uResolution; - uniform float uTime; - // The formula arrays (each with 36 elements) - uniform int uTransformSequence[36]; - uniform int uSource[36]; - uniform int uControl[36]; - uniform int uDest[36]; - out vec4 outColor; - - #define OVERSAMPLING 2 - const int MAX_TRANSFORMS = 36; - const int NUM_REGISTERS = 6; - - void main() { - vec2 pixelCoord = vUV * vec2(uResolution); - vec3 accum = vec3(0.0); - int samples = OVERSAMPLING * OVERSAMPLING; - // Loop over subpixel samples for anti-aliasing. - for (int oy = 0; oy < OVERSAMPLING; oy++) { - for (int ox = 0; ox < OVERSAMPLING; ox++) { - vec2 subPixel = (pixelCoord * float(OVERSAMPLING) + vec2(float(ox), float(oy))) / - (vec2(uResolution) * float(OVERSAMPLING)); - // Initialize registers with the subpixel coordinate and add a time offset. - vec3 r[NUM_REGISTERS]; - for (int i = 0; i < NUM_REGISTERS; i++) { - r[i] = (vec3(subPixel, float(i) / float(NUM_REGISTERS)) + vec3(0,0,uTime * 0.1)) * 1.0; - } - // Apply each of the 36 transformations. - for (int i = 0; i < MAX_TRANSFORMS; i++) { - int t = uTransformSequence[i]; - int sr = uSource[i]; - int cr = uControl[i]; - int dr = uDest[i]; - vec3 src = r[sr]; - vec3 ctrl = r[cr]; - if (t == 0) { // PROJECTION - float scalarProd = dot(src, ctrl); - r[dr] = src * scalarProd; - } else if (t == 1) { // SHIFT - r[dr] = mod(src + ctrl, 1.0); - } else if (t == 2) { // SHIFTBACK - r[dr] = mod(src - ctrl, 1.0); - } else if (t == 3) { // ROTATE - r[dr] = vec3(src.y, src.z, src.x); - } else if (t == 4) { // ROTATE2 - r[dr] = vec3(src.z, src.x, src.y); - } else if (t == 5) { // MULTIPLY - r[dr] = src * ctrl; - } else if (t == 6) { // SINE - r[dr] = vec3(0.5 + 0.5*sin(20.0*src.x*ctrl.x), - 0.5 + 0.5*sin(20.0*src.y*ctrl.y), - 0.5 + 0.5*sin(20.0*src.z*ctrl.z)); - } else if (t == 7) { // CONDITIONAL - float sum = ctrl.x + ctrl.y + ctrl.z; - r[dr] = sum > 0.5 ? src : ctrl; - } else if (t == 8) { // COMPLEMENT - r[dr] = vec3(1.0) - src; - } - } - accum += r[0]; - } - } - vec3 color = accum / float(samples); - outColor = vec4(color, 1.0); - }` - - // --- Shader Compilation and Linking --- - function createShader(gl, type, source) { - const shader = gl.createShader(type) - gl.shaderSource(shader, source) - gl.compileShader(shader) - if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { - console.error("Shader compile error:", gl.getShaderInfoLog(shader)) - gl.deleteShader(shader) - return null - } - return shader - } - function createProgram(gl, vsSource, fsSource) { - const vertexShader = createShader(gl, gl.VERTEX_SHADER, vsSource) - const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fsSource) - const program = gl.createProgram() - gl.attachShader(program, vertexShader) - gl.attachShader(program, fragmentShader) - gl.linkProgram(program) - if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { - console.error("Program link error:", gl.getProgramInfoLog(program)) - gl.deleteProgram(program) - return null - } - return program - } - - const program = createProgram(gl, vertexShaderSource, fragmentShaderSource) + // Initialize WebGL + program = createProgram(gl, vertexShaderSource, fragmentShaderSource) gl.useProgram(program) - // --- Set Up Full-Screen Quad --- + // Set up quad geometry const quadVertices = new Float32Array([ -1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1, ]) @@ -139,140 +224,29 @@ self.addEventListener("message", (event) => { gl.enableVertexAttribArray(posLoc) gl.vertexAttribPointer(posLoc, 2, gl.FLOAT, false, 0, 0) - // --- Get Uniform Locations --- - const uResolutionLoc = gl.getUniformLocation(program, "uResolution") - const uTimeLoc = gl.getUniformLocation(program, "uTime") - const uTransformSequenceLoc = gl.getUniformLocation( - program, - "uTransformSequence" - ) - const uSourceLoc = gl.getUniformLocation(program, "uSource") - const uControlLoc = gl.getUniformLocation(program, "uControl") - const uDestLoc = gl.getUniformLocation(program, "uDest") + // Get uniform locations + uResolutionLoc = gl.getUniformLocation(program, "uResolution") + uTimeLoc = gl.getUniformLocation(program, "uTime") + uTransformSequenceLoc = gl.getUniformLocation(program, "uTransformSequence") + uSourceLoc = gl.getUniformLocation(program, "uSource") + uControlLoc = gl.getUniformLocation(program, "uControl") + uDestLoc = gl.getUniformLocation(program, "uDest") gl.uniform2i(uResolutionLoc, canvas.width, canvas.height) - // --- CPU Side: Formula Generation Functions --- - const MAX_TRANSFORMS = 36 - const NUM_REGISTERS = 6 - const NUM_TRANSFORM_TYPES = 9 // 0..8 - - function randomInt(min, max) { - return Math.floor(Math.random() * (max - min)) + min - } - - function createInfo() { - const info = { - transformSequence: [], - source: [], - control: [], - dest: [], - } - for (let k = 0; k < MAX_TRANSFORMS; k++) { - info.transformSequence.push(randomInt(0, NUM_TRANSFORM_TYPES)) - info.source.push(randomInt(0, NUM_REGISTERS)) - info.control.push(randomInt(0, NUM_REGISTERS)) - info.dest.push(randomInt(0, NUM_REGISTERS)) - } - return info - } - - function modifyInfo(oldInfo) { - const newInfo = { - transformSequence: oldInfo.transformSequence.slice(), - source: oldInfo.source.slice(), - control: oldInfo.control.slice(), - dest: oldInfo.dest.slice(), - } - const n = randomInt(0, MAX_TRANSFORMS) - for (let k = 0; k < n; k++) { - switch (randomInt(0, 4)) { - case 0: - newInfo.transformSequence[randomInt(0, MAX_TRANSFORMS)] = randomInt( - 0, - NUM_TRANSFORM_TYPES - ) - break - case 1: - newInfo.source[randomInt(0, MAX_TRANSFORMS)] = randomInt( - 0, - NUM_REGISTERS - ) - break - case 2: - newInfo.control[randomInt(0, MAX_TRANSFORMS)] = randomInt( - 0, - NUM_REGISTERS - ) - break - case 3: - newInfo.dest[randomInt(0, MAX_TRANSFORMS)] = randomInt( - 0, - NUM_REGISTERS - ) - break - } - } - return newInfo - } - - // Global formula for the current animation. - let mainFormula = createInfo() - if (event.data.b64state) { - const stateObj = loadStateFromParam(event.data.b64state) - if (!stateObj) return - mainFormula = stateObj + // Initialize formula + isSingleRender = event.data.type === "init" && event.data.info + keepAlive = event.data.keepAlive + + if (isSingleRender) { + mainFormula = event.data.info + } else if (event.data.type === "init" && event.data.b64state) { + mainFormula = loadStateFromParam(event.data.b64state) + if (!mainFormula) return + } else { + mainFormula = createInfo() } - function uploadFormula(formula) { - gl.uniform1iv( - uTransformSequenceLoc, - new Int32Array(formula.transformSequence) - ) - gl.uniform1iv(uSourceLoc, new Int32Array(formula.source)) - gl.uniform1iv(uControlLoc, new Int32Array(formula.control)) - gl.uniform1iv(uDestLoc, new Int32Array(formula.dest)) - } uploadFormula(mainFormula) - - // --- Animation Loop --- - let lastMutationTime = performance.now() - function render(time) { - const t = time * 0.001 // seconds - gl.uniform1f(uTimeLoc, t) - - // Mutate the formula every 5 seconds. - // if (time - lastMutationTime > 5000) { - // mainFormula = modifyInfo(mainFormula) - // uploadFormula(mainFormula) - // lastMutationTime = time - // } - - gl.viewport(0, 0, canvas.width, canvas.height) - gl.clearColor(0, 0, 0, 1) - gl.clear(gl.COLOR_BUFFER_BIT) - gl.drawArrays(gl.TRIANGLES, 0, 6) - requestAnimationFrame(render) - } requestAnimationFrame(render) }) - -function loadStateFromParam(stateBase64) { - try { - const stateJSON = atob(stateBase64) - const stateObj = JSON.parse(stateJSON) - if ( - stateObj.transformSequence && - stateObj.source && - stateObj.control && - stateObj.dest - ) { - return stateObj - } else { - alert("Invalid pattern state") - } - } catch (e) { - console.error("Error loading state:", e) - alert("Error loading pattern state") - } -} From 86738772b3f027ad79947d26defa6615bc80b4e5 Mon Sep 17 00:00:00 2001 From: Shane Cranor Date: Fri, 18 Apr 2025 22:22:05 -0500 Subject: [PATCH 02/47] flip coords --- workerWebGL.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/workerWebGL.js b/workerWebGL.js index d0eea3a..ef2571e 100644 --- a/workerWebGL.js +++ b/workerWebGL.js @@ -19,7 +19,8 @@ const vertexShaderSource = `#version 300 es out vec2 vUV; void main() { vUV = aPosition * 0.5 + 0.5; - gl_Position = vec4(aPosition, 0.0, 1.0); + // Flip Y coordinate by negating the y component + gl_Position = vec4(aPosition.x, -aPosition.y, 0.0, 1.0); }` const fragmentShaderSource = `#version 300 es @@ -39,7 +40,7 @@ const fragmentShaderSource = `#version 300 es const int NUM_REGISTERS = 6; void main() { - vec2 pixelCoord = vUV * vec2(uResolution); + vec2 pixelCoord = (vUV * vec2(uResolution)); vec3 accum = vec3(0.0); int samples = OVERSAMPLING * OVERSAMPLING; for (int oy = 0; oy < OVERSAMPLING; oy++) { @@ -48,7 +49,7 @@ const fragmentShaderSource = `#version 300 es (vec2(uResolution) * float(OVERSAMPLING)); vec3 r[NUM_REGISTERS]; for (int i = 0; i < NUM_REGISTERS; i++) { - r[i] = (vec3(subPixel, float(i) / float(NUM_REGISTERS)) + vec3(0,0,uTime * 0.1)) * 1.0; + r[i] = (vec3(subPixel, float(i) / float(NUM_REGISTERS))) * 1.0; } for (int i = 0; i < MAX_TRANSFORMS; i++) { int t = uTransformSequence[i]; From b5e15bc649555336b98bb9c9813a4c3ef0d36770 Mon Sep 17 00:00:00 2001 From: Shane Cranor Date: Fri, 18 Apr 2025 22:22:20 -0500 Subject: [PATCH 03/47] import and export functionality --- index.html | 5 ++- qbist.js | 84 +++++++++++++++++++++++++++++------------------ qbistListeners.js | 70 ++++++++++++++++++++++++++++++++++++--- 3 files changed, 122 insertions(+), 37 deletions(-) diff --git a/index.html b/index.html index c77f6c0..696ff7d 100644 --- a/index.html +++ b/index.html @@ -46,7 +46,10 @@

Output Settings

value="2" />
- + +