diff --git a/Makefile b/Makefile index 86d0922..18de03c 100644 --- a/Makefile +++ b/Makefile @@ -20,7 +20,4 @@ all : sample0.xex \ clean : rm -f *.o rm -f *.s.png - rm -f sample[0-9].s - rm -f fruit.s mapclock.s sailboat.s sunset.s train404.s - rm -f potato.s selfie.s kitty.s meme.s rm -f *.xex diff --git a/dither-image.js b/dither-image.js index 1c00068..f804df8 100644 --- a/dither-image.js +++ b/dither-image.js @@ -28,17 +28,6 @@ function fromLinear(val) { return unit * 255; } -function fromSRGB(val) { - val /= 255; - if (val <= 0.04045) { - val /= 12.92; - } else { - val = ((val + 0.055) / 1.055) ** 2.4; - } - val *= 255; - return val; -} - function toSRGB(val) { val /= 255; if (val <= 0.0031308) { @@ -68,39 +57,6 @@ class RGB { return new RGB(r,g,b); } - static fromGTIA(val) { - // This seems off from what Atari800 does - // https://forums.atariage.com/topic/107853-need-the-256-colors/page/2/#comment-1312467 - let cr = (val >> 4) & 15; - let lm = val & 15; - let crlv = cr ? 50 : 0; - - /* - let phase = ((cr - 1) * 25 - 58) * (2 * Math.PI / 360); - - let y = 255 * (lm + 1) / 16; - let i = crlv * Math.cos(phase); - let q = crlv * Math.sin(phase); - - let r = y + 0.956 * i + 0.621 * q; - let g = y - 0.272 * i - 0.647 * q; - let b = y - 1.107 * i + 1.704 * q; - */ - - // PAL - let phase = ((cr - 1) * 25.7 - 15) * (2 * Math.PI / 360); - - let y = 255 * (lm + 1) / 16; - let i = crlv * Math.cos(phase); - let q = crlv * Math.sin(phase); - - let r = y + 0.956 * i + 0.621 * q; - let g = y - 0.272 * i - 0.647 * q; - let b = y - 1.107 * i + 1.704 * q; - - return new RGB(r, g, b).clamp().fromSRGB(); - } - map(callback) { return new RGB( callback(this.r), @@ -109,18 +65,14 @@ class RGB { ); } - fromNTSC() { + toLinear() { return this.map(toLinear); } - toNTSC() { + fromLinear() { return this.map(fromLinear); } - fromSRGB() { - return this.map(fromSRGB); - } - toSRGB() { return this.map(toSRGB); } @@ -182,24 +134,13 @@ class RGB { this.b * this.b; } - sum() { - return this.r + this.g + this.b; - } - - lumaScale() { - return new RGB( - this.r * 0.299, - this.g * 0.586, - this.b * 0.114 - ); - } - luma() { - return this.lumaScale().sum(); + return this.r * 0.299 + this.g * 0.587 + this.b * 0.114; } } -/* +const maxDist = (new RGB(255, 255, 255)).magnitude(); + // snarfed from https://lospec.com/palette-list/atari-8-bit-family-gtia // which was calculated with Retrospecs App's Atari 800 emulator let atariRGB = [ @@ -459,16 +400,8 @@ let atariRGB = [ 0xf6e46f, 0xfffa84, 0xffff99, -].map((hex) => RGB.fromHex(hex).fromNTSC()); +].map((hex) => RGB.fromHex(hex).toLinear()); //].map((hex) => RGB.fromHex(hex)); -*/ - -let atariRGB = []; -for (let i = 0; i < 256; i++) { - atariRGB[i] = RGB.fromGTIA(i); -} - - /** * Dither RGB input data with a target palette size. @@ -481,6 +414,19 @@ for (let i = 0; i < 256; i++) { * @returns {{output: number[], palette: number[], error: RGB[]}} */ 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 width = input.length; @@ -494,6 +440,7 @@ function decimate(input, palette, n) { // Apply dithering with given palette and collect color usage stats let dither = (palette) => { + let fitness = zeroes(width); let error = { cur: [], next: [], @@ -520,7 +467,7 @@ function decimate(input, palette, n) { for (let i = 0; i < palette.length; i++) { let diff = rgb.difference(atariRGB[palette[i]]); - let dist = diff.magnitude(); + let dist = diff.magnitude2(); if (dist < shortest) { nextError = diff; shortest = dist; @@ -537,10 +484,21 @@ function decimate(input, palette, n) { 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; + // 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(0, nextError.r, nextError.g, nextError.b)); + + let mag2 = nextError.magnitude2(); + distance2 += mag2; } return { output, palette, + fitness, distance2, popularity, error: error.next @@ -580,11 +538,7 @@ function decimate(input, palette, n) { // Median cut! // https://en.wikipedia.org/wiki/Median_cut - //let buckets = [input.slice()]; - - // preface the reserved bits - let buckets = reserved.slice().map((c) => [atariRGB[c]]).concat([input.slice()]); - + let buckets = [input.slice()]; let medianCut = (bucket, range) => { if (bucket.length < 2) { console.log(bucket); @@ -617,23 +571,8 @@ function decimate(input, palette, n) { ); }); let topRanges = ranges.map((rgb) => Math.max(rgb.r, rgb.g, rgb.b)); - //let greatest = Math.max(...topRanges); - //let index = topRanges.indexOf(greatest); - let greatest = 0; - let index = -1; - for (let i = 0; i < topRanges.length; i++) { - if (topRanges[i] >= greatest) { - greatest = topRanges[i]; - index = i; - } - } - if (index == -1) { - // We just ran out of colors! Pad the buckets. - while (buckets.length < n) { - buckets.push([new RGB(0, 0, 0)]); - } - break; - } + let greatest = Math.max(...topRanges); + let index = topRanges.indexOf(greatest); let [lo, hi] = medianCut(buckets[index], ranges[index]); buckets.splice(index, 1, lo, hi); } @@ -665,11 +604,9 @@ function decimate(input, palette, n) { //let rgb = bucket[bucket.length - 1]; // Take the luma-brightest color in the bucket - // wrong? bad //let rgb = bucket.slice().sort((a, b) => b.luma() - a.luma())[bucket.length - 1]; // Take the median color in the bucket - // bad //let rgb = bucket[bucket.length >> 1]; // Combine the brightest of each channel @@ -745,7 +682,7 @@ function imageToLinearRGB(rgba) { rgba[i + 0], rgba[i + 1], rgba[i + 2] - ).fromSRGB()); + ).toLinear()); } return input; } diff --git a/video/combine.sh b/video/combine.sh deleted file mode 100644 index e56e0e5..0000000 --- a/video/combine.sh +++ /dev/null @@ -1,9 +0,0 @@ -ffmpeg \ - -r 60000/1001 \ - -i 'frames/dither-%04d.png' \ - -i 'colamath-audio.wav' \ - -ac 2 \ - -ar 48000 \ - -vf 'pad=w=640:h=360:x=52:y=20' \ - -pix_fmt yuv420p \ - -y colamath-dither.mp4 diff --git a/video/extract.sh b/video/extract.sh deleted file mode 100644 index e626e3c..0000000 --- a/video/extract.sh +++ /dev/null @@ -1,17 +0,0 @@ -set -a - -mkdir -p frames - -ffmpeg \ - -i colamath-dv.avi \ - -vf 'yadif=1,scale=160:200,crop=h=160' \ - -an \ - -y 'frames/colamath-%04d.png' - -ffmpeg \ - -i colamath-dv.avi \ - -vn \ - -ac 1 \ - -ar 15734 \ - -acodec pcm_u8 \ - -y 'colamath-audio.wav' diff --git a/video/video.sh b/video/video.sh deleted file mode 100644 index 3a21da7..0000000 --- a/video/video.sh +++ /dev/null @@ -1,15 +0,0 @@ -set -e - -for frame in frames/colamath-[0-9][0-9][0-9][0-9].png -do - n="${frame#frames/colamath-}" - n="${n%.png}" - out="frames/dither-${n}" - last="${n:0-1}" - node ../dither-image.js "$frame" "$out" & - if (( last == 9 )) - then - wait - fi -done -wait