Newer
Older
rpg / tools / pixelart / canvas.mjs
/**
 * Мини-канвас с палитрой: пиксель, ромб, шум, ASCII-карты спрайтов.
 */
import { RGBA, TRANSPARENT } from './palette.mjs';
import { encodePng } from './png.mjs';

export class Canvas {
    constructor(width, height) {
        this.width = width;
        this.height = height;
        this.data = Buffer.alloc(width * height * 4);
        this.data.fill(0);
    }

    set(x, y, color, alpha = 255) {
        if (x < 0 || y < 0 || x >= this.width || y >= this.height) return;
        const i = (y * this.width + x) * 4;
        if (color === null) {
            this.data.fill(0, i, i + 4);
            return;
        }
        const [r, g, b] = RGBA[color] ?? color;
        this.data[i] = r;
        this.data[i + 1] = g;
        this.data[i + 2] = b;
        this.data[i + 3] = alpha;
    }

    get(x, y) {
        if (x < 0 || y < 0 || x >= this.width || y >= this.height) return null;
        const i = (y * this.width + x) * 4;
        if (this.data[i + 3] === 0) return null;
        return [this.data[i], this.data[i + 1], this.data[i + 2], 255];
    }

    toPng() {
        return encodePng(this.width, this.height, this.data);
    }
}

/** Детерминированный ГПСЧ (mulberry32). */
export function rng(seed) {
    return () => {
        seed |= 0;
        seed = (seed + 0x6d2b79f5) | 0;
        let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
        t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
        return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
    };
}

/**
 * Залить ромб 2:1 по канвасу: base — базовый цвет; свет на верхних гранях,
 * тень на нижних. light/dark — ключи палитры или null.
 * Центр ромба (cx, cy), ширина w, высота h.
 */
export function fillDiamond(canvas, cx, cy, w, h, base, light = null, dark = null) {
    const hw = w / 2;
    const hh = h / 2;
    for (let y = 0; y < h; y++) {
        for (let x = 0; x < w; x++) {
            const rx = x - hw + 0.5;
            const ry = y - hh + 0.5;
            if (Math.abs(rx) / hw + Math.abs(ry) / hh <= 1) {
                canvas.set(cx - hw + x, cy - hh + y, base);
            }
        }
    }
    for (let i = 0; i < hw; i++) {
        // Верхний склон: от левого угла к верхней вершине (свет) и от вершины к правому углу.
        const leftY = cy - Math.round((i / hw) * hh);
        const rightY = cy - hh + Math.round((i / hw) * hh);
        if (light) {
            canvas.set(cx - hw + i, leftY, light);
            canvas.set(cx + i, rightY, light);
        }
        // Нижний склон: от левого угла вниз к нижней вершине (тень) и от неё к правому углу.
        const botLeftY = cy + Math.round((i / hw) * hh);
        const botRightY = cy + hh - Math.round((i / hw) * hh);
        if (dark) {
            canvas.set(cx - hw + i, botLeftY, dark);
            canvas.set(cx + i, botRightY, dark);
        }
    }
}

/**
 * ASCII-карта -> спрайт. Каждый символ — ключ палитры; '.' — прозрачный пиксель.
 * Дополнительные символы передаются в extra ('x' -> 'P1' и т.п.).
 */
export function fromAscii(rows, extra = {}) {
    const h = rows.length;
    const w = Math.max(...rows.map((r) => r.length));
    const canvas = new Canvas(w, h);
    for (let y = 0; y < h; y++) {
        for (let x = 0; x < rows[y].length; x++) {
            const ch = rows[y][x];
            if (ch === '.') {
                continue;
            }
            const key = extra[ch] ?? ch;
            if (!RGBA[key]) {
                throw new Error(`Неизвестный символ '${ch}' в строке ${y}: ${rows[y]}`);
            }
            canvas.set(x, y, key);
        }
    }
    return canvas;
}