174 lines
3.9 KiB
JavaScript
174 lines
3.9 KiB
JavaScript
function hexToRgb(hex) {
|
|
hex = hex.replace(/^#/, "");
|
|
if (hex.length === 3) {
|
|
hex = hex
|
|
.split("")
|
|
.map(function (x) {
|
|
return x + x;
|
|
})
|
|
.join("");
|
|
}
|
|
var num = parseInt(hex, 16);
|
|
return [(num >> 16) & 255, (num >> 8) & 255, num & 255];
|
|
}
|
|
|
|
function colorDistance(rgb1, rgb2) {
|
|
return Math.sqrt(
|
|
Math.pow(rgb1[0] - rgb2[0], 2) +
|
|
Math.pow(rgb1[1] - rgb2[1], 2) +
|
|
Math.pow(rgb1[2] - rgb2[2], 2),
|
|
);
|
|
}
|
|
|
|
function closestColor(targetHex, colorList) {
|
|
var targetRgb = hexToRgb(targetHex);
|
|
var minDist = Infinity;
|
|
var closest = null;
|
|
for (var i = 0; i < colorList.length; i++) {
|
|
var rgb = hexToRgb(colorList[i]);
|
|
var dist = colorDistance(targetRgb, rgb);
|
|
if (dist < minDist) {
|
|
minDist = dist;
|
|
closest = colorList[i];
|
|
}
|
|
}
|
|
return closest;
|
|
}
|
|
|
|
function middleColorsAndApproximations(colorList, count) {
|
|
if (!Array.isArray(colorList) || colorList.length === 0 || count < 1) {
|
|
return {
|
|
middleColors: [],
|
|
approximations: {},
|
|
};
|
|
}
|
|
|
|
var rgbs = colorList.map(hexToRgb);
|
|
|
|
var avg = [0, 0, 0];
|
|
for (var i = 0; i < rgbs.length; i++) {
|
|
avg[0] += rgbs[i][0];
|
|
avg[1] += rgbs[i][1];
|
|
avg[2] += rgbs[i][2];
|
|
}
|
|
avg[0] = Math.round(avg[0] / rgbs.length);
|
|
avg[1] = Math.round(avg[1] / rgbs.length);
|
|
avg[2] = Math.round(avg[2] / rgbs.length);
|
|
|
|
if (count >= colorList.length) {
|
|
return {
|
|
middleColors: colorList.slice(),
|
|
approximations: Object.fromEntries(colorList.map((c) => [c, c])),
|
|
};
|
|
}
|
|
|
|
var colorDistances = colorList.map(function (hex, idx) {
|
|
return {
|
|
hex: hex,
|
|
dist: colorDistance(hexToRgb(hex), avg),
|
|
};
|
|
});
|
|
|
|
colorDistances.sort(function (a, b) {
|
|
return a.dist - b.dist;
|
|
});
|
|
|
|
var middleColors = colorDistances.slice(0, count).map(function (obj) {
|
|
return obj.hex;
|
|
});
|
|
|
|
var approximations = {};
|
|
for (var i = 0; i < colorList.length; i++) {
|
|
var hex = colorList[i];
|
|
if (middleColors.indexOf(hex) !== -1) {
|
|
approximations[hex] = hex;
|
|
} else {
|
|
approximations[hex] = closestColor(hex, middleColors);
|
|
}
|
|
}
|
|
|
|
return {
|
|
middleColors: middleColors,
|
|
approximations: approximations,
|
|
};
|
|
}
|
|
|
|
import * as fs from "fs/promises";
|
|
import { compress } from "./tiny_lz4.js";
|
|
|
|
export function pack_image(blocks) {
|
|
const colors = middleColorsAndApproximations(
|
|
blocks.map((z) => z.color),
|
|
300,
|
|
);
|
|
const header = new Uint8Array(300 * 3 + 1 + 2);
|
|
const dv = new DataView(header.buffer);
|
|
dv.setUint16(0, blocks.length);
|
|
let idx = 2;
|
|
|
|
colors.middleColors.forEach((z, i) => {
|
|
const [r, g, b] = hexToRgb(z);
|
|
dv.setUint8(idx + 1, r);
|
|
dv.setUint8(idx + 2, g);
|
|
dv.setUint8(idx + 3, b);
|
|
|
|
idx += 3;
|
|
});
|
|
|
|
function pack_block(text, paletteIdx) {
|
|
let raw_bytes = [];
|
|
|
|
text
|
|
.replaceAll("\n", "?")
|
|
.split("")
|
|
.forEach((z, i) => {
|
|
let id = i % 8;
|
|
if (id == 0) {
|
|
raw_bytes.push([]);
|
|
}
|
|
|
|
raw_bytes.at(-1).push(z == "▇" ? 0 : 1);
|
|
});
|
|
|
|
let bytes = raw_bytes.map((z) =>
|
|
z.reduce((acc, bit, i) => acc | (bit << (7 - i)), 0),
|
|
);
|
|
|
|
const final = new Uint8Array(3 + bytes.length);
|
|
const dv = new DataView(final.buffer);
|
|
|
|
dv.setUint16(0, paletteIdx);
|
|
dv.setUint8(2, (bytes.length << 4) | ((8 - raw_bytes.at(-1).length) & 0xf));
|
|
bytes.forEach((z, i) => {
|
|
dv.setUint8(i + 3, z);
|
|
});
|
|
return final;
|
|
}
|
|
|
|
const byte_blocks = [];
|
|
|
|
blocks.forEach((z) => {
|
|
byte_blocks.push(
|
|
pack_block(
|
|
z.text,
|
|
colors.middleColors.indexOf(colors.approximations[z.color]),
|
|
),
|
|
);
|
|
});
|
|
|
|
const totalBlockBytes = byte_blocks
|
|
.map((z) => z.length)
|
|
.reduce((a, b) => a + b, 0);
|
|
const final = new Uint8Array(header.length + totalBlockBytes);
|
|
|
|
final.set(header, 0);
|
|
|
|
let offset = header.length;
|
|
byte_blocks.forEach((block) => {
|
|
final.set(block, offset);
|
|
offset += block.length;
|
|
});
|
|
|
|
const compressed = compress(final);
|
|
return compressed;
|
|
}
|