Newer
Older
rpg / apps / game / tools / aiart / recolor.mjs
/**
 * Палитровый ремапер: перекраска квантованного спрайта маппингом цветов.
 * Две потребности (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(', ')})`);
}