wip convert to grayscale/single-hue mode
doesn't seem to be hitting the right palette register
This commit is contained in:
parent
77606d464e
commit
2b17930f0f
2 changed files with 42 additions and 551 deletions
571
dither-image.js
571
dither-image.js
|
@ -203,270 +203,6 @@ 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 = [
|
|
||||||
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,
|
|
||||||
].map((hex) => RGB.fromHex(hex).fromNTSC());
|
|
||||||
//].map((hex) => RGB.fromHex(hex));
|
|
||||||
*/
|
|
||||||
|
|
||||||
let atariRGB = [];
|
let atariRGB = [];
|
||||||
for (let i = 0; i < 256; i++) {
|
for (let i = 0; i < 256; i++) {
|
||||||
atariRGB[i] = RGB.fromGTIA(i);
|
atariRGB[i] = RGB.fromGTIA(i);
|
||||||
|
@ -475,17 +211,12 @@ for (let i = 0; i < 256; i++) {
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dither RGB input data with a target palette size.
|
* Dither RGB input data and pick a monochrome palette.
|
||||||
* If the number of used colors exceeds `n`, the
|
|
||||||
* palette will be reduced until it fits.
|
|
||||||
* @param {RGB[]} input source scanline data, in linear RGB
|
* @param {RGB[]} input source scanline data, in linear RGB
|
||||||
* @param {number[]} palette - current working palette, as Atari 8-bit color values (low nybble luminance, high nybble hue)
|
|
||||||
* @param {number} n - target color count
|
|
||||||
* @param {number} y
|
|
||||||
* @returns {{output: number[], palette: number[], error: RGB[]}}
|
* @returns {{output: number[], palette: number[], error: RGB[]}}
|
||||||
*/
|
*/
|
||||||
function decimate(input, palette, n) {
|
function colorize(input) {
|
||||||
|
|
||||||
let width = input.length;
|
let width = input.length;
|
||||||
|
|
||||||
let inputPixel = (x, error) => {
|
let inputPixel = (x, error) => {
|
||||||
|
@ -508,8 +239,7 @@ function decimate(input, palette, n) {
|
||||||
}
|
}
|
||||||
|
|
||||||
let output = zeroes(width);
|
let output = zeroes(width);
|
||||||
let popularity = zeroes(palette.length);
|
let distance = 0;
|
||||||
let distance2 = 0;
|
|
||||||
|
|
||||||
let nextError = new RGB(0, 0, 0);
|
let nextError = new RGB(0, 0, 0);
|
||||||
|
|
||||||
|
@ -533,7 +263,7 @@ function decimate(input, palette, n) {
|
||||||
}
|
}
|
||||||
|
|
||||||
output[x] = pick;
|
output[x] = pick;
|
||||||
popularity[pick]++;
|
distance += shortest;
|
||||||
|
|
||||||
let share = (n) => nextError.multiply(n / 16);
|
let share = (n) => nextError.multiply(n / 16);
|
||||||
|
|
||||||
|
@ -545,228 +275,23 @@ function decimate(input, palette, n) {
|
||||||
return {
|
return {
|
||||||
output,
|
output,
|
||||||
palette,
|
palette,
|
||||||
distance2,
|
distance,
|
||||||
popularity,
|
|
||||||
error: error.next
|
error: error.next
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let best = null;
|
||||||
let decimated = palette.slice();
|
for (let hue = 0; hue < 16; hue++) {
|
||||||
|
let palette = [];
|
||||||
// force to grayscale
|
for (let i = 0; i < 16; i++) {
|
||||||
//decimated = [0, 5, 10, 15];
|
palette[i] = (hue << 4) | i;
|
||||||
|
}
|
||||||
// force to rgb
|
let variant = dither(palette);
|
||||||
//decimated = [0, 0x36, 0xb6, 0x86];
|
if (!best || variant.distance < best.distance) {
|
||||||
|
best = variant;
|
||||||
// force to rWb
|
}
|
||||||
//decimated = [0, 0x36, 0x0f, 0x86];
|
|
||||||
|
|
||||||
let reserved = [0]; // black
|
|
||||||
//reserved = [0, 15]; // black, white
|
|
||||||
//reserved = [0, 5, 10, 15]; // grayscale
|
|
||||||
//reserved = [0, 0x48, 0x78, 15]; // vaporwave
|
|
||||||
//reserved = [0, 0x3c, 0x78, 15]; // red/blue/white
|
|
||||||
|
|
||||||
/*
|
|
||||||
if (( y & 1 ) === 0) {
|
|
||||||
reserved = [0, 0x3c, 0x1e, 15]; // red/yellow/white
|
|
||||||
} else {
|
|
||||||
reserved = [0, 0x76, 0x9e, 0xb8]; // blue/cyan/green
|
|
||||||
}
|
}
|
||||||
*/
|
return best;
|
||||||
|
|
||||||
let keepers = zeroes(256);
|
|
||||||
for (let i of reserved) {
|
|
||||||
keepers[i & 0xfe] = 1; // drop that 0 luminance bit!
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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()]);
|
|
||||||
if (input.length != 160) {
|
|
||||||
throw new Error('xxx bad input size');
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
let buckets = [input.slice()];
|
|
||||||
if (reserved.length > 0) {
|
|
||||||
let pxPerReserved = input.length;
|
|
||||||
for (let c of reserved) {
|
|
||||||
for (let i = 0; i < pxPerReserved; i++) {
|
|
||||||
buckets[0].unshift(atariRGB[c]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
console.log(buckets[0].length, 'xxx');
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
let magicSort = (picker) => (a, b) => {
|
|
||||||
let bychannel = picker(b) - picker(a);
|
|
||||||
if (bychannel) return bychannel;
|
|
||||||
|
|
||||||
let byluma = b.luma() - a.luma();
|
|
||||||
return byluma;
|
|
||||||
};
|
|
||||||
let medianCut = (bucket, range) => {
|
|
||||||
if (bucket.length < 2) {
|
|
||||||
throw new Error('short bucket');
|
|
||||||
}
|
|
||||||
//console.log('medianCut', bucket, range);
|
|
||||||
// Sort by the channel with the greatest range,
|
|
||||||
// then cut the bucket in two at the median.
|
|
||||||
if (range.g >= range.r && range.g >= range.b) {
|
|
||||||
//bucket.sort((a, b) => b.g - a.g);
|
|
||||||
bucket.sort(magicSort((rgb) => rgb.g));
|
|
||||||
} else if (range.r >= range.g && range.r >= range.b) {
|
|
||||||
//bucket.sort((a, b) => b.r - a.r);
|
|
||||||
bucket.sort(magicSort((rgb) => rgb.r));
|
|
||||||
} else if (range.b >= range.g && range.b >= range.r) {
|
|
||||||
//bucket.sort((a, b) => b.b - a.b);
|
|
||||||
bucket.sort(magicSort((rgb) => rgb.b));
|
|
||||||
}
|
|
||||||
let half = bucket.length >> 1;
|
|
||||||
//console.log('cutting', half, bucket.length);
|
|
||||||
let [bottom, top] = [bucket.slice(0, half), bucket.slice(half)];
|
|
||||||
//console.log({bottom, top});
|
|
||||||
return [bottom, top];
|
|
||||||
//return [bucket.slice(0, half), bucket.slice(half)];
|
|
||||||
};
|
|
||||||
while (buckets.length < n) {
|
|
||||||
// Find the bucket with the greatest range in any channel
|
|
||||||
let ranges = buckets.map((bucket) => {
|
|
||||||
if (bucket.length == 0) {
|
|
||||||
throw new Error('xxx empty bucket');
|
|
||||||
}
|
|
||||||
let red = bucket.map((rgb) => rgb.r);
|
|
||||||
let green = bucket.map((rgb) => rgb.g);
|
|
||||||
let blue = bucket.map((rgb) => rgb.b);
|
|
||||||
return new RGB(
|
|
||||||
Math.max(...red) - Math.min(...red),
|
|
||||||
Math.max(...green) - Math.min(...green),
|
|
||||||
Math.max(...blue) - Math.min(...blue)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
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) {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (buckets.length > n) {
|
|
||||||
throw new Error('xxx too many colors assigned');
|
|
||||||
}
|
|
||||||
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);
|
|
||||||
|
|
||||||
// Scale the average to the brightest
|
|
||||||
let avg_luma = rgb.luma();
|
|
||||||
let lumas = bucket.map((rgb) => rgb.luma());
|
|
||||||
let brightest = Math.max(...lumas);
|
|
||||||
if (avg_luma > 0) {
|
|
||||||
rgb = rgb.multiply(brightest / avg_luma).clamp();
|
|
||||||
}
|
|
||||||
|
|
||||||
// this also works pretty ok
|
|
||||||
// but i think keeping luma is better
|
|
||||||
//
|
|
||||||
/*
|
|
||||||
// 1) take the brightest luma
|
|
||||||
// 2) take the most saturated chroma
|
|
||||||
// 3) profit!
|
|
||||||
let lumas = bucket.map((rgb) => rgb.luma());
|
|
||||||
let brightest = Math.max(...lumas);
|
|
||||||
let saturations = bucket.map((rgb) => Math.max(rgb.r, rgb.g, rgb.b) - Math.min(rgb.r, rgb.g, rgb.b));
|
|
||||||
let saturation = Math.max(...saturations);
|
|
||||||
let saturatedIndex = saturations.indexOf(saturation);
|
|
||||||
let rgb = bucket[saturatedIndex];
|
|
||||||
let luma = rgb.luma();
|
|
||||||
if (luma > 0) {
|
|
||||||
rgb = rgb.multiply(brightest / luma).clamp();
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
// pick the luma-brightest color in the bucket
|
|
||||||
// kinda nice but really aggressive with the colors
|
|
||||||
/*
|
|
||||||
let lumas = bucket.map((rgb) => rgb.luma());
|
|
||||||
let luma = Math.max(...lumas);
|
|
||||||
let rgb = bucket[lumas.indexOf(luma)];
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Take the channel-brightest color in the bucket
|
|
||||||
// bad
|
|
||||||
//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
|
|
||||||
// this is kinda good
|
|
||||||
/*
|
|
||||||
let rgb = new RGB(
|
|
||||||
Math.max(...bucket.map((rgb) => rgb.r)),
|
|
||||||
Math.max(...bucket.map((rgb) => rgb.g)),
|
|
||||||
Math.max(...bucket.map((rgb) => rgb.b))
|
|
||||||
);
|
|
||||||
*/
|
|
||||||
|
|
||||||
// combine the median of each channel
|
|
||||||
// sux
|
|
||||||
/*
|
|
||||||
let rgb = new RGB(
|
|
||||||
bucket.map((rgb) => rgb.r).sort((a, b) => b - a)[bucket.length >> 1],
|
|
||||||
bucket.map((rgb) => rgb.g).sort((a, b) => b - a)[bucket.length >> 1],
|
|
||||||
bucket.map((rgb) => rgb.b).sort((a, b) => b - a)[bucket.length >> 1]
|
|
||||||
);
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Take the luma-median color in the bucket
|
|
||||||
//let rgb = bucket.slice().sort((a, b) => b.luma() - a.luma())[bucket.length >> 1];
|
|
||||||
|
|
||||||
// Take the brightest-channel median
|
|
||||||
//let rgb = bucket.slice()
|
|
||||||
// .sort((a, b) => Math.max(b.r, b.g, b.b) - Math.max(a.r, b.g, b.b))[bucket.length >> 1];
|
|
||||||
|
|
||||||
// And map into the Atari palette
|
|
||||||
let dists = palette.map((i) => rgb.difference(atariRGB[i]).magnitude());
|
|
||||||
let closest = Math.min(...dists);
|
|
||||||
let index = dists.indexOf(closest);
|
|
||||||
return palette[index];
|
|
||||||
});
|
|
||||||
decimated.sort((a, b) => a - b);
|
|
||||||
|
|
||||||
// Palette fits
|
|
||||||
return dither(decimated);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -781,10 +306,10 @@ async function loadImage(src) {
|
||||||
let height = image.bitmap.height;
|
let height = image.bitmap.height;
|
||||||
|
|
||||||
let aspect = width / height;
|
let aspect = width / height;
|
||||||
let dar = 2 / 1.2;
|
let dar = 4 / 1.2;
|
||||||
if (aspect > ((320 / 1.2) / 192)) {
|
if (aspect > ((320 / 1.2) / 192)) {
|
||||||
// wide
|
// wide
|
||||||
width = 160;
|
width = 80;
|
||||||
height = Math.round((width * image.bitmap.height / image.bitmap.width) * dar);
|
height = Math.round((width * image.bitmap.height / image.bitmap.width) * dar);
|
||||||
if (height & 1) {
|
if (height & 1) {
|
||||||
height++;
|
height++;
|
||||||
|
@ -820,8 +345,8 @@ function imageToLinearRGB(rgba) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read an image file, squish to 160px if necessary,
|
* Read an image file, squish to 80px if necessary,
|
||||||
* and dither to 4 colors per scan line.
|
* and dither to 16 monochrome levels per scan line.
|
||||||
*
|
*
|
||||||
* @param {string} source path to source image file
|
* @param {string} source path to source image file
|
||||||
* @returns {{width: number, height: number, lines: {palette: Array, output: Uint8Array}[]}}
|
* @returns {{width: number, height: number, lines: {palette: Array, output: Uint8Array}[]}}
|
||||||
|
@ -834,8 +359,8 @@ async function convert(source) {
|
||||||
rgba
|
rgba
|
||||||
} = await loadImage(source);
|
} = await loadImage(source);
|
||||||
|
|
||||||
if (width > 160) {
|
if (width > 80) {
|
||||||
throw new Error(`expected <160px width, got ${width} pixels`);
|
throw new Error(`expected <80px width, got ${width} pixels`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (height > 192) {
|
if (height > 192) {
|
||||||
|
@ -860,17 +385,10 @@ async function convert(source) {
|
||||||
throw new Error('inconsistent data size on input');
|
throw new Error('inconsistent data size on input');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start with all colors usable with regular CTIA modes
|
|
||||||
// (not the 16-luminance special mode on GTIA)
|
|
||||||
let allColors = [];
|
|
||||||
for (let i = 0; i < 256; i += 2) {
|
|
||||||
allColors.push(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
let left = [], right = [];
|
let left = [], right = [];
|
||||||
let padding = 0;
|
let padding = 0;
|
||||||
if (width < 160) {
|
if (width < 80) {
|
||||||
padding = 160 - width;
|
padding = 80 - width;
|
||||||
|
|
||||||
let black = new RGB(0, 0, 0);
|
let black = new RGB(0, 0, 0);
|
||||||
left = repeat(black, padding >> 1);
|
left = repeat(black, padding >> 1);
|
||||||
|
@ -889,7 +407,7 @@ async function convert(source) {
|
||||||
let error = lines[y - 1].error;
|
let error = lines[y - 1].error;
|
||||||
inputLine = inputLine.map((rgb, x) => rgb.add(error[x]).clamp());
|
inputLine = inputLine.map((rgb, x) => rgb.add(error[x]).clamp());
|
||||||
}
|
}
|
||||||
let line = decimate(inputLine, allColors, 4, y);
|
let line = colorize(inputLine);
|
||||||
lines.push(line);
|
lines.push(line);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
@ -934,15 +452,11 @@ function genAssembly(width, height, nbits, lines) {
|
||||||
let half = stride * height / 2;
|
let half = stride * height / 2;
|
||||||
let frame = {
|
let frame = {
|
||||||
palette1: new Uint8Array(height),
|
palette1: new Uint8Array(height),
|
||||||
palette2: new Uint8Array(height),
|
|
||||||
palette3: new Uint8Array(height),
|
|
||||||
bitmap: new Uint8Array(stride * height),
|
bitmap: new Uint8Array(stride * height),
|
||||||
};
|
};
|
||||||
for (let y = 0; y < height; y++) {
|
for (let y = 0; y < height; y++) {
|
||||||
let base = 0;
|
let base = 0;
|
||||||
frame.palette1[y] = lines[y + base].palette[1];
|
frame.palette1[y] = lines[y + base].palette[1];
|
||||||
frame.palette2[y] = lines[y + base].palette[2];
|
|
||||||
frame.palette3[y] = lines[y + base].palette[3];
|
|
||||||
indexedToBitmap(
|
indexedToBitmap(
|
||||||
width,
|
width,
|
||||||
nbits,
|
nbits,
|
||||||
|
@ -957,10 +471,6 @@ function genAssembly(width, height, nbits, lines) {
|
||||||
|
|
||||||
.export frame1_palette1_even
|
.export frame1_palette1_even
|
||||||
.export frame1_palette1_odd
|
.export frame1_palette1_odd
|
||||||
.export frame1_palette2_even
|
|
||||||
.export frame1_palette2_odd
|
|
||||||
.export frame1_palette3_even
|
|
||||||
.export frame1_palette3_odd
|
|
||||||
|
|
||||||
.export displaylist
|
.export displaylist
|
||||||
|
|
||||||
|
@ -982,22 +492,6 @@ ${byte2byte(even(frame.palette1))}
|
||||||
frame1_palette1_odd:
|
frame1_palette1_odd:
|
||||||
${byte2byte(odd(frame.palette1))}
|
${byte2byte(odd(frame.palette1))}
|
||||||
|
|
||||||
.align 128
|
|
||||||
frame1_palette2_even:
|
|
||||||
${byte2byte(even(frame.palette2))}
|
|
||||||
|
|
||||||
.align 128
|
|
||||||
frame1_palette2_odd:
|
|
||||||
${byte2byte(odd(frame.palette2))}
|
|
||||||
|
|
||||||
.align 128
|
|
||||||
frame1_palette3_even:
|
|
||||||
${byte2byte(even(frame.palette3))}
|
|
||||||
|
|
||||||
.align 128
|
|
||||||
frame1_palette3_odd:
|
|
||||||
${byte2byte(odd(frame.palette3))}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -1011,16 +505,17 @@ displaylist:
|
||||||
.byte $f0 ; 8 blank lines
|
.byte $f0 ; 8 blank lines
|
||||||
|
|
||||||
; ${height} lines graphics
|
; ${height} lines graphics
|
||||||
; ANTIC mode e (160px 2bpp, 1 scan line per line)
|
; ANTIC mode f (320px 1bpp, 1 scan line per line)
|
||||||
.byte $4e
|
; this is modified into GTIA grayscale mode
|
||||||
|
.byte $4f
|
||||||
.addr frame1_top
|
.addr frame1_top
|
||||||
.repeat ${height / 2 - 1}
|
.repeat ${height / 2 - 1}
|
||||||
.byte $0e
|
.byte $0f
|
||||||
.endrep
|
.endrep
|
||||||
.byte $4e
|
.byte $4f
|
||||||
.addr frame1_bottom
|
.addr frame1_bottom
|
||||||
.repeat ${height / 2 - 1}
|
.repeat ${height / 2 - 1}
|
||||||
.byte $0e
|
.byte $0f
|
||||||
.endrep
|
.endrep
|
||||||
|
|
||||||
.byte $41 ; jump and blank
|
.byte $41 ; jump and blank
|
||||||
|
@ -1077,7 +572,7 @@ async function main() {
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
let nbits = 2;
|
let nbits = 4;
|
||||||
|
|
||||||
let {width, height, lines} = await convert(process.argv[2], nbits);
|
let {width, height, lines} = await convert(process.argv[2], nbits);
|
||||||
|
|
||||||
|
|
22
mono16.s
22
mono16.s
|
@ -7,6 +7,7 @@ COLPF1 = $D017
|
||||||
COLPF2 = $D018
|
COLPF2 = $D018
|
||||||
COLPF3 = $D019
|
COLPF3 = $D019
|
||||||
COLBK = $D01A
|
COLBK = $D01A
|
||||||
|
PRIOR = $D01B
|
||||||
|
|
||||||
AUDC1 = $D201
|
AUDC1 = $D201
|
||||||
DMACTL = $D400
|
DMACTL = $D400
|
||||||
|
@ -43,10 +44,6 @@ scanline_max = (lines_per_frame - scanline_offset) / 2
|
||||||
.import frame1_bottom
|
.import frame1_bottom
|
||||||
.import frame1_palette1_even
|
.import frame1_palette1_even
|
||||||
.import frame1_palette1_odd
|
.import frame1_palette1_odd
|
||||||
.import frame1_palette2_even
|
|
||||||
.import frame1_palette2_odd
|
|
||||||
.import frame1_palette3_even
|
|
||||||
.import frame1_palette3_odd
|
|
||||||
.import displaylist
|
.import displaylist
|
||||||
|
|
||||||
.code
|
.code
|
||||||
|
@ -58,6 +55,12 @@ scanline_max = (lines_per_frame - scanline_offset) / 2
|
||||||
lda #$00
|
lda #$00
|
||||||
sta DMACTL
|
sta DMACTL
|
||||||
|
|
||||||
|
; Enable GTIA monochrome mode
|
||||||
|
lda PRIOR
|
||||||
|
and #$3f
|
||||||
|
ora #$40
|
||||||
|
sta PRIOR
|
||||||
|
|
||||||
; Disable VBI and DLI but allow Reset
|
; Disable VBI and DLI but allow Reset
|
||||||
lda #$20
|
lda #$20
|
||||||
sta NMIEN
|
sta NMIEN
|
||||||
|
@ -101,22 +104,15 @@ wait_loop:
|
||||||
;ldy scanline ; 3 cyc
|
;ldy scanline ; 3 cyc
|
||||||
;inc scanline ; 5 cyc
|
;inc scanline ; 5 cyc
|
||||||
|
|
||||||
; 23-26 cycles before break
|
; 8-9 cycles before break
|
||||||
; Leisurely memory fetches
|
; Leisurely memory fetches
|
||||||
lda frame1_palette1_even + frame_offset + line_offset - scanline_offset / 2,y ; 4/5 @FIXME alternate
|
lda frame1_palette1_even + frame_offset + line_offset - scanline_offset / 2,y ; 4/5 @FIXME alternate
|
||||||
pha ; 3
|
|
||||||
ldx frame1_palette2_even + frame_offset + line_offset - scanline_offset / 2,y ; 4/5
|
|
||||||
lda frame1_palette3_even + frame_offset + line_offset - scanline_offset / 2,y ; 4/5
|
|
||||||
tay ; 2
|
|
||||||
pla ; 3
|
|
||||||
; Wait for horizontal blank
|
; Wait for horizontal blank
|
||||||
sta WSYNC ; 4
|
sta WSYNC ; 4
|
||||||
|
|
||||||
; 12 cycles after break
|
; 4 cycles after break
|
||||||
; Update color registers as fast as possible
|
; Update color registers as fast as possible
|
||||||
sta COLPF0 ; 4
|
sta COLPF0 ; 4
|
||||||
stx COLPF1 ; 4
|
|
||||||
sty COLPF2 ; 4
|
|
||||||
.endmacro
|
.endmacro
|
||||||
|
|
||||||
.macro run_frame frame_offset
|
.macro run_frame frame_offset
|
||||||
|
|
Loading…
Reference in a new issue