Created
June 21, 2022 00:34
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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