import { describe, expect, it } from 'vitest';
import { findFigures, resizeRect, quantize } from '../imaging.mjs';
import { buildAtlas } from '../atlas.mjs';
import { encodePng, decodePng } from '../png.mjs';
/** Однотонное изображение w×h цвета [r,g,b] (alpha 255). */
const solid = (w, h, [r, g, b]) => {
const rgba = new Uint8Array(w * h * 4);
for (let p = 0; p < rgba.length; p += 4) {
rgba[p] = r; rgba[p + 1] = g; rgba[p + 2] = b; rgba[p + 3] = 255;
}
return { width: w, height: h, rgba };
};
/** Прямоугольник цвета color на фоне bg (внутри — сплошной). */
const withRect = (img, rect, [r, g, b]) => {
for (let y = rect.y; y < rect.y + rect.h; y++) {
for (let x = rect.x; x < rect.x + rect.w; x++) {
const i = (y * img.width + x) * 4;
img.rgba[i] = r; img.rgba[i + 1] = g; img.rgba[i + 2] = b;
}
}
return img;
};
describe('findFigures', () => {
it('находит фигуры на фоне и сортирует сверху-вниз/слева-направо', () => {
const img = withRect(withRect(withRect(
solid(64, 64, [255, 255, 255]),
{ x: 4, y: 8, w: 10, h: 12 }, [10, 20, 30]),
{ x: 40, y: 4, w: 12, h: 14 }, [40, 50, 60]),
{ x: 20, y: 40, w: 16, h: 16 }, [70, 80, 90]
);
expect(findFigures(img, { minArea: 30 })).toEqual([
{ x: 40, y: 4, w: 12, h: 14 }, // y=4 раньше y=8
{ x: 4, y: 8, w: 10, h: 12 },
{ x: 20, y: 40, w: 16, h: 16 }
]);
});
it('bg: corner берёт фон из углового пикселя, мелочь отсеивается по minArea', () => {
const img = withRect(solid(32, 32, [240, 240, 240]),
{ x: 2, y: 2, w: 20, h: 20 }, [0, 0, 0]);
img.rgba[(31 * 32 + 31) * 4] = 0; // одиночный пиксель в углу — шум
const figs = findFigures(img, { minArea: 100 });
expect(figs).toEqual([{ x: 2, y: 2, w: 20, h: 20 }]);
});
it('явный bg и dist: пиксели ближе порога сливаются с фоном', () => {
const img = withRect(solid(32, 32, [0, 0, 0]),
{ x: 1, y: 1, w: 10, h: 10 }, [30, 30, 30]);
// dist 100: |30|*3 = 90 < 100 — фигура не видна
expect(findFigures(img, { bg: [0, 0, 0], dist: 100, minArea: 10 })).toEqual([]);
// dist 50: фигура видна
expect(findFigures(img, { bg: [0, 0, 0], dist: 50, minArea: 10 })).toEqual([
{ x: 1, y: 1, w: 10, h: 10 }
]);
});
});
describe('resizeRect', () => {
it('nearest-сэмпл по центрам ячеек', () => {
const img = withRect(solid(8, 8, [255, 255, 255]),
{ x: 2, y: 2, w: 4, h: 4 }, [255, 0, 0]);
const out = resizeRect(img, { x: 2, y: 2, w: 4, h: 4 }, 2, 2);
// k = 2, центры (0.5,0.5)*2 = (1,1) — все сэмплы внутри красного
expect(out[0]).toBe(255); expect(out[1]).toBe(0); expect(out[3]).toBe(255);
});
it('фон становится прозрачным (bg corner + dist)', () => {
const img = withRect(solid(8, 8, [255, 255, 255]),
{ x: 2, y: 2, w: 4, h: 4 }, [255, 0, 0]);
const out = resizeRect(img, { x: 0, y: 0, w: 8, h: 8 }, 4, 4);
// k = 2: углы сэмплят фон -> alpha 0; центр (2..3,2..3) — красный
expect(out[3]).toBe(0);
const center = (2 * 4 + 2) * 4;
expect(out[center]).toBe(255); expect(out[center + 1]).toBe(0);
expect(out[center + 3]).toBe(255);
});
});
describe('buildAtlas', () => {
const frame = (r) => {
const f = new Uint8Array(16 * 24 * 4);
for (let p = 0; p < f.length; p += 4) f[p] = r, f[p + 3] = 255;
return f;
};
it('кадры в строку + Pixi JSON (формат hero_sheet)', () => {
const { png, json } = buildAtlas([frame(1), frame(2)], ['a', 'b'], { frameW: 16, frameH: 24, image: 'x.png' });
const sheet = decodePng(png);
expect(sheet.width).toBe(32);
expect(sheet.height).toBe(24);
expect(sheet.rgba[0]).toBe(1);
expect(sheet.rgba[(0 * 32 + 16) * 4]).toBe(2); // второй кадр с x=16
expect(json.frames.a.frame).toEqual({ x: 0, y: 0, w: 16, h: 24 });
expect(json.frames.b.frame).toEqual({ x: 16, y: 0, w: 16, h: 24 });
expect(json.meta).toEqual({ image: 'x.png', size: { w: 32, h: 24 }, scale: 1 });
});
it('бросает при расхождении длины имён и размере кадра', () => {
expect(() => buildAtlas([frame(1)], ['a', 'b'], { frameW: 16, frameH: 24, image: 'x.png' })).toThrow();
expect(() => buildAtlas([new Uint8Array(10)], ['a'], { frameW: 16, frameH: 24, image: 'x.png' })).toThrow();
});
});
describe('roundtrip findFigures -> resizeRect -> quantize на PNG', () => {
it('лист с двумя квадратами -> кадры в палитре', () => {
const img = withRect(withRect(solid(64, 32, [255, 255, 255]),
{ x: 4, y: 4, w: 20, h: 24 }, [255, 0, 0]),
{ x: 36, y: 4, w: 20, h: 24 }, [0, 0, 255]);
const figs = findFigures(img, { minArea: 100 });
expect(figs).toHaveLength(2);
const first = quantize(resizeRect(img, figs[0], 8, 8), [[255, 0, 0], [0, 0, 255], [255, 255, 255]]);
const second = quantize(resizeRect(img, figs[1], 8, 8), [[255, 0, 0], [0, 0, 255], [255, 255, 255]]);
expect(first.length).toBe(8 * 8 * 4);
// все сэмплы внутри bbox фигуры -> весь кадр в цвете фигуры, без фона
expect([...first.slice(0, 3)]).toEqual([255, 0, 0]);
expect([...second.slice(0, 3)]).toEqual([0, 0, 255]);
});
});