Compare commits

..

No commits in common. "71c19887c7e35edae203e7003e4131ed6ea91081" and "0dcfa88c2991c25cb6d3eec315f071a6739a95bc" have entirely different histories.

5 changed files with 36 additions and 143 deletions

View file

@ -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

View file

@ -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;
}

View file

@ -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

View file

@ -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'

View file

@ -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