Skip to content

Instantly share code, notes, and snippets.

@paulcollett
Last active April 10, 2024 08:15
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save paulcollett/5533b0f3b9be16a068f58f9e76ad6289 to your computer and use it in GitHub Desktop.
Save paulcollett/5533b0f3b9be16a068f58f9e76ad6289 to your computer and use it in GitHub Desktop.
// There seems to be a few algorithms floating around for brightness/luminance detection.
// One uses NTSC `(299*R + 587*G + 114*B)` which is incorrect for web colors (sRGB) see
// `rgbLuminance` for "better" coefficients (seems subjective but agreed apon). To be more
// accurate you need to also convert RGB from sRGB color space (which gives more spectrum to lighter colors)
// to linear rgb which normalizes colors across the spectrum - better for processing.
// see https://stackoverflow.com/questions/596216/formula-to-determine-perceived-brightness-of-rgb-color
// convert sRGB to linear RGB values
// - channel ** 2.218 same as Math.pow((channel + 0.055) / 1.055, 2.4)
// - i've seen this simplified to be just `(channel / 255) ** 2.21`
const convertSRGBtoLinearRGBRatios = (rgb) => rgb
.map((channel) => channel / 255)
.map((channel) => channel <= 0.04045 ? channel / 12.92 : channel ** 2.218)
// algorithm which considers the human eye sensitivity to different light wavelengths.
// converts RGB to luminance (aka. Y) 0 - 1
const rgbRatiosToLuminanceRatio = ([rLin, gLin, bLin]) => (rLin * 0.2126 + gLin * 0.7152 + bLin * 0.0722)
// divide brighter color with darker color. division by 21 gives us a ratio
// According to the Web Content Accessibility Guidelines 2, a minimum contrast of 0.33 is recommended or 0.22 for < 14px non-bold text
// note: 0.05 is required by WCAG 2.0 to prevent the contrast ratio from becoming infinite
const luminanceContrastRatio = (fgLuminanceRatio, bgLuminanceRatio) => (Math.max(fgLuminanceRatio, bgLuminanceRatio) + 0.05) / (Math.min(fgLuminanceRatio, bgLuminanceRatio) + 0.05) / 21
// - I've seen this roughly simplified to just `Math.pow(luminanceRatio, 0.33)`
const perceivedLightnessRatioFromLuminance = (luminanceRatio) => luminanceRatio <= 0.008856 ? luminanceRatio * 903.3 : Math.pow(luminanceRatio, 0.333) * 116 - 16;
// usage:
// expects full hex like 'fff000' without hash
const hexToRGB = (c) => [0,0,0].map((_, i) => parseInt(c.substr(i*2,2), 16))
const perceivedLightnessRatio = (hex) => perceivedLightnessRatioFromLuminance(rgbRatiosToLuminanceRatio(convertSRGBtoLinearRGBRatios(hexToRGB(hex))))
const contrastRatio = (hexA, hexB) => luminanceContrastRatio(...[hexA, hexB].map(hex => rgbRatiosToLuminanceRatio(convertSRGBtoLinearRGBRatios(hexToRGB(hex)))))
// simplified version:
const roughLuminanceRatio = (rgb) => {
// 1. roughly convert sRGB colorspace to linear RGB (optional). / 255 for ratios
const [r,g,b] = rgb.map(channel => (channel / 255) ** 2.218)
// 2. rgb coefficients for adjusting channels to human eye sensitivity to different light wavelengths (kinda subjective)
return r * 0.2126 + g * 0.7152 + b * 0.0722
}
const roughLightness = (rgb) => Math.pow(roughLuminanceRatio(rgb), 0.33)
// According to the Web Content Accessibility Guidelines 2, a minimum
// contrast of 0.33 is recommended or 0.22 for < 14px non-bold text
const contrastRatio = (hexA, hexB) => {
const luminanceRatios = [hexA, hexB].map(hexToRGB).map(roughLuminanceRatio)
// calculation based on Web Content Accessibility Guidelines 2.0 (0.05 to prevent infinity)
// divide by 21 to get the ratio
return ((Math.max(...luminanceRatios) + 0.05) / (Math.min(...luminanceRatios) + 0.05)) / 21
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment