diff --git a/dither-image.js b/dither-image.js index e2927dd..49d7ef4 100644 --- a/dither-image.js +++ b/dither-image.js @@ -98,10 +98,6 @@ class RGB { ); } - add(other) { - return RGB.add(this, other); - } - difference(other) { return new RGB( this.r - other.r, @@ -110,6 +106,14 @@ class RGB { ); } + multiply(scalar) { + return new RGB( + this.r * scalar, + this.g * scalar, + this.b * scalar, + ); + } + divide(scalar) { return new RGB( this.r / scalar, @@ -472,15 +476,12 @@ function decimate(input, palette, n, inputError, y) { output[x] = pick; popularity[pick]++; - let shares = 8; - let single = nextError.divide(shares); - let double = nextError.divide(shares / 2); - error.cur[x + 1]?.inc(double); - error.cur[x + 2]?.inc(single); + let share = (n) => nextError.multiply(n / 16); - error.next[x - 1]?.inc(single); - error.next[x]?.inc(double); - error.next[x + 1]?.inc(double); + error.cur[x + 1]?.inc(share(7)); + error.next[x - 1]?.inc(share(3)); + error.next[x ]?.inc(share(5)); + error.next[x + 1]?.inc(share(1)); let mag = nextError.magnitude(); fitness[x] = maxDist / mag; @@ -533,129 +534,6 @@ function decimate(input, palette, n, inputError, y) { keepers[i & 0xfe] = 1; // drop that 0 luminance bit! } - // this takes the top hues, and uses the brightest of each hue - // needs tuning - /* - // first, dither to the total atari palette - while (decimated.length > n) { - let {popularity} = dither(decimated); - let hues = zeros(16); - let lumas = zeros(16); - for (let i = 0; i < decimated.length; i++) { - let color = decimated[i]; - let hue = color >> 4; - let luma = color & 0xe; - hues[hue] += popularity[i]; - if (luma > lumas[hue]) { - lumas[hue] = luma; - } - } - - let a = hues; - a = a.map((count, hue) => { return {count, hue} }); - a = a.sort((a, b) => b.count - a.count); - a = a.map(({hue}) => hue); - a = a.filter((color) => !keepers[color]); - //a = a.slice(0, n - reserved.length); - a = a.slice(0, decimated.length - 1 - reserved.length); - a = a.map((hue) => (hue << 4) | lumas[hue]); - a = a.sort((a, b) => a - b); - - decimated = reserved.concat(a); - } - console.log('end', decimated); - */ - - // popularity? not really working right - // first, dither to the total atari palette - /* - while (decimated.length > n) { - //console.log(y); - - let {popularity, fitness, output} = dither(decimated); - let pops = []; - let fits = []; - pops.fill(0, 0, 256); - fits.fill(0, 0, 256); - for (let i = 0; i < decimated.length; i++) { - let c = decimated[i]; - pops[c] = popularity[i]; - for (let x = 0; x < fitness.length; x++) { - if (output[x] === i) { - fits[c] += fitness[x]; - } - } - } - let metric = (c) => { - let rgb = atariRGB[c]; - let max = Math.max(rgb.r, rgb.g, rgb.b); - let fit = fits[c]; - let pop = pops[c]; - return pop; - } - - let a = decimated.slice(); - // temporarily strip the reserved items - a = a.filter((color) => !keepers[color]); - a = a.filter((color) => popularity[color]); - a.sort((a, b) => { - return metric(b) - metric(a); - }); - console.log(a); - a = reserved.concat(a); - decimated = a.slice(0, n); - //decimated = a.slice(0, decimated.length - 1); - //console.log(decimated); - } - */ - //console.log('end', decimated); - - // old algo - /* - while (decimated.length > n) { - let {popularity, fitness, output} = dither(decimated); - - // Try dropping least used color on each iteration - let least = Infinity; - let pick = -1; - for (let i = 1; i < decimated.length; i++) { - - //let coolFactor = popularity[i]; - - let coolFactor = 0; - if (popularity[i]) { - for (let x = 0; x < width; x++) { - if (output[x] == i) { - // Scale up the scoring for close matches to prioritize - // color accuracy over raw linear usage. - //coolFactor += (fitness[x] ** 2); - coolFactor += (fitness[x] ** 4); - } - } - } - if (coolFactor < least) { - pick = i; - least = coolFactor; - } - decimated = decimated.filter((color, i) => { - if (i == 0) { - return true; - } - if (i == pick) { - return false; - } - // Also drop any non-black unused colors to save trouble. - // However -- this may change dither results. - // Try this with/without later. - if (popularity[i] == 0) { - return false; - } - return true; - }); - } - } - */ - // Median cut! // https://en.wikipedia.org/wiki/Median_cut //let buckets = [input.slice()]; @@ -694,13 +572,16 @@ function decimate(input, palette, n, inputError, y) { } decimated = buckets.map((bucket) => { // Average the RGB colors in this chunk - let rgb = bucket - .reduce((acc, rgb) => acc.inc(rgb), new RGB(0, 0, 0)) - .divide(bucket.length); + //let rgb = bucket + // .reduce((acc, rgb) => acc.inc(rgb), new RGB(0, 0, 0)) + // .divide(bucket.length); // Take the brightest color in the bucket //let rgb = bucket[bucket.length - 1]; + // Take the median color in the bucket + let rgb = bucket[bucket.length >> 1]; + // And map into the Atari palette let dists = palette.map(( i) => rgb.difference(atariRGB[i]).magnitude()); let closest = Math.min(...dists);