multi palette support #9

Merged
CrispyBaguette merged 5 commits from multi-palette-support into master 2021-12-13 22:15:12 +00:00
7 changed files with 126 additions and 46 deletions
Showing only changes of commit 0dc582bdd1 - Show all commits

View File

@ -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

View File

@ -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"

View File

@ -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}

View File

@ -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
View 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;

View File

@ -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();
} }

View File

@ -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))