feat: add registry

This commit is contained in:
2026-08-23 12:41:17 +05:00
parent f9a4973b47
commit 6fae98b25c
7 changed files with 356 additions and 5 deletions
+32 -1
View File
@@ -1,5 +1,10 @@
import { describe, expect, it } from 'vitest';
import { imageInfo } from './analyze';
import {
hasTransparency,
imageInfo,
isGrayscale,
orientationOf
} from './analyze';
import { makeImage } from './test-helpers';
describe('imageInfo', () => {
@@ -43,3 +48,29 @@ describe('imageInfo', () => {
expect(info.height).toBe(3);
});
});
describe('isGrayscale', () => {
it('серые пиксели — монохром', () => {
expect(isGrayscale(makeImage(1, 1, [[10, 10, 10, 255]]))).toBe(true);
});
it('цветной пиксель ломает монохром', () => {
expect(isGrayscale(makeImage(1, 1, [[10, 11, 10, 255]]))).toBe(false);
});
});
describe('hasTransparency', () => {
it('альфа ниже 255 — прозрачность есть', () => {
expect(hasTransparency(makeImage(1, 1, [[0, 0, 0, 254]]))).toBe(true);
});
it('все пиксели непрозрачны', () => {
expect(hasTransparency(makeImage(1, 1, [[0, 0, 0, 255]]))).toBe(false);
});
});
describe('orientationOf', () => {
it('определяет ориентацию', () => {
expect(orientationOf(makeImage(2, 3, new Array(6).fill([1, 1, 1, 1])))).toBe('portrait');
expect(orientationOf(makeImage(3, 2, new Array(6).fill([1, 1, 1, 1])))).toBe('landscape');
expect(orientationOf(makeImage(2, 2, new Array(4).fill([1, 1, 1, 1])))).toBe('square');
});
});
+24
View File
@@ -24,3 +24,27 @@ export function imageInfo(img: PixelImage): ImageInfo {
}
return { width: img.width, height: img.height, hasAlpha, colorCount: seen.size };
}
export function isGrayscale(img: PixelImage): boolean {
for (let i = 0; i < img.data.length; i += 4) {
if (img.data[i] !== img.data[i + 1] || img.data[i + 1] !== img.data[i + 2]) {
return false;
}
}
return true;
}
export function hasTransparency(img: PixelImage): boolean {
for (let i = 3; i < img.data.length; i += 4) {
if (img.data[i] < 255) return true;
}
return false;
}
export type Orientation = 'portrait' | 'landscape' | 'square';
export function orientationOf(img: PixelImage): Orientation {
if (img.height > img.width) return 'portrait';
if (img.width > img.height) return 'landscape';
return 'square';
}
+66
View File
@@ -0,0 +1,66 @@
import { describe, expect, it } from 'vitest';
import { gradientImage, noiseImage, solidImage } from './generate';
describe('solidImage', () => {
it('заливает весь холст заданным цветом', () => {
const out = solidImage(2, 2, [10, 20, 30, 255]);
expect(out.width).toBe(2);
expect([...out.data]).toEqual(new Array(4).fill([10, 20, 30, 255]).flat());
});
it.each([
[0, 10],
[10, 0],
[2.5, 10],
[-1, 5]
])('бросает ошибку на размерах %i x %i', (w, h) => {
expect(() => solidImage(w, h, [0, 0, 0, 255])).toThrow();
});
});
describe('noiseImage', () => {
it('детерминирован: одно зерно — одни байты', () => {
expect([...noiseImage(4, 4, 42).data]).toEqual([...noiseImage(4, 4, 42).data]);
});
it('разные зерна дают разные данные', () => {
const a = [...noiseImage(8, 8, 1).data];
const b = [...noiseImage(8, 8, 2).data];
expect(a).not.toEqual(b);
});
it('альфа всегда непрозрачная', () => {
const data = noiseImage(3, 3, 7).data;
for (let i = 3; i < data.length; i += 4) {
expect(data[i]).toBe(255);
}
});
});
describe('gradientImage', () => {
it('горизонтальный градиент идёт от цвета A к цвету B', () => {
const out = gradientImage(
3,
1,
[0, 0, 0, 255],
[255, 255, 255, 255],
'horizontal'
);
const px = (x: number) => [...out.data.slice(x * 4, x * 4 + 4)];
expect(px(0)).toEqual([0, 0, 0, 255]);
expect(px(1)).toEqual([128, 128, 128, 255]);
expect(px(2)).toEqual([255, 255, 255, 255]);
});
it('вертикальный градиент меняется по строкам', () => {
const out = gradientImage(
1,
2,
[0, 0, 0, 255],
[100, 100, 100, 255],
'vertical'
);
expect(out.data[0]).toBe(0);
expect(out.data[4]).toBe(100);
});
});
+74
View File
@@ -0,0 +1,74 @@
import type { PixelImage } from './types';
export function solidImage(
width: number,
height: number,
rgba: [number, number, number, number]
): PixelImage {
if (!Number.isInteger(width) || !Number.isInteger(height) || width < 1 || height < 1) {
throw new Error('Размеры должны быть целыми числами >= 1');
}
const data = new Uint8ClampedArray(width * height * 4);
for (let i = 0; i < data.length; i += 4) {
data[i] = rgba[0];
data[i + 1] = rgba[1];
data[i + 2] = rgba[2];
data[i + 3] = rgba[3];
}
return { width, height, data };
}
export function noiseImage(width: number, height: number, seed: number): PixelImage {
if (!Number.isInteger(width) || !Number.isInteger(height) || width < 1 || height < 1) {
throw new Error('Размеры должны быть целыми числами >= 1');
}
const random = mulberry32(seed);
const data = new Uint8ClampedArray(width * height * 4);
for (let i = 0; i < data.length; i += 4) {
data[i] = Math.floor(random() * 256);
data[i + 1] = Math.floor(random() * 256);
data[i + 2] = Math.floor(random() * 256);
data[i + 3] = 255;
}
return { width, height, data };
}
export function gradientImage(
width: number,
height: number,
fromRgba: [number, number, number, number],
toRgba: [number, number, number, number],
direction: 'horizontal' | 'vertical'
): PixelImage {
if (!Number.isInteger(width) || !Number.isInteger(height) || width < 1 || height < 1) {
throw new Error('Размеры должны быть целыми числами >= 1');
}
const out: PixelImage = {
width,
height,
data: new Uint8ClampedArray(width * height * 4)
};
const steps = (direction === 'horizontal' ? width : height) - 1;
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const t = steps === 0 ? 0 : (direction === 'horizontal' ? x : y) / steps;
const i = (y * width + x) * 4;
out.data[i] = fromRgba[0] + (toRgba[0] - fromRgba[0]) * t;
out.data[i + 1] = fromRgba[1] + (toRgba[1] - fromRgba[1]) * t;
out.data[i + 2] = fromRgba[2] + (toRgba[2] - fromRgba[2]) * t;
out.data[i + 3] = fromRgba[3] + (toRgba[3] - fromRgba[3]) * t;
}
}
return out;
}
function mulberry32(seed: number): () => number {
let a = seed >>> 0;
return () => {
a |= 0;
a = (a + 0x6d2b79f5) | 0;
let t = Math.imul(a ^ (a >>> 15), 1 | a);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}