work better
This commit is contained in:
parent
7aca51b4c3
commit
da3a6d69c6
4 changed files with 2142 additions and 0 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,5 +1,7 @@
|
|||
.DS_Store
|
||||
node_modules
|
||||
*.o
|
||||
*.xex
|
||||
*.pxd
|
||||
canvas*.png
|
||||
*.s.png
|
752
dither-image.js
Normal file
752
dither-image.js
Normal file
|
@ -0,0 +1,752 @@
|
|||
import {
|
||||
readFileSync,
|
||||
writeFileSync
|
||||
} from 'fs';
|
||||
|
||||
import Jimp from 'Jimp';
|
||||
|
||||
function toLinear(val) {
|
||||
// use a 2.4 gamma approximation
|
||||
// this is BT.1886 compatible
|
||||
// and simpler than sRGB
|
||||
let unit = val / 255;
|
||||
unit **= 2.4;
|
||||
return unit * 255;
|
||||
}
|
||||
|
||||
function fromLinear(val) {
|
||||
let unit = val / 255;
|
||||
unit **= (1 / 2.4);
|
||||
return unit * 255;
|
||||
}
|
||||
|
||||
class RGB {
|
||||
constructor(r, g, b) {
|
||||
this.r = r;
|
||||
this.g = g;
|
||||
this.b = b;
|
||||
}
|
||||
|
||||
static fromHex(val) {
|
||||
let r = val & 0xff;
|
||||
let g = (val >> 8) & 0xff;
|
||||
let b = (val >> 16) & 0xff;
|
||||
return new RGB(r,g,b);
|
||||
}
|
||||
|
||||
toLinear() {
|
||||
return new RGB(
|
||||
toLinear(this.r),
|
||||
toLinear(this.g),
|
||||
toLinear(this.b)
|
||||
);
|
||||
}
|
||||
|
||||
fromLinear() {
|
||||
return new RGB(
|
||||
fromLinear(this.r),
|
||||
fromLinear(this.g),
|
||||
fromLinear(this.b)
|
||||
);
|
||||
}
|
||||
|
||||
cap() {
|
||||
if (this.r < 0) {
|
||||
this.r = 0;
|
||||
}
|
||||
if (this.g < 0) {
|
||||
this.g = 0;
|
||||
}
|
||||
if (this.b < 0) {
|
||||
this.b = 0;
|
||||
}
|
||||
if (this.r > 255) {
|
||||
this.r = 255;
|
||||
}
|
||||
if (this.g > 255) {
|
||||
this.g = 255;
|
||||
}
|
||||
if (this.b > 255) {
|
||||
this.b = 255;
|
||||
}
|
||||
}
|
||||
|
||||
inc(other) {
|
||||
this.r += other.r;
|
||||
this.g += other.g;
|
||||
this.b += other.b;
|
||||
return this;
|
||||
}
|
||||
|
||||
add(other) {
|
||||
return new RGB(
|
||||
this.r + other.r,
|
||||
this.g + other.g,
|
||||
this.b + other.b
|
||||
);
|
||||
}
|
||||
|
||||
difference(other) {
|
||||
return new RGB(
|
||||
this.r - other.r,
|
||||
this.g - other.g,
|
||||
this.b - other.b
|
||||
);
|
||||
}
|
||||
|
||||
divide(scalar) {
|
||||
return new RGB(
|
||||
this.r / scalar,
|
||||
this.g / scalar,
|
||||
this.b / scalar,
|
||||
);
|
||||
}
|
||||
|
||||
magnitude() {
|
||||
return Math.sqrt(
|
||||
this.r * this.r +
|
||||
this.g * this.g +
|
||||
this.b * this.b
|
||||
);
|
||||
}
|
||||
|
||||
distance(other) {
|
||||
return this.difference(other).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 = [
|
||||
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).toLinear());
|
||||
|
||||
/**
|
||||
* Dither RGB input data with a target palette size.
|
||||
* 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 {number[]} palette - current working palette, as Atari 8-bit color values (low nybble luminance, high nybble hue)
|
||||
* @param {number} n - target color count
|
||||
* @param {Object} inputError
|
||||
* @returns {{output: Uint8Array, palette: number[], error: {red: Float64Array, green: Float64Array, blue: Float64Array}}}
|
||||
*/
|
||||
function decimate(input, palette, n, inputError) {
|
||||
// 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;
|
||||
|
||||
// Apply dithering with given palette and collect color usage stats
|
||||
let dither = (palette) => {
|
||||
let fitness = new Float64Array(width);
|
||||
let error = {
|
||||
right: new RGB(0, 0, 0),
|
||||
next: [],
|
||||
};
|
||||
for (let i = 0; i < width; i++) {
|
||||
error.next[i] = new RGB(0, 0, 0);
|
||||
}
|
||||
|
||||
let output = new Uint8Array(width);
|
||||
let popularity = new Int32Array(width);
|
||||
|
||||
let nextError = new RGB(0, 0, 0);
|
||||
|
||||
// Try dithering with this palette.
|
||||
for (let x = 0; x < width; x++) {
|
||||
let rgb = input[x];
|
||||
rgb = rgb.add(error.right);
|
||||
if (inputError) {
|
||||
rgb.inc(inputError[x]);
|
||||
}
|
||||
|
||||
// find the closest possible color
|
||||
// @todo consider doing the difference scoring in luminance and hue spaces
|
||||
let shortest = Infinity;
|
||||
let pick = 1;
|
||||
|
||||
for (let i = 0; i < palette.length; i++) {
|
||||
let diff = rgb.difference(atariRGB[palette[i]]);
|
||||
let dist = diff.magnitude();
|
||||
if (dist < shortest) {
|
||||
nextError = diff;
|
||||
shortest = dist;
|
||||
pick = i;
|
||||
}
|
||||
}
|
||||
|
||||
output[x] = pick;
|
||||
popularity[pick]++;
|
||||
|
||||
if (x == width - 1) {
|
||||
let half = nextError.divide(2);
|
||||
error.next[x - 1].inc(half);
|
||||
error.next[x].inc(half);
|
||||
} else {
|
||||
let quarter = nextError.divide(4);
|
||||
error.right = quarter;
|
||||
error.next[x - 1]?.inc(quarter); // @fixme should we change the amount?
|
||||
error.next[x].inc(quarter);
|
||||
error.next[x + 1].inc(quarter);
|
||||
}
|
||||
|
||||
// 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));
|
||||
}
|
||||
return {
|
||||
output,
|
||||
palette,
|
||||
fitness,
|
||||
popularity,
|
||||
error: error.next
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
let decimated = palette.slice();
|
||||
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
// Palette fits
|
||||
return dither(decimated);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read an image file into a buffer
|
||||
* @param {string} src
|
||||
* @returns {{width: number, height: number, rgba: Uint8Array}}
|
||||
*/
|
||||
async function loadImage(src) {
|
||||
let image = await Jimp.read(src);
|
||||
let width = image.bitmap.width;
|
||||
let height = image.bitmap.height;
|
||||
let rgba = image.bitmap.data.slice();
|
||||
return {
|
||||
width,
|
||||
height,
|
||||
rgba,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Halve the horizontal resolution, interpolating input.
|
||||
* @param {Uint8Array} rgba
|
||||
* @returns {Uint8Array}
|
||||
**/
|
||||
function halveImage(rgba) {
|
||||
let out = new Uint8Array(rgba.length >> 1);
|
||||
for (let i = 0; i < rgba.length; i += 8) {
|
||||
let j = i >> 1;
|
||||
out[j + 0] = (rgba[i + 0] + rgba[i + 4] + 1) >> 1;
|
||||
out[j + 1] = (rgba[i + 1] + rgba[i + 5] + 1) >> 1;
|
||||
out[j + 2] = (rgba[i + 2] + rgba[i + 6] + 1) >> 1;
|
||||
out[j + 3] = (rgba[i + 3] + rgba[i + 7] + 1) >> 1;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function imageToLinearRGB(rgba) {
|
||||
let input = [];
|
||||
for (let i = 0; i < rgba.length; i += 4) {
|
||||
input.push(new RGB(
|
||||
rgba[i + 0],
|
||||
rgba[i + 1],
|
||||
rgba[i + 2]
|
||||
).toLinear());
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read an image file, squish to 160px if necessary,
|
||||
* and dither to 4 colors per scan line.
|
||||
*
|
||||
* @param {string} source path to source image file
|
||||
* @returns {{width: number, height: number, lines: {palette: Array, output: Uint8Array}[]}}
|
||||
*/
|
||||
async function convert(source, nbits) {
|
||||
|
||||
let {
|
||||
width,
|
||||
height,
|
||||
rgba
|
||||
} = await loadImage(source);
|
||||
|
||||
if (width == 320) {
|
||||
width = 160;
|
||||
rgba = halveImage(rgba);
|
||||
}
|
||||
|
||||
if (width !== 160) {
|
||||
throw new Error(`expected 160px-compatible width, got ${width} pixels`);
|
||||
}
|
||||
|
||||
if (height !== 192) {
|
||||
// @fixme support up to 240px
|
||||
throw new Error(`expected 192px height, got ${height} pixels`);
|
||||
}
|
||||
|
||||
if (rgba.length != width * 4 * height) {
|
||||
console.log(`
|
||||
width: ${width}
|
||||
height: ${height}
|
||||
rgba.length: ${rgba.length}`)
|
||||
throw new Error('inconsistent data size');
|
||||
}
|
||||
|
||||
let input = imageToLinearRGB(rgba);
|
||||
|
||||
if (input.length != width * height) {
|
||||
console.log(`
|
||||
width: ${width}
|
||||
height: ${height}
|
||||
rgba.length: ${input.length}`)
|
||||
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 < 0xff; i += 2) {
|
||||
allColors.push(i);
|
||||
}
|
||||
|
||||
let lines = [];
|
||||
for (let y = 0; y < height; y++) {
|
||||
let error = lines[y - 1]?.error;
|
||||
let inputLine = input.slice(y * width, (y + 1) * width);
|
||||
console.log(`DOING LINE ${y}; have ${inputLine.length} pixels`);
|
||||
let line = decimate(inputLine, allColors, 4, error);
|
||||
lines.push(line);
|
||||
console.log(`DID LINE ${y}; have ${line.output.length} pixels`);
|
||||
}
|
||||
return {
|
||||
width,
|
||||
height,
|
||||
lines
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function indexedToBitmap(width, nbits, src, dest) {
|
||||
let nbytes = width >> nbits;
|
||||
for (let i = 0; i < nbytes; i++) {
|
||||
let a = 0;
|
||||
for (let b = 0; b < 8; b += nbits) {
|
||||
a <<= nbits;
|
||||
a |= src[i];
|
||||
}
|
||||
dest[i] = a;
|
||||
}
|
||||
}
|
||||
|
||||
function byte2byte(arr) {
|
||||
let lines = [];
|
||||
for (let i=0; i < arr.length; i++) {
|
||||
lines.push(`.byte ${lines[i]}`);
|
||||
}
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function genAssembly(width, height, nbits, lines) {
|
||||
let stride = width * nbits / 8;
|
||||
let palette1 = new Uint8Array(height);
|
||||
let palette2 = new Uint8Array(height);
|
||||
let palette3 = new Uint8Array(height);
|
||||
let framebuffer = new Uint8Array(stride * height);
|
||||
for (let y = 0; y < height; y++) {
|
||||
palette1[y] = lines[y].palette[1];
|
||||
palette2[y] = lines[y].palette[2];
|
||||
palette3[y] = lines[y].palette[3];
|
||||
indexedToBitmap(width, nbits, lines[y].output, framebuffer.slice(y * stride));
|
||||
}
|
||||
|
||||
return `
|
||||
.data
|
||||
|
||||
.export palette1:
|
||||
${byte2byte(palette1)}
|
||||
|
||||
.export palette2:
|
||||
${byte2byte(palette2)}
|
||||
|
||||
.export palette3:
|
||||
${byte2byte(palette3)}
|
||||
|
||||
.export framebuffer:
|
||||
${byte2byte(framebuffer)}
|
||||
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Double width and save as an image file
|
||||
* @param {number} width
|
||||
* @param {number} height
|
||||
* @param {{output: number[], palette: number[]}} lines
|
||||
* @param {string} dest
|
||||
*/
|
||||
async function saveImage(width, height, lines, dest) {
|
||||
let width2 = width * 2;
|
||||
let stride = width2 * 4;
|
||||
let rgba = new Uint8Array(stride * height);
|
||||
|
||||
for (let y = 0; y < height; y++) {
|
||||
let {output, palette} = lines[y];
|
||||
for (let x = 0; x < width2; x++) {
|
||||
let i = x >> 1;
|
||||
if (i >= width) {
|
||||
throw new Error('i >= width');
|
||||
}
|
||||
let rgb = atariRGB[palette[output[i]]];
|
||||
|
||||
rgba[y * stride + x * 4 + 0] = fromLinear(rgb.r);
|
||||
rgba[y * stride + x * 4 + 1] = fromLinear(rgb.g);
|
||||
rgba[y * stride + x * 4 + 2] = fromLinear(rgb.b);
|
||||
rgba[y * stride + x * 4 + 3] = 255;
|
||||
}
|
||||
}
|
||||
|
||||
let image = await new Promise((resolve, reject) => {
|
||||
new Jimp({
|
||||
data: rgba,
|
||||
width: width2,
|
||||
height,
|
||||
}, (err, image) => {
|
||||
if (err) reject(err);
|
||||
resolve(image);
|
||||
});
|
||||
});
|
||||
await image.writeAsync(dest);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
if (process.argv.length < 5) {
|
||||
console.error("Usage: node dither-image.js source-image.jpg dest-asm.s dest-preview.png");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let nbits = 2;
|
||||
|
||||
let {width, height, lines} = await convert(process.argv[2], nbits);
|
||||
|
||||
let asm = genAssembly(width, height, nbits, lines);
|
||||
writeFileSync(process.argv[3], asm, "utf-8");
|
||||
|
||||
await saveImage(width, height, lines, process.argv[4]);
|
||||
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
main();
|
1382
package-lock.json
generated
Normal file
1382
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
6
package.json
Normal file
6
package.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"jimp": "^0.16.2"
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue