React client app #5
16
.gitignore
vendored
@ -1,15 +1 @@
|
|||||||
# Binaries for programs and plugins
|
*.wasm
|
||||||
*.exe
|
|
||||||
*.exe~
|
|
||||||
*.dll
|
|
||||||
*.so
|
|
||||||
*.dylib
|
|
||||||
|
|
||||||
# Test binary, built with `go test -c`
|
|
||||||
*.test
|
|
||||||
|
|
||||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
|
||||||
*.out
|
|
||||||
|
|
||||||
# Dependency directories (remove the comment below to include it)
|
|
||||||
# vendor/
|
|
10
Makefile
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
build-wasm:
|
||||||
|
cd src && GOOS=js GOARCH=wasm go build -o ../client/public/main.wasm .
|
||||||
|
|
||||||
|
build-wasm-release:
|
||||||
|
cd src && GOOS=js GOARCH=wasm go build -ldflags="-s -w" -o ../client/public/main.wasm .
|
||||||
|
|
||||||
|
build-react:
|
||||||
|
cd client && npm run build
|
||||||
|
|
||||||
|
all: build-wasm-release build-react
|
46
client/.gitignore
vendored
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
15
client/.vscode/launch.json
vendored
Normal 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}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
46
client/README.md
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
# Getting Started with Create React App
|
||||||
|
|
||||||
|
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
||||||
|
|
||||||
|
## Available Scripts
|
||||||
|
|
||||||
|
In the project directory, you can run:
|
||||||
|
|
||||||
|
### `npm start`
|
||||||
|
|
||||||
|
Runs the app in the development mode.\
|
||||||
|
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
|
||||||
|
|
||||||
|
The page will reload if you make edits.\
|
||||||
|
You will also see any lint errors in the console.
|
||||||
|
|
||||||
|
### `npm test`
|
||||||
|
|
||||||
|
Launches the test runner in the interactive watch mode.\
|
||||||
|
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
||||||
|
|
||||||
|
### `npm run build`
|
||||||
|
|
||||||
|
Builds the app for production to the `build` folder.\
|
||||||
|
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||||
|
|
||||||
|
The build is minified and the filenames include the hashes.\
|
||||||
|
Your app is ready to be deployed!
|
||||||
|
|
||||||
|
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
||||||
|
|
||||||
|
### `npm run eject`
|
||||||
|
|
||||||
|
**Note: this is a one-way operation. Once you `eject`, you can’t go back!**
|
||||||
|
|
||||||
|
If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
||||||
|
|
||||||
|
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
|
||||||
|
|
||||||
|
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
|
||||||
|
|
||||||
|
## Learn More
|
||||||
|
|
||||||
|
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
||||||
|
|
||||||
|
To learn React, check out the [React documentation](https://reactjs.org/).
|
25297
client/package-lock.json
generated
Normal file
56
client/package.json
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
{
|
||||||
|
"name": "client",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"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/react": "^11.2.7",
|
||||||
|
"@testing-library/user-event": "^12.8.3",
|
||||||
|
"@types/jest": "^26.0.24",
|
||||||
|
"@types/node": "^12.20.37",
|
||||||
|
"@types/react": "^17.0.37",
|
||||||
|
"@types/react-dom": "^17.0.11",
|
||||||
|
"classnames": "^2.3.1",
|
||||||
|
"file-saver": "^2.0.5",
|
||||||
|
"react": "^17.0.2",
|
||||||
|
"react-dom": "^17.0.2",
|
||||||
|
"react-scripts": "next",
|
||||||
|
"typescript": "^4.5.3",
|
||||||
|
"web-vitals": "^1.1.2"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "react-scripts start",
|
||||||
|
"build": "react-scripts build",
|
||||||
|
"test": "react-scripts test",
|
||||||
|
"eject": "react-scripts eject"
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"extends": [
|
||||||
|
"react-app",
|
||||||
|
"react-app/jest"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"browserslist": {
|
||||||
|
"production": [
|
||||||
|
">0.2%",
|
||||||
|
"not dead",
|
||||||
|
"not op_mini all"
|
||||||
|
],
|
||||||
|
"development": [
|
||||||
|
"last 1 chrome version",
|
||||||
|
"last 1 firefox 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
@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
BIN
client/public/android-chrome-192x192.png
Normal file
After Width: | Height: | Size: 4.0 KiB |
BIN
client/public/android-chrome-512x512.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
client/public/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 3.8 KiB |
BIN
client/public/favicon-16x16.png
Normal file
After Width: | Height: | Size: 400 B |
BIN
client/public/favicon-32x32.png
Normal file
After Width: | Height: | Size: 598 B |
BIN
client/public/favicon.ico
Normal file
After Width: | Height: | Size: 15 KiB |
23
client/public/index.html
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="theme-color" content="#000000" />
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="Web site created using create-react-app"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="apple-touch-icon"
|
||||||
|
href="%PUBLIC_URL%/android-chrome-192x192.png"
|
||||||
|
/>
|
||||||
|
<link rel="manifest" href="%PUBLIC_URL%/site.webmanifest" />
|
||||||
|
<title>Palette Switcher</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
<div id="root"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
3
client/public/robots.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# https://www.robotstxt.org/robotstxt.html
|
||||||
|
User-agent: *
|
||||||
|
Disallow:
|
19
client/public/site.webmanifest
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"name": "",
|
||||||
|
"short_name": "",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/android-chrome-192x192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/android-chrome-512x512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"theme_color": "#ffffff",
|
||||||
|
"background_color": "#ffffff",
|
||||||
|
"display": "standalone"
|
||||||
|
}
|
744
client/public/wasm_exec.js
Normal file
@ -0,0 +1,744 @@
|
|||||||
|
/* eslint-disable no-undef */
|
||||||
|
/* eslint-disable no-restricted-globals */
|
||||||
|
// 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})();
|
@ -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",
|
||||||
@ -46,12 +46,11 @@ addEventListener(
|
|||||||
eventId: eventId,
|
eventId: eventId,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
|
||||||
self.postMessage({
|
self.postMessage({
|
||||||
eventType: "ERROR",
|
eventType: "ERROR",
|
||||||
eventData:
|
eventData:
|
||||||
"An error occured executing WASM instance function: " +
|
"An error occured executing WASM instance function: " +
|
||||||
error.toString(),
|
e.toString(),
|
||||||
eventId: eventId,
|
eventId: eventId,
|
||||||
});
|
});
|
||||||
}
|
}
|
64
client/src/App.tsx
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import React from "react";
|
||||||
|
import ImageInput from "./ImageInput";
|
||||||
|
import ImageOutput from "./ImageOutput";
|
||||||
|
import Ditherer from "./lib/Ditherer";
|
||||||
|
import ImagePreview from "./ImagePreview";
|
||||||
|
import Header from "./Header";
|
||||||
|
|
||||||
|
enum AppState {
|
||||||
|
NO_IMAGE,
|
||||||
|
IMAGE_LOADED,
|
||||||
|
IMAGE_PROCESSED,
|
||||||
|
}
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const [baseImage, setBaseImage] = React.useState<Uint8ClampedArray>();
|
||||||
|
const [ditheredImage, setDitheredImage] = React.useState<Uint8ClampedArray>();
|
||||||
|
const [appState, setAppState] = React.useState<AppState>(AppState.NO_IMAGE);
|
||||||
|
|
||||||
|
const handleImageSubmit = async (data: Uint8ClampedArray) => {
|
||||||
|
setBaseImage(data);
|
||||||
|
setAppState(AppState.IMAGE_LOADED);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const ditheredImage = await new Ditherer().dither(data);
|
||||||
|
setDitheredImage(ditheredImage);
|
||||||
|
setAppState(AppState.IMAGE_PROCESSED);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
window.alert("Something went wrong. Please try again.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-nord-6 text-nord-0 min-h-screen">
|
||||||
|
<Header />
|
||||||
|
<main className="container mx-auto">
|
||||||
|
<article className="max-w-prose mx-auto pb-5 px-2">
|
||||||
|
<h1 className="text-3xl text-center pb-3">
|
||||||
|
Go+Wasm image dithering tool
|
||||||
|
</h1>
|
||||||
|
<aside className="text-xl text-center pb-3">
|
||||||
|
Featuring the Nord Color Palette
|
||||||
|
</aside>
|
||||||
|
<p>
|
||||||
|
Load an image, click Go and wait (potentially for a while) for the
|
||||||
|
image to be processed using the Floyd-Steinberg algorithm.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
WebAssembly might run out of memory when processing larger images.
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
<ImageInput onImageSubmit={handleImageSubmit}></ImageInput>
|
||||||
|
{appState === AppState.IMAGE_LOADED && (
|
||||||
|
<ImagePreview imageData={baseImage as Uint8ClampedArray} />
|
||||||
|
)}
|
||||||
|
{appState === AppState.IMAGE_PROCESSED && (
|
||||||
|
<ImageOutput imageData={ditheredImage as Uint8ClampedArray} />
|
||||||
|
)}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
43
client/src/Header.tsx
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
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="flex items-center bg-nord-1 text-nord-5 py-3 px-2">
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
href="https://github.com/CrispyBaguette/wasm-palette-converter"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faGithub}
|
||||||
|
className="fa-2x hover:text-nord-7 mx-2"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
href="https://blog.bruyant.xyz"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faIgloo}
|
||||||
|
className="fa-2x hover:text-nord-7 mx-2"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<span className="text-sm">
|
||||||
|
Powered by{" "}
|
||||||
|
<a
|
||||||
|
href="https://ipfs.io"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="bg-nord-5 text-nord-1 hover:bg-nord-7 px-1 "
|
||||||
|
>
|
||||||
|
IPFS
|
||||||
|
</a>
|
||||||
|
. Visit using your own node !
|
||||||
|
</span>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export default Header;
|
64
client/src/ImageInput.tsx
Normal 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;
|
10
client/src/ImageOutput.css
Normal 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;
|
||||||
|
}
|
41
client/src/ImageOutput.tsx
Normal 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-center 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-center text-nord-0 bg-nord-4 hover:bg-nord-5"
|
||||||
|
>
|
||||||
|
Open in new tab
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ImageOutput;
|
20
client/src/ImagePreview.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
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;
|
22
client/src/index.css
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--nord-0: #2e3440;
|
||||||
|
--nord-1: #3b4252;
|
||||||
|
--nord-2: #434c5e;
|
||||||
|
--nord-3: #4c566a;
|
||||||
|
--nord-4: #d8dee9;
|
||||||
|
--nord-5: #e5e9f0;
|
||||||
|
--nord-6: #eceff4;
|
||||||
|
--nord-7: #8fbcbb;
|
||||||
|
--nord-8: #88c0d0;
|
||||||
|
--nord-9: #81a1c1;
|
||||||
|
--nord-10: #5e81ac;
|
||||||
|
--nord-11: #bf616a;
|
||||||
|
--nord-12: #d08770;
|
||||||
|
--nord-13: #ebcb8b;
|
||||||
|
--nord-14: #a3be8c;
|
||||||
|
--nord-15: #b48ead;
|
||||||
|
}
|
17
client/src/index.tsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import './index.css';
|
||||||
|
import App from './App';
|
||||||
|
import reportWebVitals from './reportWebVitals';
|
||||||
|
|
||||||
|
ReactDOM.render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>,
|
||||||
|
document.getElementById('root')
|
||||||
|
);
|
||||||
|
|
||||||
|
// If you want to start measuring performance in your app, pass a function
|
||||||
|
// to log results (for example: reportWebVitals(console.log))
|
||||||
|
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
||||||
|
reportWebVitals();
|
71
client/src/lib/Ditherer.ts
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
class Ditherer {
|
||||||
|
async dither(image: Uint8ClampedArray): Promise<Uint8ClampedArray> {
|
||||||
|
const worker: any = await wasmWorker("/main.wasm");
|
||||||
|
let output: Uint8ClampedArray;
|
||||||
|
try {
|
||||||
|
output = await worker.dither(image);
|
||||||
|
} finally {
|
||||||
|
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;
|
1
client/src/logo.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>
|
After Width: | Height: | Size: 2.6 KiB |
1
client/src/react-app-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/// <reference types="react-scripts" />
|
15
client/src/reportWebVitals.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { ReportHandler } from 'web-vitals';
|
||||||
|
|
||||||
|
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
|
||||||
|
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||||
|
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||||
|
getCLS(onPerfEntry);
|
||||||
|
getFID(onPerfEntry);
|
||||||
|
getFCP(onPerfEntry);
|
||||||
|
getLCP(onPerfEntry);
|
||||||
|
getTTFB(onPerfEntry);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default reportWebVitals;
|
5
client/src/setupTests.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||||
|
// allows you to do things like:
|
||||||
|
// expect(element).toHaveTextContent(/react/i)
|
||||||
|
// learn more: https://github.com/testing-library/jest-dom
|
||||||
|
import '@testing-library/jest-dom';
|
54
client/tailwind.config.js
Normal 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"],
|
||||||
|
};
|
26
client/tsconfig.json
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es5",
|
||||||
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"dom.iterable",
|
||||||
|
"esnext",
|
||||||
|
],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"strict": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx"
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src"
|
||||||
|
]
|
||||||
|
}
|
99
dist/index.html
vendored
@ -1,99 +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;
|
|
||||||
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+ */
|
|
||||||
}
|
|
||||||
@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
@ -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;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
BIN
dist/main.wasm
vendored
636
dist/wasm_exec.js
vendored
@ -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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})();
|
|
5
go.mod
@ -1,5 +0,0 @@
|
|||||||
module github.com/CrispyBaguette/wasm-palette-converter
|
|
||||||
|
|
||||||
go 1.17
|
|
||||||
|
|
||||||
require github.com/makeworld-the-better-one/dither/v2 v2.2.0 // indirect
|
|
15
src/.gitignore
vendored
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
# Binaries for programs and plugins
|
||||||
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
|
||||||
|
# Test binary, built with `go test -c`
|
||||||
|
*.test
|
||||||
|
|
||||||
|
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||||
|
*.out
|
||||||
|
|
||||||
|
# Dependency directories (remove the comment below to include it)
|
||||||
|
# vendor/
|
5
src/go.mod
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
module github.com/CrispyBaguette/wasm-palette-converter
|
||||||
|
|
||||||
|
go 1.17
|
||||||
|
|
||||||
|
require github.com/makeworld-the-better-one/dither/v2 v2.2.0
|
@ -1,8 +1,12 @@
|
|||||||
|
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/makeworld-the-better-one/dither/v2 v2.2.0 h1:VTMAiyyO1YIO07fZwuLNZZasJgKUmvsIA48ze3ALHPQ=
|
github.com/makeworld-the-better-one/dither/v2 v2.2.0 h1:VTMAiyyO1YIO07fZwuLNZZasJgKUmvsIA48ze3ALHPQ=
|
||||||
github.com/makeworld-the-better-one/dither/v2 v2.2.0/go.mod h1:VBtN8DXO7SNtyGmLiGA7IsFeKrBkQPze1/iAeM95arc=
|
github.com/makeworld-the-better-one/dither/v2 v2.2.0/go.mod h1:VBtN8DXO7SNtyGmLiGA7IsFeKrBkQPze1/iAeM95arc=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
@ -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)
|
||||||
}
|
}
|
@ -2,8 +2,8 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
b64 "encoding/base64"
|
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
"image/color"
|
"image/color"
|
||||||
_ "image/jpeg"
|
_ "image/jpeg"
|
||||||
@ -42,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}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,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
|
||||||
}
|
}
|
||||||
@ -68,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 string containing a base64 encoded png 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])
|
||||||
@ -80,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 {
|
||||||
@ -96,12 +102,25 @@ func DitherNord() js.Func {
|
|||||||
log.Printf("Image dithered in %v\n", t2.Sub(t1))
|
log.Printf("Image dithered in %v\n", t2.Sub(t1))
|
||||||
|
|
||||||
// Encode as PNG
|
// Encode as PNG
|
||||||
|
log.Println("Encoding image...")
|
||||||
|
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()
|
||||||
|
log.Printf("Image encoded in %v\n", t2.Sub(t1))
|
||||||
|
|
||||||
// Encode as img src b64 string and resolve
|
log.Println("Copying image to JS...")
|
||||||
encodedImage := b64.StdEncoding.EncodeToString(buf.Bytes())
|
t1 = time.Now()
|
||||||
resolve.Invoke(js.ValueOf(encodedImage))
|
encodedImage := js.Global().Get("Uint8ClampedArray").New(len(buf.Bytes()))
|
||||||
|
js.CopyBytesToJS(encodedImage, buf.Bytes())
|
||||||
|
t2 = time.Now()
|
||||||
|
log.Printf("Image copied in %v\n", t2.Sub(t1))
|
||||||
|
resolve.Invoke(encodedImage)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
return nil
|
return nil
|
11
wasm-palette-converter.code-workspace
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"folders": [
|
||||||
|
{
|
||||||
|
"path": "./src"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./client"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": {}
|
||||||
|
}
|