mirror of
https://github.com/tiennm99/rplace.git
synced 2026-10-12 12:14:17 +00:00
Add W/H inputs, aspect-ratio lock, resampling method dropdown (nearest / bilinear / box), "Fit to canvas" (respects current origin) and "1:1" reset buttons to the import panel. Pipeline now runs resize before palette quantization so dither/skip-matching operate on the final dimensions. Shared src/lib/image-resize.js is reused by scripts/image-to-colors.js, which gains --width, --height, --method flags with aspect-preserve when only one dimension is provided.
99 lines
3.8 KiB
JavaScript
99 lines
3.8 KiB
JavaScript
import { describe, it, expect } from 'vitest';
|
|
import { resizeRgba } from '../../src/lib/image-resize.js';
|
|
|
|
/** Build an RGBA buffer from a function (x,y) → [r,g,b,a]. */
|
|
function build(w, h, fn) {
|
|
const out = new Uint8ClampedArray(w * h * 4);
|
|
for (let y = 0; y < h; y++) {
|
|
for (let x = 0; x < w; x++) {
|
|
const [r, g, b, a] = fn(x, y);
|
|
const i = (y * w + x) * 4;
|
|
out[i] = r; out[i + 1] = g; out[i + 2] = b; out[i + 3] = a;
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
describe('resizeRgba', () => {
|
|
it('identity (same dims) returns copy with same contents', () => {
|
|
const src = build(3, 2, (x, y) => [x * 10, y * 20, 0, 255]);
|
|
const out = resizeRgba(src, 3, 2, 3, 2);
|
|
expect(out).toEqual(new Uint8ClampedArray(src));
|
|
expect(out).not.toBe(src); // must be a fresh buffer
|
|
});
|
|
|
|
it('throws on zero or negative destination dims', () => {
|
|
const src = new Uint8ClampedArray(4);
|
|
expect(() => resizeRgba(src, 1, 1, 0, 1)).toThrow();
|
|
expect(() => resizeRgba(src, 1, 1, 1, -1)).toThrow();
|
|
});
|
|
|
|
it('nearest 2x downscale picks one of each 2x2 block (integer factor)', () => {
|
|
// 4x4 where each 2x2 block has a unique color
|
|
const src = build(4, 4, (x, y) => {
|
|
const bx = x >> 1, by = y >> 1;
|
|
return [bx * 80, by * 80, 0, 255];
|
|
});
|
|
const out = resizeRgba(src, 4, 4, 2, 2, 'nearest');
|
|
expect(out.length).toBe(2 * 2 * 4);
|
|
// Each output pixel must equal one of the four pixels in its source block.
|
|
// For default nearest (floor), 2x downscale picks the top-left of the block.
|
|
expect([out[0], out[1], out[2]]).toEqual([0, 0, 0]);
|
|
expect([out[4], out[5], out[6]]).toEqual([80, 0, 0]);
|
|
expect([out[8], out[9], out[10]]).toEqual([0, 80, 0]);
|
|
expect([out[12], out[13], out[14]]).toEqual([80, 80, 0]);
|
|
});
|
|
|
|
it('nearest 2x upscale duplicates pixels', () => {
|
|
const src = build(2, 1, (x) => [x * 255, 0, 0, 255]);
|
|
const out = resizeRgba(src, 2, 1, 4, 2, 'nearest');
|
|
// Each source pixel appears in a 2x2 block
|
|
expect([out[0], out[4]]).toEqual([0, 0]);
|
|
expect([out[8], out[12]]).toEqual([255, 255]);
|
|
expect([out[16], out[20]]).toEqual([0, 0]);
|
|
expect([out[24], out[28]]).toEqual([255, 255]);
|
|
});
|
|
|
|
it('bilinear produces an in-between value at the midpoint', () => {
|
|
// Two pixels: black (0) and white (255). Midpoint should blend toward 127/128.
|
|
const src = build(2, 1, (x) => [x * 255, x * 255, x * 255, 255]);
|
|
const out = resizeRgba(src, 2, 1, 3, 1, 'bilinear');
|
|
// out[1] is the middle pixel — must be strictly between the endpoints.
|
|
const mid = out[4];
|
|
expect(mid).toBeGreaterThan(0);
|
|
expect(mid).toBeLessThan(255);
|
|
// Endpoints are preserved
|
|
expect(out[0]).toBe(0);
|
|
expect(out[8]).toBe(255);
|
|
});
|
|
|
|
it('box averages a 2x2 block on 2x downscale', () => {
|
|
const src = build(2, 2, (x, y) => {
|
|
if (x === 0 && y === 0) return [0, 0, 0, 255];
|
|
if (x === 1 && y === 0) return [100, 0, 0, 255];
|
|
if (x === 0 && y === 1) return [0, 100, 0, 255];
|
|
return [100, 100, 0, 255];
|
|
});
|
|
const out = resizeRgba(src, 2, 2, 1, 1, 'box');
|
|
expect(out.length).toBe(4);
|
|
expect(out[0]).toBe(50); // avg of 0, 100, 0, 100
|
|
expect(out[1]).toBe(50);
|
|
expect(out[2]).toBe(0);
|
|
expect(out[3]).toBe(255);
|
|
});
|
|
|
|
it('unknown method falls back to nearest', () => {
|
|
const src = build(2, 1, (x) => [x * 255, 0, 0, 255]);
|
|
const out = resizeRgba(src, 2, 1, 4, 1, /** @type any */ ('mystery'));
|
|
// Should be nearest-neighbor output: 0,0,255,255
|
|
expect([out[0], out[4], out[8], out[12]]).toEqual([0, 0, 255, 255]);
|
|
});
|
|
|
|
it('preserves alpha channel', () => {
|
|
const src = build(2, 1, (x) => [255, 0, 0, x === 0 ? 0 : 128]);
|
|
const out = resizeRgba(src, 2, 1, 2, 1, 'nearest');
|
|
expect(out[3]).toBe(0);
|
|
expect(out[7]).toBe(128);
|
|
});
|
|
});
|