UNPKG

@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
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, }; }