feat: add registry
This commit is contained in:
@@ -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,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';
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user