Merge pull request #1 from CrispyBaguette/web-worker

Web worker and HTML tweaks
This commit is contained in:
CrispyBaguette 2021-11-23 22:06:14 +01:00 committed by GitHub
commit f8f71c56e5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 178 additions and 43 deletions

View File

@ -1,19 +1,24 @@
# wasm-palette-converter # wasm-palette-converter
Go+Wasm image palette converter Go+Wasm image palette converter
Build with: Build with:
```bash ```bash
GOOS=js GOARCH=wasm go build -o dist/main.wasm . GOOS=js GOARCH=wasm go build -o dist/main.wasm .
``` ```
Access with: Access with:
``` ```
cd dist cd dist
npx http-server npx http-server
``` ```
A version is also available on IPFS: A version is also available on IPFS:
``` ```
/ipfs/QmQ4ptP2z6SonqyZfF5AiqgPa8RzHpPVyZWvZ8aVNxcoC9 /ipfs/QmU6MmYFEvEYcNLdJZKJ7LfTuAPhPnpLwiUt4YcmANQESP
``` ```
You can access it directly [here](https://ipfs.io/ipfs/QmQ4ptP2z6SonqyZfF5AiqgPa8RzHpPVyZWvZ8aVNxcoC9/).
You can access it directly [here](https://ipfs.io/ipfs/QmU6MmYFEvEYcNLdJZKJ7LfTuAPhPnpLwiUt4YcmANQESP/).

29
dist/index.html vendored
View File

@ -1,8 +1,8 @@
<html> <html>
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<script src="./wasm_exec.js"></script> <script type="module" src="./wasm_exec.js"></script>
<script async src="./main.js"></script> <script type="module" src="./main.js"></script>
<style> <style>
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
body { body {
@ -57,8 +57,19 @@
algorithm. algorithm.
</p> </p>
<p> <p>
Performance isn't great, as Go+Wasm only runs on a single thread. The Running in the browser with Wasm causes a bit of a performance penalty.
Wasm call also blocks the rendering (that would be fixable, though). Multithreading is not available (not that it matters much since
Floyd-Steinberg is single-threaded), and sending data back and forth
between JS and Wasm can take a little while.
</p>
<p>
I've re-used code from
<a
href="https://www.sitepen.com/blog/using-webassembly-with-web-workers"
>this article</a
>
to make the Wasm code run in a web worker, with some adaptations for Go
oddities.
</p> </p>
<p> <p>
If you're into that sort of thing, source code is available on If you're into that sort of thing, source code is available on
@ -68,11 +79,13 @@
</p> </p>
<form> <form>
<input type="file" id="source-image" accept="image/png, image/jpeg" /> <input type="file" id="source-image" accept="image/png, image/jpeg" />
<button id="go-btn" type="button">Go</button> <button id="go-btn" type="button" disabled>Go</button>
</form> </form>
<div class="container"> <a id="output-wrapper" download="output.png">
<img id="output" /> <div class="container">
</div> <img id="output" />
</div>
</a>
</article> </article>
</body> </body>
</html> </html>

122
dist/main.js vendored
View File

@ -1,38 +1,94 @@
(async () => { const fileInput = document.getElementById("source-image");
// Load element references const btn = document.getElementById("go-btn");
const fileInput = document.getElementById("source-image"); const output = document.getElementById("output");
const btn = document.getElementById("go-btn"); const outputWrapper = document.getElementById("output-wrapper");
// Setup Wasm stuff function wasmWorker(modulePath) {
const go = new Go(); // Create an object to later interact with
vm = await WebAssembly.instantiateStreaming( const proxy = {};
fetch("./main.wasm"),
go.importObject
);
go.run(vm.instance);
// Setup event listener // Keep track of the messages being sent
btn.addEventListener("click", async () => { // so we can resolve them correctly
// Clear image let id = 0;
outputElement = document.getElementById("output"); let idPromises = {};
outputElement.src = "";
// Check if a file was selected return new Promise((resolve, reject) => {
if (fileInput.files.length === 0) { const worker = new Worker("worker.js");
alert("No file selected"); worker.postMessage({ eventType: "INITIALISE", eventData: modulePath });
return; worker.addEventListener("message", function (event) {
} const { eventType, eventData, eventId } = event.data;
reader = new FileReader();
reader.readAsArrayBuffer(fileInput.files[0]); if (eventType === "INITIALISED") {
reader.Read; const methods = event.data.eventData;
reader.onloadend = async (evt) => { methods.forEach((method) => {
if (evt.target.readyState === FileReader.DONE) { proxy[method] = function () {
const array = new Uint8Array(evt.target.result); return new Promise((resolve, reject) => {
// Wasm magic happens here worker.postMessage({
ditheredImageData = await DitherNord(array); eventType: "CALL",
document.getElementById("output").src = eventData: {
"data:image/png;base64," + ditheredImageData; method: method,
arguments: Array.from(arguments), // arguments is not an array
},
eventId: id,
});
idPromises[id] = { resolve, reject };
id++;
});
};
});
resolve(proxy);
return;
} else if (eventType === "RESULT") {
if (eventId !== undefined && idPromises[eventId]) {
idPromises[eventId].resolve(eventData);
delete idPromises[eventId];
}
} else if (eventType === "ERROR") {
if (eventId !== undefined && idPromises[eventId]) {
idPromises[eventId].reject(event.data.eventData);
delete idPromises[eventId];
}
} }
}; });
worker.addEventListener("error", function (error) {
reject(error);
});
}); });
})(); }
let workerResolve;
const wasmReady = new Promise((resolve) => {
workerResolve = resolve;
});
let workerProxy;
wasmWorker("./main.wasm").then((w) => {
workerProxy = w;
workerResolve();
btn.removeAttribute("disabled");
});
btn.addEventListener("click", async () => {
// Clear image
output.src = "";
// Check if a file was selected
if (fileInput.files.length === 0) {
alert("No file selected");
return;
}
const reader = new FileReader();
reader.readAsArrayBuffer(fileInput.files[0]);
reader.onloadend = async (evt) => {
if (evt.target.readyState === FileReader.DONE) {
const imageData = new Uint8Array(evt.target.result);
const ditheredImage = await workerProxy.DitherNord(imageData);
const outputValue = `data:image/png;base64,${ditheredImage}`;
output.src = outputValue;
outputWrapper.href = outputValue;
}
};
});

61
dist/worker.js vendored Normal file
View File

@ -0,0 +1,61 @@
importScripts("./wasm_exec.js");
if (!WebAssembly.instantiateStreaming) {
WebAssembly.instantiateStreaming = async (resp, importObject) => {
const source = await (await resp).arrayBuffer();
return await WebAssembly.instantiate(source, importObject);
};
}
// Create promise to handle Worker calls whilst
// module is still initialising
let wasmResolve;
const wasmReady = new Promise((resolve) => {
wasmResolve = resolve;
});
const go = new self.Go();
addEventListener(
"message",
async (e) => {
const { eventType, eventData, eventId } = e.data;
if (eventType === "INITIALISE") {
const instantiatedSource = await WebAssembly.instantiateStreaming(
fetch(eventData),
go.importObject
);
go.run(instantiatedSource.instance);
// Go does nor exposes the exports in the instantiated module :(((
const methods = ["DitherNord"];
wasmResolve(methods);
postMessage({
eventType: "INITIALISED",
eventData: methods,
});
} else if (eventType === "CALL") {
await wasmReady;
try {
const method = self[eventData.method];
const result = await method.apply(null, eventData.arguments);
self.postMessage({
eventType: "RESULT",
eventData: result,
eventId: eventId,
});
} catch (e) {
console.error(e);
self.postMessage({
eventType: "ERROR",
eventData:
"An error occured executing WASM instance function: " +
error.toString(),
eventId: eventId,
});
}
}
},
false
);