initial commit
505
dither4.js
Normal file
|
@ -0,0 +1,505 @@
|
||||||
|
class RGB {
|
||||||
|
constructor(r, g, b) {
|
||||||
|
this.r = r;
|
||||||
|
this.g = g;
|
||||||
|
this.b = b;
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromHex(val) {
|
||||||
|
let r = val & 0xff;
|
||||||
|
let g = (val >> 8) & 0xff;
|
||||||
|
let b = (val >> 16) & 0xff;
|
||||||
|
return new RGB(r,g,b);
|
||||||
|
}
|
||||||
|
|
||||||
|
add(other) {
|
||||||
|
return new RGB(
|
||||||
|
this.r + other.r,
|
||||||
|
this.g + other.g,
|
||||||
|
this.b + other.b
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
difference(other) {
|
||||||
|
return new RGB(
|
||||||
|
this.r - other.r,
|
||||||
|
this.g - other.g,
|
||||||
|
this.b - other.b
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
magnitude() {
|
||||||
|
return Math.sqrt(
|
||||||
|
this.r * this.r +
|
||||||
|
this.g * this.g +
|
||||||
|
this.b * this.b
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
distance(other) {
|
||||||
|
return this.difference(other).magnitude();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// snarfed from https://lospec.com/palette-list/atari-8-bit-family-gtia
|
||||||
|
// which was calculated with Retrospecs App's Atari 800 emulator
|
||||||
|
let palette256 = [
|
||||||
|
0x000000,
|
||||||
|
0x111111,
|
||||||
|
0x222222,
|
||||||
|
0x333333,
|
||||||
|
0x444444,
|
||||||
|
0x555555,
|
||||||
|
0x666666,
|
||||||
|
0x777777,
|
||||||
|
0x888888,
|
||||||
|
0x999999,
|
||||||
|
0xaaaaaa,
|
||||||
|
0xbbbbbb,
|
||||||
|
0xcccccc,
|
||||||
|
0xdddddd,
|
||||||
|
0xeeeeee,
|
||||||
|
0xffffff,
|
||||||
|
0x190700,
|
||||||
|
0x2a1800,
|
||||||
|
0x3b2900,
|
||||||
|
0x4c3a00,
|
||||||
|
0x5d4b00,
|
||||||
|
0x6e5c00,
|
||||||
|
0x7f6d00,
|
||||||
|
0x907e09,
|
||||||
|
0xa18f1a,
|
||||||
|
0xb3a02b,
|
||||||
|
0xc3b13c,
|
||||||
|
0xd4c24d,
|
||||||
|
0xe5d35e,
|
||||||
|
0xf7e46f,
|
||||||
|
0xfff582,
|
||||||
|
0xffff96,
|
||||||
|
0x310000,
|
||||||
|
0x3f0000,
|
||||||
|
0x531700,
|
||||||
|
0x642800,
|
||||||
|
0x753900,
|
||||||
|
0x864a00,
|
||||||
|
0x975b0a,
|
||||||
|
0xa86c1b,
|
||||||
|
0xb97d2c,
|
||||||
|
0xca8e3d,
|
||||||
|
0xdb9f4e,
|
||||||
|
0xecb05f,
|
||||||
|
0xfdc170,
|
||||||
|
0xffd285,
|
||||||
|
0xffe39c,
|
||||||
|
0xfff4b2,
|
||||||
|
0x420404,
|
||||||
|
0x4f0000,
|
||||||
|
0x600800,
|
||||||
|
0x711900,
|
||||||
|
0x822a0d,
|
||||||
|
0x933b1e,
|
||||||
|
0xa44c2f,
|
||||||
|
0xb55d40,
|
||||||
|
0xc66e51,
|
||||||
|
0xd77f62,
|
||||||
|
0xe89073,
|
||||||
|
0xf9a183,
|
||||||
|
0xffb298,
|
||||||
|
0xffc3ae,
|
||||||
|
0xffd4c4,
|
||||||
|
0xffe5da,
|
||||||
|
0x410103,
|
||||||
|
0x50000f,
|
||||||
|
0x61001b,
|
||||||
|
0x720f2b,
|
||||||
|
0x83203c,
|
||||||
|
0x94314d,
|
||||||
|
0xa5425e,
|
||||||
|
0xb6536f,
|
||||||
|
0xc76480,
|
||||||
|
0xd87591,
|
||||||
|
0xe986a2,
|
||||||
|
0xfa97b3,
|
||||||
|
0xffa8c8,
|
||||||
|
0xffb9de,
|
||||||
|
0xffcaef,
|
||||||
|
0xfbdcf6,
|
||||||
|
0x330035,
|
||||||
|
0x440041,
|
||||||
|
0x55004c,
|
||||||
|
0x660c5c,
|
||||||
|
0x771d6d,
|
||||||
|
0x882e7e,
|
||||||
|
0x993f8f,
|
||||||
|
0xaa50a0,
|
||||||
|
0xbb61b1,
|
||||||
|
0xcc72c2,
|
||||||
|
0xdd83d3,
|
||||||
|
0xee94e4,
|
||||||
|
0xffa5e4,
|
||||||
|
0xffb6e9,
|
||||||
|
0xffc7ee,
|
||||||
|
0xffd8f3,
|
||||||
|
0x1d005c,
|
||||||
|
0x2e0068,
|
||||||
|
0x400074,
|
||||||
|
0x511084,
|
||||||
|
0x622195,
|
||||||
|
0x7332a6,
|
||||||
|
0x8443b7,
|
||||||
|
0x9554c8,
|
||||||
|
0xa665d9,
|
||||||
|
0xb776ea,
|
||||||
|
0xc887eb,
|
||||||
|
0xd998eb,
|
||||||
|
0xe9a9ec,
|
||||||
|
0xfbbaeb,
|
||||||
|
0xffcbef,
|
||||||
|
0xffdff9,
|
||||||
|
0x020071,
|
||||||
|
0x13007d,
|
||||||
|
0x240b8c,
|
||||||
|
0x351c9d,
|
||||||
|
0x462dae,
|
||||||
|
0x573ebf,
|
||||||
|
0x684fd0,
|
||||||
|
0x7960e1,
|
||||||
|
0x8a71f2,
|
||||||
|
0x9b82f7,
|
||||||
|
0xac93f7,
|
||||||
|
0xbda4f7,
|
||||||
|
0xceb5f7,
|
||||||
|
0xdfc6f7,
|
||||||
|
0xf0d7f7,
|
||||||
|
0xffe8f8,
|
||||||
|
0x000068,
|
||||||
|
0x000a7c,
|
||||||
|
0x081b90,
|
||||||
|
0x192ca1,
|
||||||
|
0x2a3db2,
|
||||||
|
0x3b4ec3,
|
||||||
|
0x4c5fd4,
|
||||||
|
0x5d70e5,
|
||||||
|
0x6e81f6,
|
||||||
|
0x7f92ff,
|
||||||
|
0x90a3ff,
|
||||||
|
0xa1b4ff,
|
||||||
|
0xb2c5ff,
|
||||||
|
0xc3d6ff,
|
||||||
|
0xd4e7ff,
|
||||||
|
0xe5f8ff,
|
||||||
|
0x000a4d,
|
||||||
|
0x001b63,
|
||||||
|
0x002c79,
|
||||||
|
0x023d8f,
|
||||||
|
0x134ea0,
|
||||||
|
0x245fb1,
|
||||||
|
0x3570c2,
|
||||||
|
0x4681d3,
|
||||||
|
0x5792e4,
|
||||||
|
0x68a3f5,
|
||||||
|
0x79b4ff,
|
||||||
|
0x8ac5ff,
|
||||||
|
0x9bd6ff,
|
||||||
|
0xace7ff,
|
||||||
|
0xbdf8ff,
|
||||||
|
0xceffff,
|
||||||
|
0x001a26,
|
||||||
|
0x002b3c,
|
||||||
|
0x003c52,
|
||||||
|
0x004d68,
|
||||||
|
0x065e7c,
|
||||||
|
0x176f8d,
|
||||||
|
0x28809e,
|
||||||
|
0x3991af,
|
||||||
|
0x4aa2c0,
|
||||||
|
0x5bb3d1,
|
||||||
|
0x6cc4e2,
|
||||||
|
0x7dd5f3,
|
||||||
|
0x8ee6ff,
|
||||||
|
0x9ff7ff,
|
||||||
|
0xb0ffff,
|
||||||
|
0xc1ffff,
|
||||||
|
0x01250a,
|
||||||
|
0x023610,
|
||||||
|
0x004622,
|
||||||
|
0x005738,
|
||||||
|
0x05684d,
|
||||||
|
0x16795e,
|
||||||
|
0x278a6f,
|
||||||
|
0x389b80,
|
||||||
|
0x49ac91,
|
||||||
|
0x5abda2,
|
||||||
|
0x6bceb3,
|
||||||
|
0x7cdfc4,
|
||||||
|
0x8df0d5,
|
||||||
|
0x9effe5,
|
||||||
|
0xaffff1,
|
||||||
|
0xc0fffd,
|
||||||
|
0x04260d,
|
||||||
|
0x043811,
|
||||||
|
0x054713,
|
||||||
|
0x005a1b,
|
||||||
|
0x106b1b,
|
||||||
|
0x217c2c,
|
||||||
|
0x328d3d,
|
||||||
|
0x439e4e,
|
||||||
|
0x54af5f,
|
||||||
|
0x65c070,
|
||||||
|
0x76d181,
|
||||||
|
0x87e292,
|
||||||
|
0x98f3a3,
|
||||||
|
0xa9ffb3,
|
||||||
|
0xbaffbf,
|
||||||
|
0xcbffcb,
|
||||||
|
0x00230a,
|
||||||
|
0x003510,
|
||||||
|
0x044613,
|
||||||
|
0x155613,
|
||||||
|
0x266713,
|
||||||
|
0x377813,
|
||||||
|
0x488914,
|
||||||
|
0x599a25,
|
||||||
|
0x6aab36,
|
||||||
|
0x7bbc47,
|
||||||
|
0x8ccd58,
|
||||||
|
0x9dde69,
|
||||||
|
0xaeef7a,
|
||||||
|
0xbfff8b,
|
||||||
|
0xd0ff97,
|
||||||
|
0xe1ffa3,
|
||||||
|
0x001707,
|
||||||
|
0x0e2808,
|
||||||
|
0x1f3908,
|
||||||
|
0x304a08,
|
||||||
|
0x415b08,
|
||||||
|
0x526c08,
|
||||||
|
0x637d08,
|
||||||
|
0x748e0d,
|
||||||
|
0x859f1e,
|
||||||
|
0x96b02f,
|
||||||
|
0xa7c140,
|
||||||
|
0xb8d251,
|
||||||
|
0xc9e362,
|
||||||
|
0xdaf473,
|
||||||
|
0xebff82,
|
||||||
|
0xfcff8e,
|
||||||
|
0x1b0701,
|
||||||
|
0x2c1801,
|
||||||
|
0x3c2900,
|
||||||
|
0x4d3b00,
|
||||||
|
0x5f4c00,
|
||||||
|
0x705e00,
|
||||||
|
0x816f00,
|
||||||
|
0x938009,
|
||||||
|
0xa4921a,
|
||||||
|
0xb2a02b,
|
||||||
|
0xc7b43d,
|
||||||
|
0xd8c64e,
|
||||||
|
0xead760,
|
||||||
|
0xf6e46f,
|
||||||
|
0xfffa84,
|
||||||
|
0xffff99,
|
||||||
|
].map((hex) => RGB.fromHex(hex));
|
||||||
|
|
||||||
|
function decimate(input, palette, n) {
|
||||||
|
// to brute-force, the possible palettes are:
|
||||||
|
// 255 * 254 * 253 = 16,386,810
|
||||||
|
//
|
||||||
|
// we could brute force it but that's a lot :D
|
||||||
|
// but can do some bisection :D
|
||||||
|
//
|
||||||
|
// need a fitness metric.
|
||||||
|
// each pixel in the dithered line gives a distance
|
||||||
|
// sum/average them? median? maximum?
|
||||||
|
// summing evens out the ups/downs from dithering
|
||||||
|
// but doesn't distinguish between two close and two distant options
|
||||||
|
// consider median, 90th-percentile, and max of abs(distance)
|
||||||
|
// consider doing the distance for each channel?
|
||||||
|
|
||||||
|
let line = input.slice();
|
||||||
|
|
||||||
|
// Apply dithering with given palette
|
||||||
|
let dither = (palette) => {
|
||||||
|
let fitness = 0;
|
||||||
|
let error = {
|
||||||
|
right: new RGB(0, 0, 0),
|
||||||
|
red: new Float64Array(line.length),
|
||||||
|
green: new Float64Array(line.length),
|
||||||
|
blue: new Float64Array(line.length),
|
||||||
|
}
|
||||||
|
|
||||||
|
let output = new Int32Array(line.length);
|
||||||
|
let popularity = new Int32Array(palette.length);
|
||||||
|
|
||||||
|
// Try dithering with this palette.
|
||||||
|
for (let x = 0; x < line.length; x++) {
|
||||||
|
let rgb = line[x];
|
||||||
|
rgb = rgb.difference(error.right);
|
||||||
|
|
||||||
|
// find the closest possible color
|
||||||
|
let shortest = Infinity;
|
||||||
|
let pick = -1;
|
||||||
|
|
||||||
|
for (let i = 0; i < palette.length; i++) {
|
||||||
|
let diff = palette[i].difference(rgb);
|
||||||
|
let dist = diff.magnitude();
|
||||||
|
if (dist < shortest) {
|
||||||
|
nextError = diff;
|
||||||
|
shortest = dist;
|
||||||
|
pick = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output[x] = pick;
|
||||||
|
popularity[pick]++;
|
||||||
|
|
||||||
|
error.right.r = nextError.r / 2;
|
||||||
|
error.right.g = nextError.g / 2;
|
||||||
|
error.right.b = nextError.b / 2;
|
||||||
|
|
||||||
|
if (x == 0) {
|
||||||
|
error.red[x - 1] += nextError.r / 8;
|
||||||
|
error.green[x - 1] += nextError.g / 8;
|
||||||
|
error.blue[x - 1] += nextError.b / 8;
|
||||||
|
} else {
|
||||||
|
error.red[x] += nextError.r / 8;
|
||||||
|
error.green[x] += nextError.g / 8;
|
||||||
|
error.blue[x] += nextError.b / 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
error.red[x] += nextError.r / 4;
|
||||||
|
error.green[x] += nextError.g / 4;
|
||||||
|
error.blue[x] += nextError.b / 4;
|
||||||
|
|
||||||
|
error.red[x + 1] += nextError.r / 8;
|
||||||
|
error.green[x + 1] += nextError.g / 8;
|
||||||
|
error.blue[x + 1] += nextError.b / 8;
|
||||||
|
|
||||||
|
//fitness += error.r;
|
||||||
|
//fitness += error.g;
|
||||||
|
//fitness += error.b;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
output,
|
||||||
|
palette,
|
||||||
|
fitness,
|
||||||
|
popularity,
|
||||||
|
error
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
let start = Date.now();
|
||||||
|
let decimated = palette.slice();
|
||||||
|
while (decimated.length > n) {
|
||||||
|
let {popularity} = dither(decimated);
|
||||||
|
|
||||||
|
// Try dropping least used color on each iteration
|
||||||
|
let least = Infinity;
|
||||||
|
let pick = -1;
|
||||||
|
for (let i = 1; i < decimated.length; i++) {
|
||||||
|
if (popularity[i] < least) {
|
||||||
|
pick = i;
|
||||||
|
least = popularity[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let old = decimated.length;
|
||||||
|
decimated = decimated.filter((rgb, i) => {
|
||||||
|
if (i == 0) {
|
||||||
|
return true; // keep black always
|
||||||
|
}
|
||||||
|
if (i == pick) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (popularity[i] == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
if (decimated.length >= old) {
|
||||||
|
throw new Error('logic error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let delta = Date.now() - start;
|
||||||
|
console.log(`${delta}ms for line`);
|
||||||
|
|
||||||
|
// Palette fits
|
||||||
|
return dither(decimated);
|
||||||
|
}
|
||||||
|
|
||||||
|
function convert(source, sink) {
|
||||||
|
|
||||||
|
let width = 320;
|
||||||
|
let height = 192;
|
||||||
|
|
||||||
|
let canvas = sink;
|
||||||
|
let ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
|
// Draw the source image down, then grab it
|
||||||
|
// and re-draw it with custom palette & dither.
|
||||||
|
|
||||||
|
ctx.drawImage(source, 0, 0);
|
||||||
|
|
||||||
|
let imageData = ctx.getImageData(0, 0, width, height);
|
||||||
|
let {data} = imageData;
|
||||||
|
let nextError;
|
||||||
|
|
||||||
|
for (let y = 0; y < height; y++) {
|
||||||
|
let line = new Uint8Array(data.buffer, y * width * 4, width * 4);
|
||||||
|
let input = [];
|
||||||
|
|
||||||
|
// Note we take two pixels because we're using the 160-wide 4-color mode
|
||||||
|
for (let x = 0; x < width; x += 2) {
|
||||||
|
let i = x >> 1;
|
||||||
|
let rgb = new RGB(
|
||||||
|
(line[x * 4 + 0] + line[x * 4 + 4]) / 2,
|
||||||
|
(line[x * 4 + 1] + line[x * 4 + 5]) / 2,
|
||||||
|
(line[x * 4 + 2] + line[x * 4 + 6]) / 2
|
||||||
|
);
|
||||||
|
if (nextError) {
|
||||||
|
rgb.r -= nextError.red[i];
|
||||||
|
rgb.g -= nextError.green[i];
|
||||||
|
rgb.b -= nextError.blue[i];
|
||||||
|
} else {
|
||||||
|
if (y > 0) {
|
||||||
|
debugger;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
input.push(rgb);
|
||||||
|
}
|
||||||
|
|
||||||
|
let {output, palette, error} = decimate(input, palette256, 4);
|
||||||
|
nextError = error;
|
||||||
|
|
||||||
|
for (let x = 0; x < width; x++) {
|
||||||
|
let rgb = palette[output[x >> 1]];
|
||||||
|
line[x * 4 + 0] = rgb.r;
|
||||||
|
line[x * 4 + 1] = rgb.g;
|
||||||
|
line[x * 4 + 2] = rgb.b;
|
||||||
|
line[x * 4 + 3] = 0xff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.putImageData(imageData, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function run() {
|
||||||
|
for (let i = 0; i < 7; i++) {
|
||||||
|
let source = document.querySelector('#source' + i);
|
||||||
|
let sink = document.querySelector('#sink' + i);
|
||||||
|
|
||||||
|
let doit = () => convert(source, sink);
|
||||||
|
|
||||||
|
if (source.complete) {
|
||||||
|
doit();
|
||||||
|
} else {
|
||||||
|
source.addEventListener('load', doit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
addEventListener('DOMContentLoaded', run);
|
||||||
|
} else {
|
||||||
|
run();
|
||||||
|
}
|
42
index.html
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Atari 4-color palette dither generator</title>
|
||||||
|
<style type="text/css">
|
||||||
|
body {
|
||||||
|
background-color: #4c5fd4;
|
||||||
|
color: #e5f8ff;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
img, canvas {
|
||||||
|
/*width: 640px;
|
||||||
|
height: 384px;*/
|
||||||
|
image-rendering: pixelated;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script async src=dither4.js></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<img id=source0 with=320 height=192 src=sample0.jpg>
|
||||||
|
<canvas id=sink0 width=320 height=192></canvas>
|
||||||
|
|
||||||
|
<img id=source1 with=320 height=192 src=sample1.jpg>
|
||||||
|
<canvas id=sink1 width=320 height=192></canvas>
|
||||||
|
|
||||||
|
<img id=source2 with=320 height=192 src=sample2.jpg>
|
||||||
|
<canvas id=sink2 width=320 height=192></canvas>
|
||||||
|
|
||||||
|
<img id=source3 with=320 height=192 src=sample3.jpg>
|
||||||
|
<canvas id=sink3 width=320 height=192></canvas>
|
||||||
|
|
||||||
|
<img id=source4 with=320 height=192 src=sample4.jpg>
|
||||||
|
<canvas id=sink4 width=320 height=192></canvas>
|
||||||
|
|
||||||
|
<img id=source5 with=320 height=192 src=sample5.jpg>
|
||||||
|
<canvas id=sink5 width=320 height=192></canvas>
|
||||||
|
|
||||||
|
<img id=source6 with=320 height=192 src=sample6.jpg>
|
||||||
|
<canvas id=sink6 width=320 height=192></canvas>
|
||||||
|
</body>
|
||||||
|
</html>
|
BIN
sample0.jpg
Normal file
After Width: | Height: | Size: 33 KiB |
BIN
sample1.jpg
Normal file
After Width: | Height: | Size: 28 KiB |
BIN
sample2.jpg
Normal file
After Width: | Height: | Size: 33 KiB |
BIN
sample3.jpg
Normal file
After Width: | Height: | Size: 32 KiB |
BIN
sample4.jpg
Normal file
After Width: | Height: | Size: 24 KiB |
BIN
sample5.jpg
Normal file
After Width: | Height: | Size: 34 KiB |
BIN
sample6.jpg
Normal file
After Width: | Height: | Size: 36 KiB |