Functional app

This commit is contained in:
CrispyBaguette 2021-12-12 17:50:58 +01:00
parent 83b44f5fd3
commit b32887cd3c
24 changed files with 8165 additions and 21455 deletions

15
client/.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,15 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "pwa-chrome",
"request": "launch",
"name": "Launch Chrome against localhost",
"url": "http://localhost:3000",
"webRoot": "${workspaceFolder}"
}
]
}

28516
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -3,6 +3,10 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.36",
"@fortawesome/free-brands-svg-icons": "^5.15.4",
"@fortawesome/free-solid-svg-icons": "^5.15.4",
"@fortawesome/react-fontawesome": "^0.1.16",
"@testing-library/jest-dom": "^5.16.1", "@testing-library/jest-dom": "^5.16.1",
"@testing-library/react": "^11.2.7", "@testing-library/react": "^11.2.7",
"@testing-library/user-event": "^12.8.3", "@testing-library/user-event": "^12.8.3",
@ -10,9 +14,11 @@
"@types/node": "^12.20.37", "@types/node": "^12.20.37",
"@types/react": "^17.0.37", "@types/react": "^17.0.37",
"@types/react-dom": "^17.0.11", "@types/react-dom": "^17.0.11",
"classnames": "^2.3.1",
"file-saver": "^2.0.5",
"react": "^17.0.2", "react": "^17.0.2",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-scripts": "4.0.3", "react-scripts": "next",
"typescript": "^4.5.3", "typescript": "^4.5.3",
"web-vitals": "^1.1.2" "web-vitals": "^1.1.2"
}, },
@ -39,5 +45,12 @@
"last 1 firefox version", "last 1 firefox version",
"last 1 safari version" "last 1 safari version"
] ]
},
"devDependencies": {
"@tailwindcss/forms": "^0.4.0",
"@types/file-saver": "^2.0.4",
"autoprefixer": "^10.4.0",
"postcss": "^8.4.4",
"tailwindcss": "^3.0.1"
} }
} }

6
client/postcss.config.js Normal file
View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@ -15,38 +15,6 @@
</head> </head>
<body> <body>
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>
<article>
<h1>Go+Wasm image dithering tool</h1>
<aside>Featuring the Nord Color Palette</aside>
<p>
Load an image, click Go and wait (potentially for a while) for the image
to be dithered using the Nord color palette and the Floyd-Steinberg
algorithm.
</p>
<p>
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
<a href="https://github.com/CrispyBaguette/wasm-palette-converter">
GitHub
</a>
.
</p>
</article>
<div id="root"></div> <div id="root"></div>
</body> </body>
</html> </html>

View File

@ -29,7 +29,7 @@ addEventListener(
go.run(instantiatedSource.instance); go.run(instantiatedSource.instance);
// Go does nor exposes the exports in the instantiated module :((( // Go does nor exposes the exports in the instantiated module :(((
const methods = ["DitherNord"]; const methods = ["dither"];
wasmResolve(methods); wasmResolve(methods);
postMessage({ postMessage({
eventType: "INITIALISED", eventType: "INITIALISED",

View File

@ -1,10 +0,0 @@
#output {
image-rendering: optimizeSpeed; /* Legal fallback */
image-rendering: -moz-crisp-edges; /* Firefox */
image-rendering: -o-crisp-edges; /* Opera */
image-rendering: -webkit-optimize-contrast; /* Safari */
image-rendering: optimize-contrast; /* CSS3 Proposed */
image-rendering: crisp-edges; /* CSS4 Proposed */
image-rendering: pixelated; /* CSS4 Proposed */
-ms-interpolation-mode: nearest-neighbor; /* IE8+ */
}

View File

@ -1,9 +0,0 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});

View File

@ -1,116 +1,58 @@
import React from "react"; import React from "react";
import "./App.css"; import ImageInput from "./ImageInput";
import ImageOutput from "./ImageOutput";
import Ditherer from "./Ditherer";
import ImagePreview from "./ImagePreview";
import Header from "./Header";
enum AppState {
NO_IMAGE,
IMAGE_LOADED,
IMAGE_PROCESSED,
}
function App() { function App() {
const [imageSrc, setImageSrc] = React.useState(""); const [baseImage, setBaseImage] = React.useState<Uint8ClampedArray>();
const fileInput = React.useRef<HTMLInputElement>(null); const [ditheredImage, setDitheredImage] = React.useState<Uint8ClampedArray>();
const [appState, setAppState] = React.useState<AppState>(AppState.NO_IMAGE);
const handleClick = async () => { const handleImageSubmit = async (data: Uint8ClampedArray) => {
if (!fileInput.current) { setBaseImage(data);
return; setAppState(AppState.IMAGE_LOADED);
} const ditheredImage = await new Ditherer().dither(data);
const workerProxy: any = await wasmWorker("main.wasm"); setDitheredImage(ditheredImage);
setImageSrc(""); setAppState(AppState.IMAGE_PROCESSED);
// Check if a file was selected
if (fileInput.current.files!.length === 0) {
alert("No file selected");
return;
}
const reader = new FileReader();
reader.readAsArrayBuffer(fileInput.current.files![0]);
reader.onloadend = async (evt) => {
if (evt.target!.readyState === FileReader.DONE) {
const imageData = new Uint8Array(evt.target!.result as ArrayBuffer);
const ditheredImageArray = await workerProxy.DitherNord(imageData);
const imageBlob = new Blob([ditheredImageArray.buffer], {
type: "image/png",
});
const url = URL.createObjectURL(imageBlob);
setImageSrc(url);
}
};
}; };
return ( return (
<div className="App"> <div className="bg-nord-6 text-nord-0 min-h-screen">
<input <Header />
type="file" <main className="container mx-auto">
id="source-image" <article className="max-w-prose mx-auto pb-5 px-2">
ref={fileInput} <h1 className="text-3xl text-center pb-3">
accept="image/png, image/jpeg" Go+Wasm image dithering tool
/> </h1>
<button id="go-btn" type="button" onClick={handleClick}> <aside className="text-xl text-center pb-3">
Go Featuring the Nord Color Palette
</button> </aside>
<a id="output-wrapper" download="output.png" href={imageSrc}> <p>
<div className="container"> Load an image, click Go and wait (potentially for a while) for the
<img image to be processed using the Floyd-Steinberg algorithm.
id="output" </p>
alt="dithering output" <p>
src={imageSrc} WebAssembly might run out of memory when processing larger images.
style={{ display: imageSrc !== "" ? "block" : "none" }} </p>
/> </article>
</div> <ImageInput onImageSubmit={handleImageSubmit}></ImageInput>
</a> {appState === AppState.IMAGE_LOADED && (
<ImagePreview imageData={baseImage as Uint8ClampedArray} />
)}
{appState === AppState.IMAGE_PROCESSED && (
<ImageOutput imageData={ditheredImage as Uint8ClampedArray} />
)}
</main>
</div> </div>
); );
} }
function wasmWorker(modulePath: string) {
// Create an object to later interact with
const proxy: any = {};
// Keep track of the messages being sent
// so we can resolve them correctly
let id = 0;
let idPromises: any = {};
return new Promise((resolve, reject) => {
const worker = new Worker("./worker.js");
worker.postMessage({ eventType: "INITIALISE", eventData: modulePath });
worker.addEventListener("message", function (event: any) {
const { eventType, eventData, eventId } = event.data;
if (eventType === "INITIALISED") {
const methods = event.data.eventData;
methods.forEach((method: any) => {
proxy[method] = (...args: any[]) => {
return new Promise((resolve, reject) => {
worker.postMessage({
eventType: "CALL",
eventData: {
method: method,
arguments: Array.from(args),
},
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: any) {
reject(error);
});
});
}
export default App; export default App;

68
client/src/Ditherer.ts Normal file
View File

@ -0,0 +1,68 @@
class Ditherer {
async dither(image: Uint8ClampedArray): Promise<Uint8ClampedArray> {
const worker: any = await wasmWorker("/main.wasm");
let output: Uint8ClampedArray;
output = await worker.dither(image);
worker.terminate();
return output;
}
}
function wasmWorker(modulePath: string) {
// Create an object to later interact with
const proxy: any = {};
// Keep track of the messages being sent
// so we can resolve them correctly
let id = 0;
let idPromises: any = {};
return new Promise((resolve, reject) => {
const worker = new Worker("./worker.js");
proxy.terminate = () => worker.terminate();
worker.postMessage({ eventType: "INITIALISE", eventData: modulePath });
worker.addEventListener("message", function (event: any) {
const { eventType, eventData, eventId } = event.data;
if (eventType === "INITIALISED") {
const methods = event.data.eventData;
methods.forEach((method: any) => {
proxy[method] = (...args: any[]) => {
return new Promise((resolve, reject) => {
worker.postMessage({
eventType: "CALL",
eventData: {
method: method,
arguments: Array.from(args),
},
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: any) {
reject(error);
});
});
}
export default Ditherer;

23
client/src/Header.tsx Normal file
View File

@ -0,0 +1,23 @@
import { faGithub } from "@fortawesome/free-brands-svg-icons";
import { faIgloo } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
function Header() {
return (
<header className="bg-nord-1 text-nord-5 py-3 px-2">
<a href="https://github.com/CrispyBaguette/wasm-palette-converter">
<FontAwesomeIcon
icon={faGithub}
className="fa-2x hover:text-nord-7 mx-2"
/>
</a>
<a href="https://blog.bruyant.xyz">
<FontAwesomeIcon
icon={faIgloo}
className="fa-2x hover:text-nord-7 mx-2"
/>
</a>
</header>
);
}
export default Header;

64
client/src/ImageInput.tsx Normal file
View File

@ -0,0 +1,64 @@
import React, { FormEventHandler } from "react";
interface Props {
onImageSubmit: (image: Uint8ClampedArray) => void;
}
function ImageInput({ onImageSubmit }: Props) {
let fileReader: FileReader;
const fileInputRef = React.useRef<HTMLInputElement>(null);
const handleSubmit: FormEventHandler = (e) => {
e.preventDefault();
if (
!fileInputRef.current ||
!fileInputRef.current.files ||
fileInputRef.current.files.length === 0
) {
return;
}
fileReader = new FileReader();
fileReader.onloadend = handleFileRead;
fileReader.readAsArrayBuffer(fileInputRef.current.files[0]);
};
const handleFileRead: EventListener = (e) => {
if (fileReader.result) {
const image = new Uint8ClampedArray(fileReader.result as ArrayBuffer);
onImageSubmit(image);
}
};
return (
<form
className="grid grid-cols-1 max-w-md mx-auto my-4 px-2"
onSubmit={handleSubmit}
>
<label className="block">
<span>Select an image:</span>
<input
type="file"
accept="image/png, image/jpeg"
ref={fileInputRef}
className="block w-full mt-1 text-sm text-nord-0 file:mr-4 file:py-2 file:px-4 file:border-0 file:text-sm file:font-semibold file:bg-nord-4 file:text-nord-0 hover:file:bg-nord-5"
/>
</label>
{/* <label>
<span className="block">Select a color palette:</span>
<select className="form-select block w-full mt-1">
<option value="nord">Nord</option>
</select>
</label> */}
<button
type="submit"
value="Go"
className="block w-32 mx-auto mt-1 py-2 px-4 text-sm text-nord-0 bg-nord-4 hover:bg-nord-5"
>
Go
</button>
</form>
);
}
export default ImageInput;

View File

@ -0,0 +1,10 @@
img {
image-rendering: optimizeSpeed;
image-rendering: -moz-crisp-edges;
image-rendering: -o-crisp-edges;
image-rendering: -webkit-optimize-contrast;
image-rendering: optimize-contrast;
image-rendering: crisp-edges;
image-rendering: pixelated;
-ms-interpolation-mode: nearest-neighbor;
}

View File

@ -0,0 +1,41 @@
import FileSaver from "file-saver";
import "./ImageOutput.css";
interface OutputProps {
imageData: Uint8ClampedArray;
}
function ImageOutput({ imageData }: OutputProps) {
const imageBlob = new Blob([imageData], { type: "image/png" });
const imageUrl = URL.createObjectURL(imageBlob);
const handleClick = () => {
FileSaver.saveAs(imageBlob, "image.png");
};
return (
<div className="mx-auto">
<img
alt="dithering output"
src={imageUrl}
className="object-contain max-h-96 max-w-96 mx-auto"
/>
<button
onClick={handleClick}
className="block w-48 mx-auto mt-1 py-2 px-4 text-sm text-nord-0 bg-nord-4 hover:bg-nord-5"
>
Download
</button>
<a
href={imageUrl}
target="_blank"
rel="noopener noreferrer"
className="block w-48 mx-auto mt-1 py-2 px-4 text-sm text-nord-0 bg-nord-4 hover:bg-nord-5"
>
Open in new tab
</a>
</div>
);
}
export default ImageOutput;

View File

@ -0,0 +1,24 @@
import { faSpinner } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import classNames from "classnames";
interface ImagePreviewProps {
imageData: Uint8ClampedArray;
}
function ImagePreview({ imageData }: ImagePreviewProps) {
const imageBlob = new Blob([imageData], { type: "image/png" });
const imageUrl = URL.createObjectURL(imageBlob);
return (
<div className="blur-sm mx-auto">
<img
alt="preview"
src={imageUrl}
className="object-contain max-h-96 max-w-96 mx-auto"
/>
</div>
);
}
export default ImagePreview;

View File

@ -1,41 +1,22 @@
@media (prefers-color-scheme: dark) { @tailwind base;
body { @tailwind components;
color: #fff; @tailwind utilities;
background: #000;
} :root {
a:link { --nord-0: #2e3440;
color: #9cf; --nord-1: #3b4252;
} --nord-2: #434c5e;
a:hover, --nord-3: #4c566a;
a:visited:hover { --nord-4: #d8dee9;
color: #cef; --nord-5: #e5e9f0;
} --nord-6: #eceff4;
a:visited { --nord-7: #8fbcbb;
color: #c9f; --nord-8: #88c0d0;
} --nord-9: #81a1c1;
} --nord-10: #5e81ac;
body { --nord-11: #bf616a;
margin: 1em auto; --nord-12: #d08770;
max-width: 40em; --nord-13: #ebcb8b;
padding: 0 0.62em; --nord-14: #a3be8c;
font: 1.2em/1.62 sans-serif; --nord-15: #b48ead;
}
h1,
h2,
h3 {
line-height: 1.2;
}
.container {
display: flex;
flex-direction: column;
justify-content: center;
}
#output {
max-width: 100%;
align-self: center;
}
@media print {
body {
max-width: none;
}
} }

54
client/tailwind.config.js Normal file
View File

@ -0,0 +1,54 @@
module.exports = {
content: ["./src/**/*.{ts,tsx}", "./public/index.html}"],
theme: {
extend: {
colors: {
nord: {
0: "var(--nord-0)",
1: "var(--nord-1)",
2: "var(--nord-2)",
3: "var(--nord-3)",
4: "var(--nord-4)",
5: "var(--nord-5)",
6: "var(--nord-6)",
7: "var(--nord-7)",
8: "var(--nord-8)",
9: "var(--nord-9)",
10: "var(--nord-10)",
11: "var(--nord-11)",
12: "var(--nord-12)",
13: "var(--nord-13)",
14: "var(--nord-14)",
15: "var(--nord-15)",
},
polar: {
1: "var(--nord-0)",
2: "var(--nord-1)",
3: "var(--nord-2)",
4: "var(--nord-3)",
},
snow: {
1: "var(--nord-4)",
2: "var(--nord-5)",
3: "var(--nord-6)",
},
frost: {
1: "var(--nord-7)",
2: "var(--nord-8)",
3: "var(--nord-9)",
4: "var(--nord-10)",
},
aurora: {
1: "var(--nord-11)",
2: "var(--nord-12)",
3: "var(--nord-13)",
4: "var(--nord-14)",
5: "var(--nord-15)",
},
},
},
},
variants: {},
plugins: [require("@tailwindcss/forms")],
safelist: ["bg-black"],
};

View File

@ -4,7 +4,7 @@
"lib": [ "lib": [
"dom", "dom",
"dom.iterable", "dom.iterable",
"esnext" "esnext",
], ],
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "skipLibCheck": true,

91
dist/index.html vendored
View File

@ -1,91 +0,0 @@
<html>
<head>
<meta charset="utf-8" />
<script type="module" src="./wasm_exec.js"></script>
<script type="module" src="./main.js"></script>
<style>
@media (prefers-color-scheme: dark) {
body {
color: #fff;
background: #000;
}
a:link {
color: #9cf;
}
a:hover,
a:visited:hover {
color: #cef;
}
a:visited {
color: #c9f;
}
}
body {
margin: 1em auto;
max-width: 40em;
padding: 0 0.62em;
font: 1.2em/1.62 sans-serif;
}
h1,
h2,
h3 {
line-height: 1.2;
}
.container {
display: flex;
flex-direction: column;
justify-content: center;
}
#output {
max-width: 100%;
align-self: center;
}
@media print {
body {
max-width: none;
}
}
</style>
</head>
<body>
<article>
<h1>Go+Wasm image dithering tool</h1>
<aside>Featuring the Nord Color Palette</aside>
<p>
Load an image, click Go and wait (potentially for a while) for the image
to be dithered using the Nord color palette and the Floyd-Steinberg
algorithm.
</p>
<p>
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
<a href="https://github.com/CrispyBaguette/wasm-palette-converter"
>GitHub</a
>.
</p>
<form>
<input type="file" id="source-image" accept="image/png, image/jpeg" />
<button id="go-btn" type="button" disabled>Go</button>
</form>
<a id="output-wrapper" download="output.png">
<div class="container">
<img id="output" />
</div>
</a>
</article>
</body>
</html>

94
dist/main.js vendored
View File

@ -1,94 +0,0 @@
const fileInput = document.getElementById("source-image");
const btn = document.getElementById("go-btn");
const output = document.getElementById("output");
const outputWrapper = document.getElementById("output-wrapper");
function wasmWorker(modulePath) {
// Create an object to later interact with
const proxy = {};
// Keep track of the messages being sent
// so we can resolve them correctly
let id = 0;
let idPromises = {};
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;
}
};
});

636
dist/wasm_exec.js vendored
View File

@ -1,636 +0,0 @@
// Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
(() => {
// Map multiple JavaScript environments to a single common API,
// preferring web standards over Node.js API.
//
// Environments considered:
// - Browsers
// - Node.js
// - Electron
// - Parcel
// - Webpack
if (typeof global !== "undefined") {
// global already exists
} else if (typeof window !== "undefined") {
window.global = window;
} else if (typeof self !== "undefined") {
self.global = self;
} else {
throw new Error("cannot export Go (neither global, window nor self is defined)");
}
if (!global.require && typeof require !== "undefined") {
global.require = require;
}
if (!global.fs && global.require) {
const fs = require("fs");
if (typeof fs === "object" && fs !== null && Object.keys(fs).length !== 0) {
global.fs = fs;
}
}
const enosys = () => {
const err = new Error("not implemented");
err.code = "ENOSYS";
return err;
};
if (!global.fs) {
let outputBuf = "";
global.fs = {
constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused
writeSync(fd, buf) {
outputBuf += decoder.decode(buf);
const nl = outputBuf.lastIndexOf("\n");
if (nl != -1) {
console.log(outputBuf.substr(0, nl));
outputBuf = outputBuf.substr(nl + 1);
}
return buf.length;
},
write(fd, buf, offset, length, position, callback) {
if (offset !== 0 || length !== buf.length || position !== null) {
callback(enosys());
return;
}
const n = this.writeSync(fd, buf);
callback(null, n);
},
chmod(path, mode, callback) { callback(enosys()); },
chown(path, uid, gid, callback) { callback(enosys()); },
close(fd, callback) { callback(enosys()); },
fchmod(fd, mode, callback) { callback(enosys()); },
fchown(fd, uid, gid, callback) { callback(enosys()); },
fstat(fd, callback) { callback(enosys()); },
fsync(fd, callback) { callback(null); },
ftruncate(fd, length, callback) { callback(enosys()); },
lchown(path, uid, gid, callback) { callback(enosys()); },
link(path, link, callback) { callback(enosys()); },
lstat(path, callback) { callback(enosys()); },
mkdir(path, perm, callback) { callback(enosys()); },
open(path, flags, mode, callback) { callback(enosys()); },
read(fd, buffer, offset, length, position, callback) { callback(enosys()); },
readdir(path, callback) { callback(enosys()); },
readlink(path, callback) { callback(enosys()); },
rename(from, to, callback) { callback(enosys()); },
rmdir(path, callback) { callback(enosys()); },
stat(path, callback) { callback(enosys()); },
symlink(path, link, callback) { callback(enosys()); },
truncate(path, length, callback) { callback(enosys()); },
unlink(path, callback) { callback(enosys()); },
utimes(path, atime, mtime, callback) { callback(enosys()); },
};
}
if (!global.process) {
global.process = {
getuid() { return -1; },
getgid() { return -1; },
geteuid() { return -1; },
getegid() { return -1; },
getgroups() { throw enosys(); },
pid: -1,
ppid: -1,
umask() { throw enosys(); },
cwd() { throw enosys(); },
chdir() { throw enosys(); },
}
}
if (!global.crypto && global.require) {
const nodeCrypto = require("crypto");
global.crypto = {
getRandomValues(b) {
nodeCrypto.randomFillSync(b);
},
};
}
if (!global.crypto) {
throw new Error("global.crypto is not available, polyfill required (getRandomValues only)");
}
if (!global.performance) {
global.performance = {
now() {
const [sec, nsec] = process.hrtime();
return sec * 1000 + nsec / 1000000;
},
};
}
if (!global.TextEncoder && global.require) {
global.TextEncoder = require("util").TextEncoder;
}
if (!global.TextEncoder) {
throw new Error("global.TextEncoder is not available, polyfill required");
}
if (!global.TextDecoder && global.require) {
global.TextDecoder = require("util").TextDecoder;
}
if (!global.TextDecoder) {
throw new Error("global.TextDecoder is not available, polyfill required");
}
// End of polyfills for common API.
const encoder = new TextEncoder("utf-8");
const decoder = new TextDecoder("utf-8");
global.Go = class {
constructor() {
this.argv = ["js"];
this.env = {};
this.exit = (code) => {
if (code !== 0) {
console.warn("exit code:", code);
}
};
this._exitPromise = new Promise((resolve) => {
this._resolveExitPromise = resolve;
});
this._pendingEvent = null;
this._scheduledTimeouts = new Map();
this._nextCallbackTimeoutID = 1;
const setInt64 = (addr, v) => {
this.mem.setUint32(addr + 0, v, true);
this.mem.setUint32(addr + 4, Math.floor(v / 4294967296), true);
}
const getInt64 = (addr) => {
const low = this.mem.getUint32(addr + 0, true);
const high = this.mem.getInt32(addr + 4, true);
return low + high * 4294967296;
}
const loadValue = (addr) => {
const f = this.mem.getFloat64(addr, true);
if (f === 0) {
return undefined;
}
if (!isNaN(f)) {
return f;
}
const id = this.mem.getUint32(addr, true);
return this._values[id];
}
const storeValue = (addr, v) => {
const nanHead = 0x7FF80000;
if (typeof v === "number" && v !== 0) {
if (isNaN(v)) {
this.mem.setUint32(addr + 4, nanHead, true);
this.mem.setUint32(addr, 0, true);
return;
}
this.mem.setFloat64(addr, v, true);
return;
}
if (v === undefined) {
this.mem.setFloat64(addr, 0, true);
return;
}
let id = this._ids.get(v);
if (id === undefined) {
id = this._idPool.pop();
if (id === undefined) {
id = this._values.length;
}
this._values[id] = v;
this._goRefCounts[id] = 0;
this._ids.set(v, id);
}
this._goRefCounts[id]++;
let typeFlag = 0;
switch (typeof v) {
case "object":
if (v !== null) {
typeFlag = 1;
}
break;
case "string":
typeFlag = 2;
break;
case "symbol":
typeFlag = 3;
break;
case "function":
typeFlag = 4;
break;
}
this.mem.setUint32(addr + 4, nanHead | typeFlag, true);
this.mem.setUint32(addr, id, true);
}
const loadSlice = (addr) => {
const array = getInt64(addr + 0);
const len = getInt64(addr + 8);
return new Uint8Array(this._inst.exports.mem.buffer, array, len);
}
const loadSliceOfValues = (addr) => {
const array = getInt64(addr + 0);
const len = getInt64(addr + 8);
const a = new Array(len);
for (let i = 0; i < len; i++) {
a[i] = loadValue(array + i * 8);
}
return a;
}
const loadString = (addr) => {
const saddr = getInt64(addr + 0);
const len = getInt64(addr + 8);
return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len));
}
const timeOrigin = Date.now() - performance.now();
this.importObject = {
go: {
// Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters)
// may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported
// function. A goroutine can switch to a new stack if the current stack is too small (see morestack function).
// This changes the SP, thus we have to update the SP used by the imported function.
// func wasmExit(code int32)
"runtime.wasmExit": (sp) => {
sp >>>= 0;
const code = this.mem.getInt32(sp + 8, true);
this.exited = true;
delete this._inst;
delete this._values;
delete this._goRefCounts;
delete this._ids;
delete this._idPool;
this.exit(code);
},
// func wasmWrite(fd uintptr, p unsafe.Pointer, n int32)
"runtime.wasmWrite": (sp) => {
sp >>>= 0;
const fd = getInt64(sp + 8);
const p = getInt64(sp + 16);
const n = this.mem.getInt32(sp + 24, true);
fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n));
},
// func resetMemoryDataView()
"runtime.resetMemoryDataView": (sp) => {
sp >>>= 0;
this.mem = new DataView(this._inst.exports.mem.buffer);
},
// func nanotime1() int64
"runtime.nanotime1": (sp) => {
sp >>>= 0;
setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000);
},
// func walltime() (sec int64, nsec int32)
"runtime.walltime": (sp) => {
sp >>>= 0;
const msec = (new Date).getTime();
setInt64(sp + 8, msec / 1000);
this.mem.setInt32(sp + 16, (msec % 1000) * 1000000, true);
},
// func scheduleTimeoutEvent(delay int64) int32
"runtime.scheduleTimeoutEvent": (sp) => {
sp >>>= 0;
const id = this._nextCallbackTimeoutID;
this._nextCallbackTimeoutID++;
this._scheduledTimeouts.set(id, setTimeout(
() => {
this._resume();
while (this._scheduledTimeouts.has(id)) {
// for some reason Go failed to register the timeout event, log and try again
// (temporary workaround for https://github.com/golang/go/issues/28975)
console.warn("scheduleTimeoutEvent: missed timeout event");
this._resume();
}
},
getInt64(sp + 8) + 1, // setTimeout has been seen to fire up to 1 millisecond early
));
this.mem.setInt32(sp + 16, id, true);
},
// func clearTimeoutEvent(id int32)
"runtime.clearTimeoutEvent": (sp) => {
sp >>>= 0;
const id = this.mem.getInt32(sp + 8, true);
clearTimeout(this._scheduledTimeouts.get(id));
this._scheduledTimeouts.delete(id);
},
// func getRandomData(r []byte)
"runtime.getRandomData": (sp) => {
sp >>>= 0;
crypto.getRandomValues(loadSlice(sp + 8));
},
// func finalizeRef(v ref)
"syscall/js.finalizeRef": (sp) => {
sp >>>= 0;
const id = this.mem.getUint32(sp + 8, true);
this._goRefCounts[id]--;
if (this._goRefCounts[id] === 0) {
const v = this._values[id];
this._values[id] = null;
this._ids.delete(v);
this._idPool.push(id);
}
},
// func stringVal(value string) ref
"syscall/js.stringVal": (sp) => {
sp >>>= 0;
storeValue(sp + 24, loadString(sp + 8));
},
// func valueGet(v ref, p string) ref
"syscall/js.valueGet": (sp) => {
sp >>>= 0;
const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16));
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 32, result);
},
// func valueSet(v ref, p string, x ref)
"syscall/js.valueSet": (sp) => {
sp >>>= 0;
Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32));
},
// func valueDelete(v ref, p string)
"syscall/js.valueDelete": (sp) => {
sp >>>= 0;
Reflect.deleteProperty(loadValue(sp + 8), loadString(sp + 16));
},
// func valueIndex(v ref, i int) ref
"syscall/js.valueIndex": (sp) => {
sp >>>= 0;
storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16)));
},
// valueSetIndex(v ref, i int, x ref)
"syscall/js.valueSetIndex": (sp) => {
sp >>>= 0;
Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24));
},
// func valueCall(v ref, m string, args []ref) (ref, bool)
"syscall/js.valueCall": (sp) => {
sp >>>= 0;
try {
const v = loadValue(sp + 8);
const m = Reflect.get(v, loadString(sp + 16));
const args = loadSliceOfValues(sp + 32);
const result = Reflect.apply(m, v, args);
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 56, result);
this.mem.setUint8(sp + 64, 1);
} catch (err) {
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 56, err);
this.mem.setUint8(sp + 64, 0);
}
},
// func valueInvoke(v ref, args []ref) (ref, bool)
"syscall/js.valueInvoke": (sp) => {
sp >>>= 0;
try {
const v = loadValue(sp + 8);
const args = loadSliceOfValues(sp + 16);
const result = Reflect.apply(v, undefined, args);
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 40, result);
this.mem.setUint8(sp + 48, 1);
} catch (err) {
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 40, err);
this.mem.setUint8(sp + 48, 0);
}
},
// func valueNew(v ref, args []ref) (ref, bool)
"syscall/js.valueNew": (sp) => {
sp >>>= 0;
try {
const v = loadValue(sp + 8);
const args = loadSliceOfValues(sp + 16);
const result = Reflect.construct(v, args);
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 40, result);
this.mem.setUint8(sp + 48, 1);
} catch (err) {
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 40, err);
this.mem.setUint8(sp + 48, 0);
}
},
// func valueLength(v ref) int
"syscall/js.valueLength": (sp) => {
sp >>>= 0;
setInt64(sp + 16, parseInt(loadValue(sp + 8).length));
},
// valuePrepareString(v ref) (ref, int)
"syscall/js.valuePrepareString": (sp) => {
sp >>>= 0;
const str = encoder.encode(String(loadValue(sp + 8)));
storeValue(sp + 16, str);
setInt64(sp + 24, str.length);
},
// valueLoadString(v ref, b []byte)
"syscall/js.valueLoadString": (sp) => {
sp >>>= 0;
const str = loadValue(sp + 8);
loadSlice(sp + 16).set(str);
},
// func valueInstanceOf(v ref, t ref) bool
"syscall/js.valueInstanceOf": (sp) => {
sp >>>= 0;
this.mem.setUint8(sp + 24, (loadValue(sp + 8) instanceof loadValue(sp + 16)) ? 1 : 0);
},
// func copyBytesToGo(dst []byte, src ref) (int, bool)
"syscall/js.copyBytesToGo": (sp) => {
sp >>>= 0;
const dst = loadSlice(sp + 8);
const src = loadValue(sp + 32);
if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) {
this.mem.setUint8(sp + 48, 0);
return;
}
const toCopy = src.subarray(0, dst.length);
dst.set(toCopy);
setInt64(sp + 40, toCopy.length);
this.mem.setUint8(sp + 48, 1);
},
// func copyBytesToJS(dst ref, src []byte) (int, bool)
"syscall/js.copyBytesToJS": (sp) => {
sp >>>= 0;
const dst = loadValue(sp + 8);
const src = loadSlice(sp + 16);
if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) {
this.mem.setUint8(sp + 48, 0);
return;
}
const toCopy = src.subarray(0, dst.length);
dst.set(toCopy);
setInt64(sp + 40, toCopy.length);
this.mem.setUint8(sp + 48, 1);
},
"debug": (value) => {
console.log(value);
},
}
};
}
async run(instance) {
if (!(instance instanceof WebAssembly.Instance)) {
throw new Error("Go.run: WebAssembly.Instance expected");
}
this._inst = instance;
this.mem = new DataView(this._inst.exports.mem.buffer);
this._values = [ // JS values that Go currently has references to, indexed by reference id
NaN,
0,
null,
true,
false,
global,
this,
];
this._goRefCounts = new Array(this._values.length).fill(Infinity); // number of references that Go has to a JS value, indexed by reference id
this._ids = new Map([ // mapping from JS values to reference ids
[0, 1],
[null, 2],
[true, 3],
[false, 4],
[global, 5],
[this, 6],
]);
this._idPool = []; // unused ids that have been garbage collected
this.exited = false; // whether the Go program has exited
// Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory.
let offset = 4096;
const strPtr = (str) => {
const ptr = offset;
const bytes = encoder.encode(str + "\0");
new Uint8Array(this.mem.buffer, offset, bytes.length).set(bytes);
offset += bytes.length;
if (offset % 8 !== 0) {
offset += 8 - (offset % 8);
}
return ptr;
};
const argc = this.argv.length;
const argvPtrs = [];
this.argv.forEach((arg) => {
argvPtrs.push(strPtr(arg));
});
argvPtrs.push(0);
const keys = Object.keys(this.env).sort();
keys.forEach((key) => {
argvPtrs.push(strPtr(`${key}=${this.env[key]}`));
});
argvPtrs.push(0);
const argv = offset;
argvPtrs.forEach((ptr) => {
this.mem.setUint32(offset, ptr, true);
this.mem.setUint32(offset + 4, 0, true);
offset += 8;
});
// The linker guarantees global data starts from at least wasmMinDataAddr.
// Keep in sync with cmd/link/internal/ld/data.go:wasmMinDataAddr.
const wasmMinDataAddr = 4096 + 8192;
if (offset >= wasmMinDataAddr) {
throw new Error("total length of command line and environment variables exceeds limit");
}
this._inst.exports.run(argc, argv);
if (this.exited) {
this._resolveExitPromise();
}
await this._exitPromise;
}
_resume() {
if (this.exited) {
throw new Error("Go program has already exited");
}
this._inst.exports.resume();
if (this.exited) {
this._resolveExitPromise();
}
}
_makeFuncWrapper(id) {
const go = this;
return function () {
const event = { id: id, this: this, args: arguments };
go._pendingEvent = event;
go._resume();
return event.result;
};
}
}
if (
typeof module !== "undefined" &&
global.require &&
global.require.main === module &&
global.process &&
global.process.versions &&
!global.process.versions.electron
) {
if (process.argv.length < 3) {
console.error("usage: go_js_wasm_exec [wasm binary] [arguments]");
process.exit(1);
}
const go = new Go();
go.argv = process.argv.slice(2);
go.env = Object.assign({ TMPDIR: require("os").tmpdir() }, process.env);
go.exit = process.exit;
WebAssembly.instantiate(fs.readFileSync(process.argv[2]), go.importObject).then((result) => {
process.on("exit", (code) => { // Node.js exits if no event handler is pending
if (code === 0 && !go.exited) {
// deadlock, make Go print error and stack traces
go._pendingEvent = { id: 0 };
go._resume();
}
});
return go.run(result.instance);
}).catch((err) => {
console.error(err);
process.exit(1);
});
}
})();

61
dist/worker.js vendored
View File

@ -1,61 +0,0 @@
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
);

View File

@ -5,6 +5,6 @@ import (
) )
func main() { func main() {
js.Global().Set("DitherNord", DitherNord()) js.Global().Set("dither", Dither())
<-make(chan bool) <-make(chan bool)
} }

View File

@ -3,6 +3,7 @@ package main
import ( import (
"bytes" "bytes"
"encoding/hex" "encoding/hex"
"fmt"
"image" "image"
"image/color" "image/color"
_ "image/jpeg" _ "image/jpeg"
@ -41,6 +42,11 @@ func buildPalette(pal []string) (color.Palette, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
if len(b) != 3 {
return nil, fmt.Errorf("invalid color length: %v", len(b))
}
palette[i] = color.RGBA{b[0], b[1], b[2], 0xff} palette[i] = color.RGBA{b[0], b[1], b[2], 0xff}
} }
@ -52,8 +58,7 @@ func ditherImage(img image.Image) image.Image {
ditherer := dither.NewDitherer(nordPalette) ditherer := dither.NewDitherer(nordPalette)
ditherer.Matrix = dither.FloydSteinberg ditherer.Matrix = dither.FloydSteinberg
// Dither image in a copy dst := ditherer.Dither(img)
dst := ditherer.DitherCopy(img)
return dst return dst
} }
@ -67,9 +72,9 @@ func decodeImage(imageData []byte) (image.Image, error) {
return img, nil return img, nil
} }
// DittherNord returns a Promise that takes a UintArray containing a Jpeg or png image, // Dither returns a Promise that takes a UintArray containing a Jpeg or png image,
// and resolves to a UintArray containing the dithered image. // and resolves to a UintArray containing the dithered image.
func DitherNord() js.Func { func Dither() js.Func {
return js.FuncOf(func(this js.Value, args []js.Value) interface{} { return js.FuncOf(func(this js.Value, args []js.Value) interface{} {
imageBytes := make([]byte, args[0].Length()) imageBytes := make([]byte, args[0].Length())
js.CopyBytesToGo(imageBytes, args[0]) js.CopyBytesToGo(imageBytes, args[0])
@ -79,6 +84,8 @@ func DitherNord() js.Func {
reject := args[1] reject := args[1]
go func() { go func() {
errorConstructor := js.Global().Get("Error")
// Decode image from raw bytes // Decode image from raw bytes
img, err := decodeImage(imageBytes) img, err := decodeImage(imageBytes)
if err != nil { if err != nil {
@ -98,7 +105,12 @@ func DitherNord() js.Func {
log.Println("Encoding image...") log.Println("Encoding image...")
t1 = time.Now() t1 = time.Now()
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
png.Encode(buf, ditheredImage) err = png.Encode(buf, ditheredImage)
if err != nil {
log.Printf("Error encoding image: %v\n", err)
errorObject := errorConstructor.New(err.Error())
reject.Invoke(errorObject)
}
t2 = time.Now() t2 = time.Now()
log.Printf("Image encoded in %v\n", t2.Sub(t1)) log.Printf("Image encoded in %v\n", t2.Sub(t1))