/**
* Палитровый ремапер: перекраска квантованного спрайта маппингом цветов.
* Две потребности (docs/plan.md, работа B): привести «поплавшие» промпт-цвета
* AI-генерации к каноническим и дешёвые цветовые вариации NPC из одного спрайта.
*
* CLI: node recolor.mjs <in.png> <out.png> <из=в ...>
* цвета — ключи палитры (C1) или #hex; вход должен быть квантован
* в палитру (иначе «из» не найдётся — замена по точному совпадению).
*/
import { readFileSync, writeFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import path from 'node:path';
import { decodePng, encodePng } from '@rpg/engine/tools/png.mjs';
import { PALETTE } from '../pixelart/palette.mjs';
/** Цвет из аргумента: ключ палитры (C1) или #hex → [r,g,b]. */
export function parseColor(token) {
if (!token) throw new Error('пустой цвет');
if (token.startsWith('#')) {
const n = parseInt(token.slice(1), 16);
return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
}
if (!(token in PALETTE)) throw new Error(`«${token}» — нет такого ключа палитры`);
return hexToRgb(PALETTE[token]);
}
function hexToRgb(hex) {
const n = parseInt(hex.slice(1), 16);
return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
}
/**
* Замена цветов по точному совпадению RGB (alpha не трогаем).
* @param pairs массив [[fromRGB, toRGB], ...]
*/
export function remap(rgba, pairs) {
const lookup = new Map(pairs.map(([from, to]) => [from.join(','), to]));
const out = new Uint8Array(rgba);
for (let p = 0; p < out.length; p += 4) {
const to = lookup.get(`${out[p]},${out[p + 1]},${out[p + 2]}`);
if (to) {
out[p] = to[0]; out[p + 1] = to[1]; out[p + 2] = to[2];
}
}
return out;
}
// ---------- CLI (только при прямом запуске) ----------
const invoked = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
if (invoked) {
const [, , src, dst, ...tokens] = process.argv;
if (!src || !dst || !tokens.length) {
console.log('Использование: node recolor.mjs <in.png> <out.png> <из=в ...> (цвета: ключ палитры или #hex)');
process.exit(1);
}
const pairs = tokens.map((t) => {
const [from, to] = t.split('=');
return [parseColor(from), parseColor(to)];
});
const img = decodePng(readFileSync(src));
writeFileSync(dst, encodePng(img.width, img.height, Buffer.from(remap(img.rgba, pairs))));
console.log(`[recolor] ${src} -> ${dst} (замены: ${pairs.map(([f, t]) => `#${f.map((v) => v.toString(16).padStart(2, '0')).join('')}→#${t.map((v) => v.toString(16).padStart(2, '0')).join('')}`).join(', ')})`);
}