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