Прежде чем я продолжу, как я пытался это сделать, немного предыстории.
WCAG предоставляет метод, который вычисляет яркость как часть вычисление коэффициента контрастности.
То, что я пытаюсь сделать, — это дать цвет в пространстве RGB, преобразовать его в пространство HSL и настроить значение L так, чтобы оно соответствовало яркости другого цвета (в моем случае я выбрал #808080).
Идея состоит в том, чтобы сделать переходы между затемнением / осветлением более равномерными.
Вот мой код, в котором я выполняю вычисления вместе с преобразованиями.
// https://www.w3.org/TR/WCAG20-TECHS/G17.html
/**
* RGB Values. Each number is a number between 0.0 to 1.0
*/
type Rgb = { r: number; g: number; b: number };
/**
* HSL Values.
* @property h hue 0 to 360
* @property s saturation 0.0 to 1.0
* @property l luminiance 0.0 to 1.0
*/
type Hsl = { h: number; s: number; l: number };
/**
* Given a sRGB hex code convert to RGB
* @param hex
* @returns
*/
const hexToRgb = (hex: string): Rgb => {
const match = hex
.toLowerCase()
.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/);
return {
r: parseInt(match![1], 16) / 255.0,
g: parseInt(match![2], 16) / 255.0,
b: parseInt(match![3], 16) / 255.0,
};
};
/**
*
* @param rgb
* @returns
*/
const rgbToHex = (rgb: Rgb): string => {
return (
"#" +
Math.round(rgb.r * 255.0).toString(16).padStart(2, "0") +
Math.round(rgb.g * 255.0).toString(16).padStart(2, "0") +
Math.round(rgb.b * 255.0).toString(16).padStart(2, "0")
);
};
const luminance = (rgb: Rgb): number => {
const r =
rgb.r <= 0.03928 ? rgb.r / 12.92 : Math.pow((rgb.r + 0.055) / 1.055, 2.4);
const g =
rgb.g <= 0.03928 ? rgb.g / 12.92 : Math.pow((rgb.g + 0.055) / 1.055, 2.4);
const b =
rgb.b <= 0.03928 ? rgb.b / 12.92 : Math.pow((rgb.b + 0.055) / 1.055, 2.4);
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
};
const contrastRatio = (c1: Rgb, c2: Rgb): number => {
const l1 = luminance(c1);
const l2 = luminance(c2);
if (l1 > l2) {
return (l1 + 0.05) / (l2 + 0.05);
} else {
return (l2 + 0.05) / (l1 + 0.05);
}
};
/**
* https://en.wikipedia.org/wiki/HSL_and_HSV#HSL_to_RGB_alternative
* @param hsl
* @returns
*/
const hslToRgb = (hsl: Hsl): Rgb => {
const f = (n: number): number => {
const k = (n + hsl.h / 30) % 12;
const a = hsl.s * Math.min(hsl.l, 1 - hsl.l);
return hsl.l - a * Math.max(-1, Math.min(k - 3, 9 - k, 1));
};
return {
r: f(0),
g: f(8),
b: f(4),
};
};
const rgbToHsl = (rgb: Rgb): Hsl => {
// Minimum and Maximum RGB values are used in the HSL calculations
const min = Math.min(rgb.r, Math.min(rgb.g, rgb.b));
const max = Math.max(rgb.r, Math.max(rgb.g, rgb.b));
// Calculate the Hue
let h;
if (max === min) {
h = 0;
} else if (max === rgb.r) {
h = ((60 * (rgb.g - rgb.b)) / (max - min) + 360) % 360;
} else if (max === rgb.g) {
h = (60 * (rgb.b - rgb.r)) / (max - min) + 120;
} else if (max === rgb.b) {
h = (60 * (rgb.r - rgb.g)) / (max - min) + 240;
} else {
throw "unexpected";
}
// Calculate the Luminance
const l = (max + min) / 2;
// Calculate the Saturation
let s: number;
if (max == min) {
s = 0;
} else if (l <= 0.5) {
s = (max - min) / (max + min);
} else {
s = (max - min) / (2 - max - min);
}
return { h, s, l };
};
/**
* Given an RGB value, find the luminance value for HSL where
* the luminance matches that of gray.
*/
const hslWithIdealLuminance = (rgb: Rgb): Hsl => {
const grayRgb = { r: 0.5, g: 0.5, b: 0.5 };
const grayLuminance = luminance(grayRgb);
let delta = 0.1;
let currentLuminance = luminance(rgb);
const hsl = rgbToHsl(rgb);
while (delta > 0.0000001) {
console.log(rgbToHex(rgb), currentLuminance, grayLuminance, delta);
if (currentLuminance > grayLuminance) {
while (currentLuminance > grayLuminance) {
hsl.l -= delta;
currentLuminance = luminance(hslToRgb(hsl));
}
}
else {
while (currentLuminance < grayLuminance) {
hsl.l += delta;
currentLuminance = luminance(hslToRgb(hsl));
}
}
delta /= 10;
}
return hsl;
};
export const luminanceHex = (hex: string): number => {
return luminance(hexToRgb(hex));
};
export const normalizeLuminance = (hex: string): string => {
return rgbToHex(hslToRgb(hslWithIdealLuminance(hexToRgb(hex))));
};
export const contrastRatioHex = (c1:string, c2:string): number => {
return contrastRatio(hexToRgb(c1), hexToRgb(c2));
}
Проблемная функция hslWithIdealLuminance где я ищу оптимальное значение, я думаю, что может быть какой-то способ сделать это менее грубой силой. Я, вероятно, смогу еще немного оптимизировать цикл, но я бы предпочел избавиться от него. Если я смогу свести это к математической функции, я, вероятно, смогу объединить всю эту логику в SASS.

