Merge pull request #1 from CrispyBaguette/web-worker
Web worker and HTML tweaks
This commit is contained in:
commit
f8f71c56e5
@ -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
29
dist/index.html
vendored
@ -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
122
dist/main.js
vendored
@ -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
61
dist/worker.js
vendored
Normal 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
|
||||
);
|
Loading…
Reference in New Issue
Block a user