@fimbul-works/vec-color
Version:
A comprehensive, type-safe color manipulation library for TypeScript that provides a wide range of color space conversions, blending operations, and accessibility utilities.
136 lines (135 loc) • 5.18 kB
JavaScript
import { hslToRGB, rgbToHSL } from "./hsl";
import { Vec3 } from "@fimbul-works/vec";
import { calculateColorSimilarityLab } from "./analyze";
/**
* Simulation matrices for different types of color blindness
* Based on color blind simulation research by Brettel, Viénot, and Mollon
*/
const SIMULATION_MATRICES = {
protanopia: [
[0.567, 0.433, 0],
[0.558, 0.442, 0],
[0, 0.242, 0.758],
],
deuteranopia: [
[0.625, 0.375, 0],
[0.7, 0.3, 0],
[0, 0.3, 0.7],
],
tritanopia: [
[0.95, 0.05, 0],
[0, 0.433, 0.567],
[0, 0.475, 0.525],
],
achromatopsia: [
[0.299, 0.587, 0.114],
[0.299, 0.587, 0.114],
[0.299, 0.587, 0.114],
],
};
/**
* Simulates how a color would appear to someone with color blindness
* @param color Input color as Vec3 RGB
* @param type Type of color blindness to simulate
* @returns Simulated color as Vec3 RGB
*/
export function simulateColorBlindness(color, type) {
const matrix = SIMULATION_MATRICES[type];
return new Vec3(color.x * matrix[0][0] + color.y * matrix[0][1] + color.z * matrix[0][2], color.x * matrix[1][0] + color.y * matrix[1][1] + color.z * matrix[1][2], color.x * matrix[2][0] + color.y * matrix[2][1] + color.z * matrix[2][2]);
}
/**
* Checks if two colors are distinguishable for different types of color blindness
* @param color1 First color as Vec3 RGB
* @param color2 Second color as Vec3 RGB
* @param type Type of color blindness to check
* @param threshold Minimum difference threshold (0-1, default: 0.1)
* @returns Boolean indicating if colors are distinguishable
*/
export function isDistinguishableForColorBlindness(color1, color2, type, threshold = 0.1) {
const simulated1 = simulateColorBlindness(color1, type);
const simulated2 = simulateColorBlindness(color2, type);
const difference = Math.sqrt((simulated1.x - simulated2.x) ** 2 +
(simulated1.y - simulated2.y) ** 2 +
(simulated1.z - simulated2.z) ** 2);
return difference > threshold;
}
/**
* Optimizes a set of colors to be distinguishable for color blind users
* @param colors Array of colors to optimize
* @returns Optimized colors that maintain distinctiveness across color blindness types
*/
export function optimizeForColorBlindness(colors) {
const optimized = [];
const types = [
"protanopia",
"deuteranopia",
"tritanopia",
"achromatopsia",
];
if (colors.length > 0) {
optimized.push(colors[0]);
}
for (const color of colors.slice(1)) {
let bestColor = color;
let maxMinDifference = 0;
const hsl = rgbToHSL(color);
for (let hueShift = 0; hueShift < 360; hueShift += 15) {
for (let satAdjust = -0.2; satAdjust <= 0.2; satAdjust += 0.1) {
for (let lightAdjust = -0.2; lightAdjust <= 0.2; lightAdjust += 0.1) {
const testColor = hslToRGB(new Vec3(((hsl.x * 360 + hueShift) % 360) / 360, Math.max(0.1, Math.min(1, hsl.y + satAdjust)), Math.max(0.1, Math.min(0.9, hsl.z + lightAdjust))));
let minDifference = 1;
for (const type of types) {
const simulatedTest = simulateColorBlindness(testColor, type);
for (const existing of optimized) {
const simulatedExisting = simulateColorBlindness(existing, type);
const difference = 1 -
calculateColorSimilarityLab(simulatedTest, simulatedExisting);
minDifference = Math.min(minDifference, difference);
}
}
if (minDifference > maxMinDifference) {
maxMinDifference = minDifference;
bestColor = testColor;
}
}
}
}
optimized.push(bestColor);
}
return optimized;
}
/**
* Validates if a color palette is safe for color blind users
* @param colors Array of colors to validate
* @returns Validation result with detailed issues
*/
export function validateColorBlindnessSafety(colors) {
const types = [
"protanopia",
"deuteranopia",
"tritanopia",
"achromatopsia",
];
const issues = [];
const similarityThreshold = 0.1;
for (const type of types) {
const problematicPairs = [];
for (let i = 0; i < colors.length; i++) {
for (let j = i + 1; j < colors.length; j++) {
const color1 = simulateColorBlindness(colors[i], type);
const color2 = simulateColorBlindness(colors[j], type);
const difference = 1 - calculateColorSimilarityLab(color1, color2);
if (difference < similarityThreshold) {
problematicPairs.push([colors[i], colors[j]]);
}
}
}
if (problematicPairs.length > 0) {
issues.push({ type, problematicPairs });
}
}
return {
safe: issues.length === 0,
issues,
};
}