From f5c8d219e86bdd7fbb9a82c5a6585784ec73a94c Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Thu, 23 Mar 2023 16:52:48 -0700 Subject: [PATCH 01/10] whee --- Makefile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Makefile b/Makefile index 18de03c..86d0922 100644 --- a/Makefile +++ b/Makefile @@ -20,4 +20,7 @@ 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 From 1d3712be5c89e3528d58da4d29514cbce7d9d030 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Fri, 24 Mar 2023 00:23:59 -0700 Subject: [PATCH 02/10] wip --- dither-image.js | 43 ++++++++++++++----------------------------- 1 file changed, 14 insertions(+), 29 deletions(-) diff --git a/dither-image.js b/dither-image.js index f804df8..ee313d0 100644 --- a/dither-image.js +++ b/dither-image.js @@ -134,13 +134,23 @@ 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.r * 0.299 + this.g * 0.587 + this.b * 0.114; + return this.lumaScale().sum(); } } -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 = [ @@ -414,19 +424,6 @@ let atariRGB = [ * @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; @@ -440,7 +437,6 @@ 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: [], @@ -467,7 +463,7 @@ function decimate(input, palette, n) { for (let i = 0; i < palette.length; i++) { let diff = rgb.difference(atariRGB[palette[i]]); - let dist = diff.magnitude2(); + let dist = diff.magnitude(); if (dist < shortest) { nextError = diff; shortest = dist; @@ -484,21 +480,10 @@ 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 From fe6314e2a0852959172a0d1ae7f1ccb79c4f16a5 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Fri, 24 Mar 2023 00:28:07 -0700 Subject: [PATCH 03/10] just use srgb for consistency with the pngs --- dither-image.js | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/dither-image.js b/dither-image.js index ee313d0..2c9e252 100644 --- a/dither-image.js +++ b/dither-image.js @@ -13,6 +13,7 @@ function zeroes(n) { return arr; } +/* function toLinear(val) { // use a 2.4 gamma approximation // this is BT.1886 compatible @@ -27,6 +28,18 @@ function fromLinear(val) { unit **= (1 / 2.4); 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; @@ -65,6 +78,7 @@ class RGB { ); } + /* toLinear() { return this.map(toLinear); } @@ -72,6 +86,11 @@ class RGB { fromLinear() { return this.map(fromLinear); } + */ + + fromSRGB() { + return this.map(fromSRGB); + } toSRGB() { return this.map(toSRGB); @@ -410,7 +429,7 @@ let atariRGB = [ 0xf6e46f, 0xfffa84, 0xffff99, -].map((hex) => RGB.fromHex(hex).toLinear()); +].map((hex) => RGB.fromHex(hex).fromSRGB()); //].map((hex) => RGB.fromHex(hex)); /** @@ -667,7 +686,7 @@ function imageToLinearRGB(rgba) { rgba[i + 0], rgba[i + 1], rgba[i + 2] - ).toLinear()); + ).fromSRGB()); } return input; } From 325102021baa0da6ea19ff6ce0d835157db5d8be Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Fri, 24 Mar 2023 00:30:25 -0700 Subject: [PATCH 04/10] nice --- dither-image.js | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/dither-image.js b/dither-image.js index 2c9e252..ebe7faf 100644 --- a/dither-image.js +++ b/dither-image.js @@ -13,7 +13,6 @@ function zeroes(n) { return arr; } -/* function toLinear(val) { // use a 2.4 gamma approximation // this is BT.1886 compatible @@ -28,7 +27,6 @@ function fromLinear(val) { unit **= (1 / 2.4); return unit * 255; } -*/ function fromSRGB(val) { val /= 255; @@ -78,15 +76,13 @@ class RGB { ); } - /* - toLinear() { + fromNTSC() { return this.map(toLinear); } - fromLinear() { + toNTSC() { return this.map(fromLinear); } - */ fromSRGB() { return this.map(fromSRGB); @@ -429,7 +425,7 @@ let atariRGB = [ 0xf6e46f, 0xfffa84, 0xffff99, -].map((hex) => RGB.fromHex(hex).fromSRGB()); +].map((hex) => RGB.fromHex(hex).fromNTSC()); //].map((hex) => RGB.fromHex(hex)); /** From 1d3908410bdf8f87cba7501c4b022bc901ebd535 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Fri, 24 Mar 2023 01:18:49 -0700 Subject: [PATCH 05/10] using the 'pal' colors is closer? why???? --- dither-image.js | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/dither-image.js b/dither-image.js index ebe7faf..1bf07af 100644 --- a/dither-image.js +++ b/dither-image.js @@ -68,6 +68,39 @@ 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), @@ -166,6 +199,7 @@ class RGB { } } +/* // snarfed from https://lospec.com/palette-list/atari-8-bit-family-gtia // which was calculated with Retrospecs App's Atari 800 emulator let atariRGB = [ @@ -427,6 +461,14 @@ let atariRGB = [ 0xffff99, ].map((hex) => RGB.fromHex(hex).fromNTSC()); //].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. From 527ef1ef05d09afe9da9c2153d329014983fb8e8 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Fri, 24 Mar 2023 03:08:40 -0700 Subject: [PATCH 06/10] bwahahah --- video/combine.sh | 9 +++++++++ video/extract.sh | 17 +++++++++++++++++ video/video.sh | 15 +++++++++++++++ 3 files changed, 41 insertions(+) create mode 100644 video/combine.sh create mode 100644 video/extract.sh create mode 100644 video/video.sh diff --git a/video/combine.sh b/video/combine.sh new file mode 100644 index 0000000..75e7117 --- /dev/null +++ b/video/combine.sh @@ -0,0 +1,9 @@ +ffmpeg \ + -r 60000/1001 \ + -i 'frames/dither-%04d.png' \ + -i 'colamath-audio.wav' \ + -ac 2 \ + -ar 48000 \ + -vf 'pad=w=534' \ + -pix_fmt yuv420p \ + -y colamath-dither.mp4 diff --git a/video/extract.sh b/video/extract.sh new file mode 100644 index 0000000..e626e3c --- /dev/null +++ b/video/extract.sh @@ -0,0 +1,17 @@ +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 new file mode 100644 index 0000000..3a21da7 --- /dev/null +++ b/video/video.sh @@ -0,0 +1,15 @@ +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 From 363cf21ba5b8c4b5fef3dcc6a89f9530c83b5a5a Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Fri, 24 Mar 2023 03:16:01 -0700 Subject: [PATCH 07/10] woo --- video/combine.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/video/combine.sh b/video/combine.sh index 75e7117..cc81874 100644 --- a/video/combine.sh +++ b/video/combine.sh @@ -4,6 +4,6 @@ ffmpeg \ -i 'colamath-audio.wav' \ -ac 2 \ -ar 48000 \ - -vf 'pad=w=534' \ + -vf 'pad=w=640:h=480:x=52:y=80' \ -pix_fmt yuv420p \ -y colamath-dither.mp4 From 37b06789b06ba9697447129f50b26f734a14e3d6 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Fri, 24 Mar 2023 12:52:22 -0700 Subject: [PATCH 08/10] cleaner handling of reserved colors --- dither-image.js | 6 +++++- video/combine.sh | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/dither-image.js b/dither-image.js index 1bf07af..422686c 100644 --- a/dither-image.js +++ b/dither-image.js @@ -580,7 +580,11 @@ function decimate(input, palette, n) { // Median cut! // https://en.wikipedia.org/wiki/Median_cut - let buckets = [input.slice()]; + //let buckets = [input.slice()]; + + // preface the reserved bits + let buckets = reserved.slice().map((c) => [atariRGB[c]]).concat([input.slice()]); + let medianCut = (bucket, range) => { if (bucket.length < 2) { console.log(bucket); diff --git a/video/combine.sh b/video/combine.sh index cc81874..e56e0e5 100644 --- a/video/combine.sh +++ b/video/combine.sh @@ -4,6 +4,6 @@ ffmpeg \ -i 'colamath-audio.wav' \ -ac 2 \ -ar 48000 \ - -vf 'pad=w=640:h=480:x=52:y=80' \ + -vf 'pad=w=640:h=360:x=52:y=20' \ -pix_fmt yuv420p \ -y colamath-dither.mp4 From 0c8ca1380da7f81edf9c1e53f9486f5a5d4d43e8 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Fri, 24 Mar 2023 13:11:56 -0700 Subject: [PATCH 09/10] wip retooling some bits better handling of reserved color --- dither-image.js | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/dither-image.js b/dither-image.js index 422686c..618b884 100644 --- a/dither-image.js +++ b/dither-image.js @@ -617,8 +617,23 @@ 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 = 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 [lo, hi] = medianCut(buckets[index], ranges[index]); buckets.splice(index, 1, lo, hi); } From 71c19887c7e35edae203e7003e4131ed6ea91081 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Fri, 24 Mar 2023 13:19:31 -0700 Subject: [PATCH 10/10] wip --- dither-image.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dither-image.js b/dither-image.js index 618b884..1c00068 100644 --- a/dither-image.js +++ b/dither-image.js @@ -665,9 +665,11 @@ 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