Support dithering with other palettes
This commit is contained in:
parent
42cf96cb53
commit
0dc582bdd1
@ -4,6 +4,7 @@ import ImageOutput from "./ImageOutput";
|
|||||||
import Ditherer from "./lib/Ditherer";
|
import Ditherer from "./lib/Ditherer";
|
||||||
import ImagePreview from "./ImagePreview";
|
import ImagePreview from "./ImagePreview";
|
||||||
import Header from "./Header";
|
import Header from "./Header";
|
||||||
|
import Palette from "./Palette";
|
||||||
|
|
||||||
enum AppState {
|
enum AppState {
|
||||||
NO_IMAGE,
|
NO_IMAGE,
|
||||||
@ -16,13 +17,13 @@ function App() {
|
|||||||
const [ditheredImage, setDitheredImage] = React.useState<Blob>();
|
const [ditheredImage, setDitheredImage] = React.useState<Blob>();
|
||||||
const [appState, setAppState] = React.useState<AppState>(AppState.NO_IMAGE);
|
const [appState, setAppState] = React.useState<AppState>(AppState.NO_IMAGE);
|
||||||
|
|
||||||
const handleImageSubmit = async (data: Blob) => {
|
const handleImageSubmit = async (data: Blob, palette: Palette) => {
|
||||||
setBaseImage(data);
|
setBaseImage(data);
|
||||||
setAppState(AppState.IMAGE_LOADED);
|
setAppState(AppState.IMAGE_LOADED);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const imageArray = new Uint8ClampedArray(await data.arrayBuffer());
|
const imageArray = new Uint8ClampedArray(await data.arrayBuffer());
|
||||||
const ditheredImage = await new Ditherer().dither(imageArray);
|
const ditheredImage = await new Ditherer().dither(imageArray, palette);
|
||||||
setDitheredImage(new Blob([ditheredImage], { type: "image/png" }));
|
setDitheredImage(new Blob([ditheredImage], { type: "image/png" }));
|
||||||
setAppState(AppState.IMAGE_PROCESSED);
|
setAppState(AppState.IMAGE_PROCESSED);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -34,7 +35,7 @@ function App() {
|
|||||||
return (
|
return (
|
||||||
<div className="bg-nord-6 text-nord-0 min-h-screen">
|
<div className="bg-nord-6 text-nord-0 min-h-screen">
|
||||||
<Header />
|
<Header />
|
||||||
<main className="container mx-auto">
|
<main className="container mx-auto pb-5">
|
||||||
<article className="max-w-prose mx-auto pb-5 px-2">
|
<article className="max-w-prose mx-auto pb-5 px-2">
|
||||||
<h1 className="text-3xl text-center pb-3">
|
<h1 className="text-3xl text-center pb-3">
|
||||||
Go+Wasm image dithering tool
|
Go+Wasm image dithering tool
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
import React, { FormEventHandler } from "react";
|
import React, { FormEventHandler } from "react";
|
||||||
|
import Palette, { palettes } from "./Palette";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onImageSubmit: (image: Blob) => void;
|
onImageSubmit: (image: Blob, palette: Palette) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ImageInput({ onImageSubmit }: Props) {
|
function ImageInput({ onImageSubmit }: Props) {
|
||||||
const fileInputRef = React.useRef<HTMLInputElement>(null);
|
const fileInputRef = React.useRef<HTMLInputElement>(null);
|
||||||
|
const [paletteIndex, setPaletteIndex] = React.useState(0);
|
||||||
|
|
||||||
const handleSubmit: FormEventHandler = (e) => {
|
const handleSubmit: FormEventHandler = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@ -17,7 +19,11 @@ function ImageInput({ onImageSubmit }: Props) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
onImageSubmit(fileInputRef.current.files[0]);
|
onImageSubmit(fileInputRef.current.files[0], palettes[paletteIndex]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePaletteChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
setPaletteIndex(parseInt(event.target.value));
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -34,12 +40,20 @@ function ImageInput({ onImageSubmit }: Props) {
|
|||||||
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"
|
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>
|
||||||
{/* <label>
|
<label>
|
||||||
<span className="block">Select a color palette:</span>
|
<span className="block">Select a color palette:</span>
|
||||||
<select className="form-select block w-full mt-1">
|
<select
|
||||||
<option value="nord">Nord</option>
|
value={paletteIndex}
|
||||||
|
onChange={handlePaletteChange}
|
||||||
|
className="form-select block w-full mt-1"
|
||||||
|
>
|
||||||
|
{palettes.map((palette, i) => (
|
||||||
|
<option key={i} value={i}>
|
||||||
|
{palette.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
</select>
|
</select>
|
||||||
</label> */}
|
</label>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
value="Go"
|
value="Go"
|
||||||
|
@ -9,10 +9,10 @@ interface OutputProps {
|
|||||||
function ImageOutput({ imageData }: OutputProps) {
|
function ImageOutput({ imageData }: OutputProps) {
|
||||||
const imageUrl = URL.createObjectURL(imageData);
|
const imageUrl = URL.createObjectURL(imageData);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
URL.revokeObjectURL(imageUrl);
|
URL.revokeObjectURL(imageUrl);
|
||||||
}
|
};
|
||||||
}, [imageUrl]);
|
}, [imageUrl]);
|
||||||
|
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
@ -21,11 +21,11 @@ function ImageOutput({ imageData }: OutputProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto">
|
<div className="max-w-4xl mx-auto">
|
||||||
<img
|
<img
|
||||||
alt="dithering output"
|
alt="dithering output"
|
||||||
src={imageUrl}
|
src={imageUrl}
|
||||||
className="object-contain max-h-96 max-w-96 mx-auto"
|
className="object-cover min-w-[70%] my-4 px-2 mx-auto"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
|
@ -7,18 +7,18 @@ interface ImagePreviewProps {
|
|||||||
function ImagePreview({ imageData }: ImagePreviewProps) {
|
function ImagePreview({ imageData }: ImagePreviewProps) {
|
||||||
const imageUrl = URL.createObjectURL(imageData);
|
const imageUrl = URL.createObjectURL(imageData);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
URL.revokeObjectURL(imageUrl);
|
URL.revokeObjectURL(imageUrl);
|
||||||
}
|
};
|
||||||
}, [imageUrl]);
|
}, [imageUrl]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="blur-sm mx-auto">
|
<div className="blur-sm max-w-4xl mx-auto">
|
||||||
<img
|
<img
|
||||||
alt="preview"
|
alt="preview"
|
||||||
src={imageUrl}
|
src={imageUrl}
|
||||||
className="object-contain max-h-96 max-w-96 mx-auto"
|
className="object-cover min-w-[70%] my-4 px-2 mx-auto"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
68
client/src/Palette.ts
Normal file
68
client/src/Palette.ts
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
class Palette {
|
||||||
|
colors: string[];
|
||||||
|
label: string;
|
||||||
|
|
||||||
|
constructor(label: string, colors: string[]) {
|
||||||
|
this.label = label;
|
||||||
|
this.colors = colors;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const nord = new Palette("Nord", [
|
||||||
|
"2e3440",
|
||||||
|
"3b4252",
|
||||||
|
"434c5e",
|
||||||
|
"4c566a",
|
||||||
|
"d8dee9",
|
||||||
|
"e5e9f0",
|
||||||
|
"eceff4",
|
||||||
|
"8fbcbb",
|
||||||
|
"88c0d0",
|
||||||
|
"81a1c1",
|
||||||
|
"5e81ac",
|
||||||
|
"bf616a",
|
||||||
|
"d08770",
|
||||||
|
"ebcb8b",
|
||||||
|
"a3be8c",
|
||||||
|
"b48ead",
|
||||||
|
]);
|
||||||
|
|
||||||
|
const monokai = new Palette("Monokai", [
|
||||||
|
"2e2e2e",
|
||||||
|
"797979",
|
||||||
|
"d6d6d6",
|
||||||
|
"e5b567",
|
||||||
|
"b4d273",
|
||||||
|
"e87d3e",
|
||||||
|
"9e86c8",
|
||||||
|
"b05279",
|
||||||
|
"6c99bb",
|
||||||
|
]);
|
||||||
|
|
||||||
|
const greyScale1bit = new Palette("Grey Scale 1 bit (Black & White)", [
|
||||||
|
"000000",
|
||||||
|
"ffffff",
|
||||||
|
]);
|
||||||
|
|
||||||
|
const greyScale2bits = new Palette("Grey Scale 2 bits", [
|
||||||
|
"000000",
|
||||||
|
"676767",
|
||||||
|
"b6b6b6",
|
||||||
|
"ffffff",
|
||||||
|
]);
|
||||||
|
|
||||||
|
let greyColors: string[] = [];
|
||||||
|
for (let i = 0; i < 256; i++) {
|
||||||
|
const hexValue = i.toString(16).padStart(2, "0");
|
||||||
|
greyColors.push(`${hexValue}${hexValue}${hexValue}`);
|
||||||
|
}
|
||||||
|
const greyScale8bits = new Palette("Grey Scale 8 bits", greyColors);
|
||||||
|
|
||||||
|
export const palettes = [
|
||||||
|
nord,
|
||||||
|
monokai,
|
||||||
|
greyScale1bit,
|
||||||
|
greyScale2bits,
|
||||||
|
greyScale8bits,
|
||||||
|
];
|
||||||
|
export default Palette;
|
@ -1,9 +1,14 @@
|
|||||||
|
import Palette from "../Palette";
|
||||||
|
|
||||||
class Ditherer {
|
class Ditherer {
|
||||||
async dither(image: Uint8ClampedArray): Promise<Uint8ClampedArray> {
|
async dither(
|
||||||
|
image: Uint8ClampedArray,
|
||||||
|
palette: Palette
|
||||||
|
): Promise<Uint8ClampedArray> {
|
||||||
const worker: any = await wasmWorker("./main.wasm");
|
const worker: any = await wasmWorker("./main.wasm");
|
||||||
let output: Uint8ClampedArray;
|
let output: Uint8ClampedArray;
|
||||||
try {
|
try {
|
||||||
output = await worker.dither(image);
|
output = await worker.dither(image, palette.colors);
|
||||||
} finally {
|
} finally {
|
||||||
worker.terminate();
|
worker.terminate();
|
||||||
}
|
}
|
||||||
|
42
src/wasm.go
42
src/wasm.go
@ -15,25 +15,6 @@ import (
|
|||||||
"github.com/makeworld-the-better-one/dither/v2"
|
"github.com/makeworld-the-better-one/dither/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
var nordPalette, _ = buildPalette([]string{
|
|
||||||
"2e3440",
|
|
||||||
"3b4252",
|
|
||||||
"434c5e",
|
|
||||||
"4c566a",
|
|
||||||
"d8dee9",
|
|
||||||
"e5e9f0",
|
|
||||||
"eceff4",
|
|
||||||
"8fbcbb",
|
|
||||||
"88c0d0",
|
|
||||||
"81a1c1",
|
|
||||||
"5e81ac",
|
|
||||||
"bf616a",
|
|
||||||
"d08770",
|
|
||||||
"ebcb8b",
|
|
||||||
"a3be8c",
|
|
||||||
"b48ead",
|
|
||||||
})
|
|
||||||
|
|
||||||
func buildPalette(pal []string) (color.Palette, error) {
|
func buildPalette(pal []string) (color.Palette, error) {
|
||||||
var palette = make(color.Palette, len(pal))
|
var palette = make(color.Palette, len(pal))
|
||||||
|
|
||||||
@ -53,9 +34,9 @@ func buildPalette(pal []string) (color.Palette, error) {
|
|||||||
return palette, nil
|
return palette, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ditherImage(img image.Image) image.Image {
|
func ditherImage(img image.Image, palette color.Palette) image.Image {
|
||||||
// Build ditherer
|
// Build ditherer
|
||||||
ditherer := dither.NewDitherer(nordPalette)
|
ditherer := dither.NewDitherer(palette)
|
||||||
ditherer.Matrix = dither.FloydSteinberg
|
ditherer.Matrix = dither.FloydSteinberg
|
||||||
|
|
||||||
dst := ditherer.Dither(img)
|
dst := ditherer.Dither(img)
|
||||||
@ -79,9 +60,9 @@ func Dither() js.Func {
|
|||||||
imageBytes := make([]byte, args[0].Length())
|
imageBytes := make([]byte, args[0].Length())
|
||||||
js.CopyBytesToGo(imageBytes, args[0])
|
js.CopyBytesToGo(imageBytes, args[0])
|
||||||
|
|
||||||
handler := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
handler := js.FuncOf(func(promiseThis js.Value, promiseArgs []js.Value) interface{} {
|
||||||
resolve := args[0]
|
resolve := promiseArgs[0]
|
||||||
reject := args[1]
|
reject := promiseArgs[1]
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
errorConstructor := js.Global().Get("Error")
|
errorConstructor := js.Global().Get("Error")
|
||||||
@ -94,10 +75,21 @@ func Dither() js.Func {
|
|||||||
reject.Invoke(errorObject)
|
reject.Invoke(errorObject)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build palette
|
||||||
|
colors := make([]string, args[1].Length())
|
||||||
|
for i := 0; i < args[1].Length(); i++ {
|
||||||
|
colors[i] = args[1].Index(i).String()
|
||||||
|
}
|
||||||
|
palette, err := buildPalette(colors)
|
||||||
|
if err != nil {
|
||||||
|
errorObject := errorConstructor.New(err.Error())
|
||||||
|
reject.Invoke(errorObject)
|
||||||
|
}
|
||||||
|
|
||||||
// Perform dithering
|
// Perform dithering
|
||||||
log.Println("Dithering image...")
|
log.Println("Dithering image...")
|
||||||
t1 := time.Now()
|
t1 := time.Now()
|
||||||
ditheredImage := ditherImage(img)
|
ditheredImage := ditherImage(img, palette)
|
||||||
t2 := time.Now()
|
t2 := time.Now()
|
||||||
log.Printf("Image dithered in %v\n", t2.Sub(t1))
|
log.Printf("Image dithered in %v\n", t2.Sub(t1))
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user