Skip to content

Instantly share code, notes, and snippets.

@lleyton
Created June 21, 2022 00:34
Show Gist options
  • Save lleyton/97d5833373bf17f8b3610f7c9843787a to your computer and use it in GitHub Desktop.
Save lleyton/97d5833373bf17f8b3610f7c9843787a to your computer and use it in GitHub Desktop.
Naïve method of "fixing" a foreground color to contrast with a background color
type RGB8bitColor = [number, number, number];
// Adapted from https://gist.github.com/jfsiii/5641126
// and from http://www.w3.org/TR/WCAG20/#relativeluminancedef
const relativeLuminanceW3C = (color: RGB8bitColor) => {
const RsRGB = color[0] / 255;
const GsRGB = color[1] / 255;
const BsRGB = color[2] / 255;
const R =
RsRGB <= 0.03928 ? RsRGB / 12.92 : Math.pow((RsRGB + 0.055) / 1.055, 2.4);
const G =
GsRGB <= 0.03928 ? GsRGB / 12.92 : Math.pow((GsRGB + 0.055) / 1.055, 2.4);
const B =
BsRGB <= 0.03928 ? BsRGB / 12.92 : Math.pow((BsRGB + 0.055) / 1.055, 2.4);
// For the sRGB colorspace, the relative luminance of a color is defined as:
const L = 0.2126 * R + 0.7152 * G + 0.0722 * B;
return L;
};
const contrastRatio = (luminance1: number, luminance2: number) =>
(Math.max(luminance1, luminance2) + 0.05) /
(Math.min(luminance1, luminance2) + 0.05);
// Adapted from https://github.com/gka/chroma.js
const interpolate = (
color1: RGB8bitColor,
color2: RGB8bitColor
): RGB8bitColor => [
Math.round(color1[0] + 0.5 * (color2[0] - color1[0])),
Math.round(color1[1] + 0.5 * (color2[1] - color1[1])),
Math.round(color1[2] + 0.5 * (color2[2] - color1[2])),
];
// Adapted from https://github.com/gka/chroma.js
const EPS = 1e-7;
const MAX_ITER = 20;
const adjustLuminance = (color: RGB8bitColor, target: number) => {
const cur_lum = relativeLuminanceW3C(color);
let max_iter = MAX_ITER;
const test = (low: RGB8bitColor, high: RGB8bitColor): RGB8bitColor => {
const mid = interpolate(low, high);
const lm = relativeLuminanceW3C(mid);
if (Math.abs(target - lm) < EPS || !max_iter--) {
// close enough
return mid;
}
return lm > target ? test(low, mid) : test(mid, high);
};
return cur_lum > target
? test([0, 0, 0], color)
: test(color, [255, 255, 255]);
};
const fixFgContrast = (bgColor: RGB8bitColor, fgColor: RGB8bitColor) => {
const bgLuminance = relativeLuminanceW3C(bgColor);
const fgLuminance = relativeLuminanceW3C(fgColor);
const ratio = contrastRatio(bgLuminance, fgLuminance);
if (ratio >= 7) {
return fgColor;
}
if (fgLuminance > bgLuminance) {
const denominator = bgLuminance + 0.05;
const targetLuminance = 7 * denominator - 0.05;
return adjustLuminance(fgColor, targetLuminance);
} else {
const numerator = bgLuminance + 0.05;
const targetLuminance = numerator / 7 - 0.05;
return adjustLuminance(fgColor, targetLuminance);
}
};
console.log(fixFgContrast([255, 255, 255], [246, 211, 45])); // [103, 89, 21] #675915, this has a contrast ratio of 6.96 close enough to the target of 7
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment