mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
Add colorBlindness() operation with 8 simulation matrices (Vienot/Machado) for protanopia, deuteranopia, tritanopia, protanomaly, deuteranomaly, tritanomaly, achromatopsia, and blue cone monochromacy.
88 lines
2.8 KiB
TypeScript
88 lines
2.8 KiB
TypeScript
import type { ColorBlindnessType } from "@snapotter/image-engine";
|
|
import { COLOR_BLINDNESS_MATRICES, colorBlindness } from "@snapotter/image-engine";
|
|
import sharp from "sharp";
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
const ALL_TYPES: ColorBlindnessType[] = [
|
|
"protanopia",
|
|
"deuteranopia",
|
|
"tritanopia",
|
|
"protanomaly",
|
|
"deuteranomaly",
|
|
"tritanomaly",
|
|
"achromatopsia",
|
|
"blueConeMonochromacy",
|
|
];
|
|
|
|
function makeColorImage(r: number, g: number, b: number): sharp.Sharp {
|
|
return sharp({
|
|
create: { width: 10, height: 10, channels: 3, background: { r, g, b } },
|
|
}).png();
|
|
}
|
|
|
|
describe("Color blindness matrices", () => {
|
|
it("all 8 types have a valid 3x3 matrix", () => {
|
|
for (const type of ALL_TYPES) {
|
|
const matrix = COLOR_BLINDNESS_MATRICES[type];
|
|
expect(matrix).toBeDefined();
|
|
expect(matrix).toHaveLength(3);
|
|
for (const row of matrix) {
|
|
expect(row).toHaveLength(3);
|
|
for (const val of row) {
|
|
expect(Number.isFinite(val)).toBe(true);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
it("matrix row sums are in a reasonable range (0 to 1.5)", () => {
|
|
for (const type of ALL_TYPES) {
|
|
const matrix = COLOR_BLINDNESS_MATRICES[type];
|
|
for (const row of matrix) {
|
|
const sum = row[0] + row[1] + row[2];
|
|
expect(sum).toBeGreaterThanOrEqual(0);
|
|
expect(sum).toBeLessThanOrEqual(1.5);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("colorBlindness operation", () => {
|
|
it("returns a Sharp instance for each type", async () => {
|
|
for (const type of ALL_TYPES) {
|
|
const result = await colorBlindness(makeColorImage(255, 0, 0), { type });
|
|
const buf = await result.toBuffer();
|
|
expect(buf.length).toBeGreaterThan(0);
|
|
}
|
|
});
|
|
|
|
it("different types produce different outputs on a red image", async () => {
|
|
const outputs = new Map<string, Buffer>();
|
|
for (const type of ALL_TYPES) {
|
|
const result = await colorBlindness(makeColorImage(255, 0, 0), { type });
|
|
const { data } = await result.removeAlpha().raw().toBuffer({ resolveWithObject: true });
|
|
outputs.set(type, Buffer.from(data));
|
|
}
|
|
const uniqueOutputs = new Set([...outputs.values()].map((b) => `${b[0]},${b[1]},${b[2]}`));
|
|
expect(uniqueOutputs.size).toBeGreaterThan(1);
|
|
});
|
|
|
|
it("achromatopsia produces grayscale output (R === G === B)", async () => {
|
|
const result = await colorBlindness(makeColorImage(255, 0, 0), {
|
|
type: "achromatopsia",
|
|
});
|
|
const { data } = await result.removeAlpha().raw().toBuffer({ resolveWithObject: true });
|
|
expect(data[0]).toBe(data[1]);
|
|
expect(data[1]).toBe(data[2]);
|
|
});
|
|
|
|
it("preserves image dimensions", async () => {
|
|
const result = await colorBlindness(makeColorImage(100, 150, 200), {
|
|
type: "deuteranomaly",
|
|
});
|
|
const meta = await result.metadata();
|
|
expect(meta.width).toBe(10);
|
|
expect(meta.height).toBe(10);
|
|
});
|
|
});
|