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
Go+Wasm image palette converter
Build with:
```bash
GOOS=js GOARCH=wasm go build -o dist/main.wasm .
```
Access with:
```
cd dist
npx http-server
```
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>
<head>
<meta charset="utf-8" />
<script src="./wasm_exec.js"></script>
<script async src="./main.js"></script>
<script type="module" src="./wasm_exec.js"></script>
<script type="module" src="./main.js"></script>
<style>
@media (prefers-color-scheme: dark) {
body {
@ -57,8 +57,19 @@
algorithm.
</p>
<p>
Performance isn't great, as Go+Wasm only runs on a single thread. The
Wasm call also blocks the rendering (that would be fixable, though).
Running in the browser with Wasm causes a bit of a performance penalty.
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>
If you're into that sort of thing, source code is available on
@ -68,11 +79,13 @@
</p>
<form>
<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>
<div class="container">
<img id="output" />
</div>
<a id="output-wrapper" download="output.png">
<div class="container">
<img id="output" />
</div>
</a>
</article>
</body>
</html>

122
dist/main.js vendored
View File

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