From aae81b8a04ffe28567e6fe92e4d8071849b2af8f Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Wed, 30 Apr 2025 07:35:17 +0200 Subject: [PATCH] :tada: Add wasm playground environment --- frontend/resources/wasm-playground/js/lib.js | 132 ++++++++++++++++++ frontend/resources/wasm-playground/rects.html | 69 +++++++++ frontend/scripts/_helpers.js | 13 ++ frontend/scripts/build-app-assets.js | 1 + frontend/scripts/watch.js | 7 + 5 files changed, 222 insertions(+) create mode 100644 frontend/resources/wasm-playground/js/lib.js create mode 100644 frontend/resources/wasm-playground/rects.html diff --git a/frontend/resources/wasm-playground/js/lib.js b/frontend/resources/wasm-playground/js/lib.js new file mode 100644 index 000000000..124b86a3c --- /dev/null +++ b/frontend/resources/wasm-playground/js/lib.js @@ -0,0 +1,132 @@ +let Module = null; + +let scale = 1; +let offsetX = 0; +let offsetY = 0; + +let isPanning = false; +let lastX = 0; +let lastY = 0; + +export function init(moduleInstance) { + Module = moduleInstance; +} + +export function assignCanvas(canvas) { + const glModule = Module.GL; + const context = canvas.getContext("webgl2", { + antialias: true, + depth: true, + alpha: false, + stencil: true, + preserveDrawingBuffer: true, + }); + + const handle = glModule.registerContext(context, { majorVersion: 2 }); + glModule.makeContextCurrent(handle); + context.getExtension("WEBGL_debug_renderer_info"); + + Module._init(canvas.width, canvas.height); + Module._set_render_options(0, 1); +} + +export function hexToU32ARGB(hex, opacity = 1) { + const rgb = parseInt(hex.slice(1), 16); + const a = Math.floor(opacity * 0xFF); + const argb = (a << 24) | rgb; + return argb >>> 0; +} + +export function getRandomInt(min, max) { + return Math.floor(Math.random() * (max - min)) + min; +} + +export function getRandomColor() { + const r = getRandomInt(0, 256).toString(16).padStart(2, '0'); + const g = getRandomInt(0, 256).toString(16).padStart(2, '0'); + const b = getRandomInt(0, 256).toString(16).padStart(2, '0'); + return `#${r}${g}${b}`; +} + +export function getRandomFloat(min, max) { + return Math.random() * (max - min) + min; +} + +function getU32(id) { + const hex = id.replace(/-/g, ""); + const buffer = new Uint32Array(4); + for (let i = 0; i < 4; i++) { + buffer[i] = parseInt(hex.slice(i * 8, (i + 1) * 8), 16); + } + return buffer; +} + +function heapU32SetUUID(id, heap, offset) { + const buffer = getU32(id); + heap.set(buffer, offset); + return buffer; +} + +function ptr8ToPtr32(ptr8) { + return ptr8 >>> 2; +} + +function allocBytes(size) { + return Module._alloc_bytes(size); +} + +function getHeapU32() { + return Module.HEAPU32; +} + +export function setShapeChildren(shapeIds) { + const offset = allocBytes(shapeIds.length * 16); + const heap = getHeapU32(); + let currentOffset = offset; + for (const id of shapeIds) { + heapU32SetUUID(id, heap, ptr8ToPtr32(currentOffset)); + currentOffset += 16; + } + return Module._set_children(); +} + +export function useShape(id) { + const buffer = getU32(id); + Module._use_shape(...buffer); +} + +export function setupInteraction(canvas) { + canvas.addEventListener("wheel", (e) => { + e.preventDefault(); + const zoomFactor = e.deltaY < 0 ? 1.1 : 0.9; + scale *= zoomFactor; + const mouseX = e.offsetX; + const mouseY = e.offsetY; + offsetX -= (mouseX - offsetX) * (zoomFactor - 1); + offsetY -= (mouseY - offsetY) * (zoomFactor - 1); + Module._set_view(scale, offsetX, offsetY); + Module._render(Date.now()); + }); + + canvas.addEventListener("mousedown", (e) => { + isPanning = true; + lastX = e.offsetX; + lastY = e.offsetY; + }); + + canvas.addEventListener("mousemove", (e) => { + if (isPanning) { + const dx = e.offsetX - lastX; + const dy = e.offsetY - lastY; + offsetX += dx; + offsetY += dy; + lastX = e.offsetX; + lastY = e.offsetY; + Module._set_view(scale, offsetX, offsetY); + Module._render(Date.now()); + } + }); + + canvas.addEventListener("mouseup", () => { isPanning = false; }); + canvas.addEventListener("mouseout", () => { isPanning = false; }); +} diff --git a/frontend/resources/wasm-playground/rects.html b/frontend/resources/wasm-playground/rects.html new file mode 100644 index 000000000..120ee688b --- /dev/null +++ b/frontend/resources/wasm-playground/rects.html @@ -0,0 +1,69 @@ + + + + + WASM + WebGL2 Canvas + + + + + + + diff --git a/frontend/scripts/_helpers.js b/frontend/scripts/_helpers.js index 12ab0552c..5df2acf0d 100644 --- a/frontend/scripts/_helpers.js +++ b/frontend/scripts/_helpers.js @@ -573,3 +573,16 @@ export async function copyAssets() { const end = process.hrtime(start); log.info("done: copy assets", `(${ppt(end)})`); } + +export async function copyWasmPlayground() { + const start = process.hrtime(); + log.info("init: copy wasm playground"); + + await syncDirs( + "resources/wasm-playground/", + "resources/public/wasm-playground/", + ); + + const end = process.hrtime(start); + log.info("done: copy wasm playground", `(${ppt(end)})`); +} diff --git a/frontend/scripts/build-app-assets.js b/frontend/scripts/build-app-assets.js index 902f4c39e..5008d9a09 100644 --- a/frontend/scripts/build-app-assets.js +++ b/frontend/scripts/build-app-assets.js @@ -2,6 +2,7 @@ import * as h from "./_helpers.js"; await h.compileStyles(); await h.copyAssets(); +await h.copyWasmPlayground(); await h.compileSvgSprites(); await h.compileTemplates(); await h.compilePolyfills(); diff --git a/frontend/scripts/watch.js b/frontend/scripts/watch.js index 54a4f015b..fc4963295 100644 --- a/frontend/scripts/watch.js +++ b/frontend/scripts/watch.js @@ -44,6 +44,7 @@ async function compileSass(path) { await fs.mkdir("./resources/public/css/", { recursive: true }); await compileSassAll(); await h.copyAssets(); +await h.copyWasmPlayground(); await h.compileSvgSprites(); await h.compileTemplates(); await h.compilePolyfills(); @@ -88,4 +89,10 @@ h.watch( }, ); +log.info("watch: wasm playground (~)"); +h.watch(["resources/wasm-playground"], null, async function (path) { + log.info("changed:", path); + await h.copyWasmPlayground(); +}); + worker.terminate();