dither4/dither4.js

653 lines
14 KiB
JavaScript
Raw Normal View History

2022-11-06 02:38:31 +00:00
function toLinear(val) {
// use a 2.4 gamma approximation
// this is BT.1886 compatible
// and simpler than sRGB
2022-11-06 02:34:31 +00:00
let unit = val / 255;
2022-11-06 02:38:31 +00:00
unit **= 2.4;
2022-11-06 02:34:31 +00:00
return unit * 255;
}
2022-11-06 02:38:31 +00:00
function fromLinear(val) {
2022-11-06 02:34:31 +00:00
let unit = val / 255;
2022-11-06 02:38:31 +00:00
unit **= (1 / 2.4);
2022-11-06 02:34:31 +00:00
return unit * 255;
}
2022-11-06 00:57:34 +00:00
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);
}
2022-11-06 02:38:31 +00:00
toLinear() {
2022-11-06 02:34:31 +00:00
return new RGB(
2022-11-06 02:38:31 +00:00
toLinear(this.r),
toLinear(this.g),
toLinear(this.b)
2022-11-06 02:34:31 +00:00
);
}
2022-11-06 02:38:31 +00:00
fromLinear() {
2022-11-06 02:34:31 +00:00
return new RGB(
2022-11-06 02:38:31 +00:00
fromLinear(this.r),
fromLinear(this.g),
fromLinear(this.b)
2022-11-06 02:34:31 +00:00
);
}
2022-11-06 02:22:59 +00:00
cap() {
if (this.r < 0) {
this.r = 0;
}
if (this.g < 0) {
this.g = 0;
}
if (this.b < 0) {
this.b = 0;
}
if (this.r > 255) {
this.r = 255;
}
if (this.g > 255) {
this.g = 255;
}
if (this.b > 255) {
this.b = 255;
}
}
2022-11-06 00:57:34 +00:00
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,
2022-11-06 02:38:31 +00:00
].map((hex) => RGB.fromHex(hex).toLinear());
2022-11-06 00:57:34 +00:00
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) => {
2022-11-06 01:24:01 +00:00
let fitness = new Float64Array(line.length);
2022-11-06 00:57:34 +00:00
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];
2022-11-06 02:22:59 +00:00
rgb = rgb.add(error.right);
//rgb.cap();
2022-11-06 00:57:34 +00:00
// find the closest possible color
let shortest = Infinity;
let pick = 1;
2022-11-06 00:57:34 +00:00
for (let i = 0; i < palette.length; i++) {
2022-11-06 02:22:59 +00:00
let diff = rgb.difference(palette[i]);
2022-11-06 00:57:34 +00:00
let dist = diff.magnitude();
let darker = Math.min(diff.r, diff.g, diff.b) < 0;
if (darker) {
dist **= 2;
}
2022-11-06 00:57:34 +00:00
if (dist < shortest) {
nextError = diff;
shortest = dist;
pick = i;
}
}
output[x] = pick;
popularity[pick]++;
2022-11-06 01:24:01 +00:00
/*
// horiz only
error.right.r = nextError.r;
error.right.g = nextError.g;
error.right.b = nextError.b;
*/
2022-11-06 02:22:59 +00:00
/*
2022-11-06 03:08:47 +00:00
error.red[x] += nextError.r;
error.green[x] += nextError.g;
error.blue[x] += nextError.b;
*/
2022-11-06 03:22:04 +00:00
/*
2022-11-06 00:57:34 +00:00
error.right.r = nextError.r / 2;
error.right.g = nextError.g / 2;
error.right.b = nextError.b / 2;
2022-11-06 03:08:47 +00:00
error.red[x] += nextError.r / 2;
error.green[x] += nextError.g / 2;
error.blue[x] += nextError.b / 2;
2022-11-06 03:22:04 +00:00
*/
2022-11-06 02:22:59 +00:00
error.right.r = nextError.r / 2;
error.right.g = nextError.g / 2;
error.right.b = nextError.b / 2;
2022-11-21 01:28:11 +00:00
if (x == 159) {
error.red[0] = error.right.r;
error.green[0] = error.right.g;
error.blue[0] = error.right.b;
}
2022-11-06 02:22:59 +00:00
error.red[x - 1] += nextError.r / 8;
error.green[x - 1] += nextError.g / 8;
error.blue[x - 1] += nextError.b / 8;
2022-11-06 00:57:34 +00:00
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;
2022-11-06 03:08:47 +00:00
/*
2022-11-06 03:08:47 +00:00
error.right.r = nextError.r / 4;
error.right.g = nextError.g / 4;
error.right.b = nextError.b / 4;
error.red[x - 1] += nextError.r / 4;
error.green[x - 1] += nextError.g / 4;
error.blue[x - 1] += nextError.b / 4;
error.red[x] += nextError.r / 4;
error.green[x] += nextError.g / 4;
error.blue[x] += nextError.b / 4;
error.red[x + 1] += nextError.r / 4;
error.green[x + 1] += nextError.g / 4;
error.blue[x + 1] += nextError.b / 4;
*/
2022-11-06 00:57:34 +00:00
2022-11-06 03:08:47 +00:00
// 442 is the 3d distance across the rgb cube
//fitness[x] = 442 - (nextError.magnitude());
//fitness[x] = 442 / (442 - nextError.magnitude());
fitness[x] = 255 / (256 - Math.max(nextError.r, nextError.g, nextError.b));
/*
fitness[x] = Math.max(
255 - Math.abs(nextError.r),
255 - Math.abs(nextError.g),
255 - Math.abs(nextError.b),
);
*/
2022-11-06 00:57:34 +00:00
}
return {
output,
palette,
fitness,
popularity,
error
};
};
// black, red, blue, white
let rbw = [
palette256[0x00],
palette256[0x87],
palette256[0xf7],
palette256[0x0f],
];
let rgb = [
palette256[0x00],
palette256[0x87],
palette256[0xc7],
palette256[0xf7],
];
// grayscale
let gray = [
palette256[0x00],
palette256[0x05],
palette256[0x0a],
palette256[0x0f],
];
//palette = rgb;
//palette = rbw;
//palette = gray;
2022-11-06 00:57:34 +00:00
let start = Date.now();
let decimated = palette.slice();
2022-11-06 00:57:34 +00:00
while (decimated.length > n) {
2022-11-06 01:24:01 +00:00
let {popularity, fitness, output} = dither(decimated);
2022-11-06 00:57:34 +00:00
// Try dropping least used color on each iteration
let least = Infinity;
let pick = -1;
for (let i = 1; i < decimated.length; i++) {
if (i == 0) {
continue; // keep black always
}
if (decimated[i].r == 255 && decimated[i].g == 255 && decimated[i].b == 255) {
//continue; // keep white always
2022-11-06 01:24:01 +00:00
}
2022-11-06 03:08:47 +00:00
//let coolFactor = popularity[i];
2022-11-06 03:08:47 +00:00
let coolFactor = 0;
2022-11-06 01:24:01 +00:00
for (let x = 0; x < line.length; x++) {
if (output[x] == i) {
2022-11-21 01:28:11 +00:00
coolFactor += (fitness[x] ** 2);
2022-11-06 01:24:01 +00:00
}
}
2022-11-06 03:08:47 +00:00
2022-11-06 01:24:01 +00:00
if (coolFactor < least) {
2022-11-06 00:57:34 +00:00
pick = i;
2022-11-06 01:24:01 +00:00
least = coolFactor;
2022-11-06 00:57:34 +00:00
}
}
let old = decimated.length;
decimated = decimated.filter((rgb, i) => {
if (i == pick) {
return false;
}
if (popularity[i] == 0) {
return false;
}
return true;
});
if (decimated.length >= old) {
console.log(decimated);
debugger;
2022-11-06 00:57:34 +00:00
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
2022-11-06 02:38:31 +00:00
).toLinear();
2022-11-06 00:57:34 +00:00
if (nextError) {
2022-11-06 02:22:59 +00:00
rgb.r += nextError.red[i];
rgb.g += nextError.green[i];
rgb.b += nextError.blue[i];
2022-11-21 01:28:11 +00:00
//rgb.cap();
2022-11-06 00:57:34 +00:00
}
input.push(rgb);
}
let {output, palette, error} = decimate(input, palette256, 4);
nextError = error;
for (let x = 0; x < width; x++) {
2022-11-06 02:38:31 +00:00
let rgb = palette[output[x >> 1]].fromLinear();
2022-11-06 00:57:34 +00:00
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();
}