Files
SnapOtter 0b6a970912 feat: add color blindness simulation image-engine operation
Add colorBlindness() operation with 8 simulation matrices (Vienot/Machado)
for protanopia, deuteranopia, tritanopia, protanomaly, deuteranomaly,
tritanomaly, achromatopsia, and blue cone monochromacy.
2026-05-08 15:11:36 +08:00

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);
});
});