diff --git a/QbistExporter.js b/QbistExporter.js new file mode 100644 index 0000000..a72be17 --- /dev/null +++ b/QbistExporter.js @@ -0,0 +1,100 @@ +import { QbistRenderer } from "./QbistRenderer.js" +export class QbistExporter { + constructor() { + this.exportCanvas = null + this.renderer = null + } + + async exportImage(info, width, height) { + try { + this.cleanup() + + this.exportCanvas = document.createElement("canvas") + this.exportCanvas.id = "exportCanvas" + this.exportCanvas.width = width + this.exportCanvas.height = height + document.body.appendChild(this.exportCanvas) + console.log(`[Canvas Create] Created export canvas ${width}x${height}`) + + this.renderer = new QbistRenderer(this.exportCanvas) + const result = await this.renderer.render(info, { isExport: true }) + + if (result.kind === "bitmap") { + await this.handleBitmapExport(result.bitmap, width, height) + } else if (result.kind === "pixels") { + await this.handlePixelExport(result.pixels, result.width, result.height) + } + } finally { + this.cleanup() + } + } + + async handleBitmapExport(bitmap, width, height) { + const link = document.createElement("a") + const tempCanvas = document.createElement("canvas") + tempCanvas.width = width + tempCanvas.height = height + console.log( + `[Canvas Create] Created temporary canvas for bitmap export ${width}x${height}` + ) + + const ctx = tempCanvas.getContext("2d") + ctx.drawImage(bitmap, 0, 0) + + link.href = tempCanvas.toDataURL("image/png") + link.download = "qbist.png" + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + console.log(`[Canvas Delete] Removed temporary canvas`) + } + + async handlePixelExport(pixelBuffer, width, height) { + const tempCanvas = document.createElement("canvas") + tempCanvas.width = width + tempCanvas.height = height + console.log( + `[Canvas Create] Created temporary canvas for pixel export ${width}x${height}` + ) + + const ctx = tempCanvas.getContext("2d") + const imageData = new ImageData( + new Uint8ClampedArray(pixelBuffer), + width, + height + ) + + // Need to flip the image vertically since WebGL reads pixels from bottom-left + const flippedCanvas = document.createElement("canvas") + flippedCanvas.width = width + flippedCanvas.height = height + const flippedCtx = flippedCanvas.getContext("2d") + + // Put the pixels on the temporary canvas + ctx.putImageData(imageData, 0, 0) + + // Flip the image by drawing it upside down + flippedCtx.scale(1, -1) + flippedCtx.drawImage(tempCanvas, 0, -height) + + const link = document.createElement("a") + link.href = flippedCanvas.toDataURL("image/png") + link.download = "qbist.png" + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + console.log(`[Canvas Delete] Removed temporary canvases`) + } + + cleanup() { + if (this.renderer) { + this.renderer.cleanup() + this.renderer = null + } + if (this.exportCanvas && this.exportCanvas.parentNode) { + console.log(`[Canvas Delete] Removed export canvas`) + document.body.removeChild(this.exportCanvas) + this.exportCanvas = null + } + } +} diff --git a/QbistRenderer.js b/QbistRenderer.js new file mode 100644 index 0000000..83dc49a --- /dev/null +++ b/QbistRenderer.js @@ -0,0 +1,131 @@ +export class QbistRenderer { + constructor(canvas) { + this.canvas = canvas + this.worker = null + this.isInitialized = false + this.keepAlive = false + console.log(`[Canvas Create] Created renderer for canvas ${canvas.id}`) + this._setupWorker() + } + + _setupWorker() { + if (typeof Worker === "undefined") { + throw new Error("Web Workers are not supported in this browser") + } + + // Cleanup existing worker if any + this.cleanup() + + // Use absolute path for better cross-browser compatibility with module workers + const workerURL = new URL("./workerWebGL.js", window.location.href).href + this.worker = new Worker(workerURL, { type: "module" }) + this.worker.onerror = (err) => { + console.error("Worker error:", err) + this.cleanup() + } + } + + cleanup() { + if (this.worker) { + this.worker.postMessage({ type: "cleanup" }) + this.worker = null + } + this.isInitialized = false + this.keepAlive = false + console.log( + `[Canvas Delete] Cleaned up renderer for canvas ${this.canvas.id}` + ) + } + + async render(info, options = {}) { + const { + keepAlive = false, + refreshEveryFrame = false, + isExport = false, + } = options + this.keepAlive = keepAlive + + return new Promise((resolve, reject) => { + const loadingOverlay = document.getElementById("loadingOverlay") + const loadingBar = document.getElementById("loadingBar") + if (!this.worker) { + this._setupWorker() + } + + const onMessage = (e) => { + if (e.data.command === "rendered") { + if (!this.keepAlive) { + this.worker.removeEventListener("message", onMessage) + } + if (isExport) { + loadingOverlay.style.display = "none" + } + resolve(e.data) + } else if (e.data.command === "error") { + this.worker.removeEventListener("message", onMessage) + reject(new Error(e.data.message)) + loadingOverlay.style.display = "none" + } + } + + this.worker.addEventListener("message", onMessage) + + try { + if (isExport) { + loadingOverlay.style.display = "flex" + loadingBar.style.width = "100%" + } + + if (!this.isInitialized) { + let canvas = this.canvas + let transferList = [] + + // Try to use OffscreenCanvas + try { + const offscreen = this.canvas.transferControlToOffscreen() + canvas = offscreen + transferList = [offscreen] + } catch (err) { + console.warn( + "OffscreenCanvas not supported, falling back to regular canvas", + err + ) + } + + this.worker.postMessage( + { + type: "init", + canvas: canvas, + info, + keepAlive: this.keepAlive, + refreshEveryFrame, + }, + transferList + ) + this.isInitialized = true + } else { + // Just update the info for subsequent renders + this.worker.postMessage({ + type: "update", + info, + keepAlive: this.keepAlive, + refreshEveryFrame, + }) + } + } catch (err) { + this.worker.removeEventListener("message", onMessage) + reject(err) + } + }) + } + + update(info) { + if (this.worker) { + this.worker.postMessage({ + type: "update", + info, + keepAlive: this.keepAlive, + }) + } + } +} diff --git a/index.html b/index.html index c77f6c0..f1d840d 100644 --- a/index.html +++ b/index.html @@ -46,7 +46,10 @@

Output Settings

value="2" />
- + +