@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.
203 lines (202 loc) • 7.33 kB
JavaScript
import { contrastRatio, relativeLuminance } from "./analyze";
import { Vec3 } from "@fimbul-works/vec";
import { classifyColor } from "./analyze";
import { colorToString } from "./format";
import { estimateColorTemperature } from "./kelvin";
import { rgbToHSL } from "./hsl";
import { rgbToLAB } from "./lab";
import { simulateColorBlindness } from "./colorblind";
/**
* Provides comprehensive debug information about a color
* @param color Color to analyze
* @returns Detailed color information for debugging
*/
export function debugColor(color) {
const white = new Vec3(1, 1, 1);
const black = new Vec3(0, 0, 0);
const gray = new Vec3(0.5, 0.5, 0.5);
const hsl = rgbToHSL(color);
const lab = rgbToLAB(color);
const luminance = relativeLuminance(color);
const temperature = estimateColorTemperature(color);
return {
original: {
vec3: `Vec3(${color.x.toFixed(3)}, ${color.y.toFixed(3)}, ${color.z.toFixed(3)})`,
rgb: colorToString(color, { format: "rgb" }),
hex: colorToString(color, { format: "hex" }),
hsl: colorToString(color, { format: "hsl" }),
},
colorSpaces: {
rgb: {
r: Math.round(color.x * 255),
g: Math.round(color.y * 255),
b: Math.round(color.z * 255),
},
hsl: {
h: Math.round(hsl.x * 360),
s: Math.round(hsl.y * 100),
l: Math.round(hsl.z * 100),
},
lab: {
l: Math.round(lab.x),
a: Math.round(lab.y),
b: Math.round(lab.z),
},
},
characteristics: {
luminance,
temperature,
classification: classifyColor(color),
},
accessibility: {
contrastRatios: {
onWhite: contrastRatio(color, white),
onBlack: contrastRatio(color, black),
onGray: contrastRatio(color, gray),
},
wcag: {
AANormal: contrastRatio(color, white) >= 4.5 ||
contrastRatio(color, black) >= 4.5,
AAANormal: contrastRatio(color, white) >= 7 || contrastRatio(color, black) >= 7,
AALarge: contrastRatio(color, white) >= 3 || contrastRatio(color, black) >= 3,
AAALarge: contrastRatio(color, white) >= 4.5 ||
contrastRatio(color, black) >= 4.5,
},
},
colorBlindness: {
protanopia: colorToString(simulateColorBlindness(color, "protanopia"), {
format: "hex",
}),
deuteranopia: colorToString(simulateColorBlindness(color, "deuteranopia"), { format: "hex" }),
tritanopia: colorToString(simulateColorBlindness(color, "tritanopia"), {
format: "hex",
}),
achromatopsia: colorToString(simulateColorBlindness(color, "achromatopsia"), { format: "hex" }),
},
};
}
/**
* Validates if a color is within valid ranges for its color space
* and provides detailed validation information
* @param color Color to validate
* @param space Color space to validate against
* @returns Validation results with detailed information
*/
export function validateColorSpace(color, space = "RGB") {
const issues = [];
const warnings = [];
let inGamut = true;
const channelValidation = {};
switch (space) {
case "RGB": {
["R", "G", "B"].forEach((channel, i) => {
const value = color.xyz[i];
channelValidation[channel] = {
valid: value >= 0 && value <= 1,
value: value,
min: 0,
max: 1,
};
if (value < 0 || value > 1) {
issues.push(`${channel} channel value ${value} is outside valid range [0,1]`);
}
if (value < 0.001 && value > 0) {
warnings.push(`${channel} channel has very small positive value: ${value}`);
}
});
if (color.xyz.some((v) => !Number.isFinite(v))) {
issues.push("Color contains NaN or Infinity values");
}
break;
}
case "HSL": {
const hsl = rgbToHSL(color);
channelValidation.H = {
valid: Number.isFinite(hsl.x),
value: hsl.x * 360,
min: 0,
max: 360,
};
if (!Number.isFinite(hsl.x)) {
issues.push("Hue value is invalid (NaN or Infinity)");
}
channelValidation.S = {
valid: hsl.y >= 0 && hsl.y <= 1,
value: hsl.y,
min: 0,
max: 1,
};
if (hsl.y < 0 || hsl.y > 1) {
issues.push(`Saturation value ${hsl.y} is outside valid range [0,1]`);
}
channelValidation.L = {
valid: hsl.z >= 0 && hsl.z <= 1,
value: hsl.z,
min: 0,
max: 1,
};
if (hsl.z < 0 || hsl.z > 1) {
issues.push(`Lightness value ${hsl.z} is outside valid range [0,1]`);
}
// Special cases
if (hsl.y === 0 && hsl.x !== 0) {
warnings.push("Hue value is meaningless when saturation is 0");
}
if (hsl.z === 0 || hsl.z === 1) {
warnings.push("Saturation has no effect at lightness 0 or 1");
}
break;
}
case "LAB": {
const lab = rgbToLAB(color);
channelValidation.L = {
valid: lab.x >= 0 && lab.x <= 100,
value: lab.x,
min: 0,
max: 100,
};
if (lab.x < 0 || lab.x > 100) {
issues.push(`L* value ${lab.x} is outside valid range [0,100]`);
}
channelValidation.a = {
valid: lab.y >= -128 && lab.y <= 127,
value: lab.y,
min: -128,
max: 127,
};
if (lab.y < -128 || lab.y > 127) {
warnings.push(`a* value ${lab.y} is outside typical range [-128,127]`);
}
channelValidation.b = {
valid: lab.z >= -128 && lab.z <= 127,
value: lab.z,
min: -128,
max: 127,
};
if (lab.z < -128 || lab.z > 127) {
warnings.push(`b* value ${lab.z} is outside typical range [-128,127]`);
}
const [r, g, b] = color.xyz;
inGamut =
r >= -0.0001 &&
r <= 1.0001 &&
g >= -0.0001 &&
g <= 1.0001 &&
b >= -0.0001 &&
b <= 1.0001;
if (!inGamut) {
issues.push("Color is outside sRGB gamut");
}
break;
}
}
return {
valid: issues.length === 0,
issues,
warnings,
colorSpace: {
inGamut,
channelValidation,
},
};
}