Convert from relative luminance to HSL
Asked Answered
M

1

6

Given a certain color in HSL (let's say hsl(74,64%,59%)), I want to calculate what darker shade (with the same h and s values) gives me enough contrast to satisfy W3C color contrast requirements.

There are formulas to convert HSL to RGB (for example https://en.wikipedia.org/wiki/HSL_and_HSV#HSL_to_RGB) and to calculate the relative luminance from that RGB (for example https://www.w3.org/TR/WCAG20/#relativeluminancedef). Based on the color contrast formula (https://www.w3.org/TR/WCAG20/#contrast-ratiodef) I can calculate what the relative luminance for my other color should be.

However, then I'm stuck. I find no way to calculate back from a given relative luminance, to an HSL color with given h and s.

Using tools like https://contrast-ratio.com/ I can just decrease the lightness until it satisfies the requirements, but I would like a formula (preferably in JavaScript) to do this calculation for a large selection of colors.

(I am currently using a binary search method to find the closest value, by testing many conversions from HSL to RGB to relative lightness, but that is quite intensive plus I wonder if the conversion to RGB in between introduces inaccuracies.)

Misprision answered 30/4, 2020 at 13:57 Comment(0)
P
2

Hope this is what you need

Using the formulas in this SO answer, and below:

// Relative luminance calculations
function adjustGamma(p) {
    if (p <= 0.03928) {
        return p / 12.92;
    } else {
        return Math.pow( ( p + 0.055 ) / 1.055, 2.4 );
    }
}

function relativeLuminance(rgb) {
    const r = adjustGamma( rgb[0] / 255 );
    const g = adjustGamma( rgb[1] / 255 );
    const b = adjustGamma( rgb[2] / 255 );
    return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}

// Contrast calculations
function contrastRatio(a,b) {
    const ratio = (a + 0.05) / (b + 0.05);
    return ratio >= 1 ? ratio : 1 / ratio;
}

// Loop for correct lightness
function rgbFromHslContrast(h, s, l1, ratio) {
    var inc = -0.01;
    var l2 = ( ( l1 + 0.05 ) / ratio - 0.05 );
    if (l2 < 0) {
        l2 = ( ratio * ( l1 + 0.05 ) - 0.05 );
        inc = -inc;
    }
    while (contrastRatio(l1, relativeLuminance(hslToRgb(h, s, l2))) < ratio) {
        l2 += inc;
    }
    return hslToRgb(h, s, l2);
}

The function you want to call is:

const originalHslAsRgb = hslToRgb(0.2, 0.2, 0.2);
const l1 = relativeLuminance(originalHslAsRgb);
const contrastRgb = rgbFromHslContrast(0.2, 0.2, l1, 3.5) // 3.5 is minimum contrast factor we target for..
// [139, 149, 100]
// equivalent to hsl(72, 20%, 53%)
Pyrexia answered 12/5, 2020 at 21:3 Comment(2)
Thank you Michael, this did answer my question, even though you're basically doing the same as I was doing. (I mentioned I'm doing a binary search for the required result, while you are testing all results with .01 increments.)ButMisprision
...But I see in the comment at https://mcmap.net/q/1916183/-how-to-convert-hsl-hue-saturation-lightness-to-hslum-hue-saturation-luminance-and-hslum-rgb it's apparently not possible to calculate directly without going through RGB. One small comment to your code, I asked for HSL as result, so your line return hslToRgb(h,s,l2); at the end of rgbFromHslContrast could simply be return [h,s,l2]. Also note that l1 is relative luminance, while l2 is lightness. :)Misprision

© 2022 - 2024 — McMap. All rights reserved.