Convert RGB to RGBA over white

JavascriptColorsCssRgba

Javascript Problem Overview


I have a hex color, e.g. #F4F8FB (or rgb(244, 248, 251)) that I want converted into an as-transparent-as-possible rgba color (when displayed over white). Make sense? I'm looking for an algorithm, or at least idea of an algorithm for how to do so.

For Example:

rgb( 128, 128, 255 ) --> rgba(   0,   0, 255,  .5 )
rgb( 152, 177, 202 ) --> rgba(  50, 100, 150,  .5 ) // can be better(lower alpha)

Ideas?


FYI solution based on Guffa's answer:

function RGBtoRGBA(r, g, b){
    if((g == null) && (typeof r === 'string')){
        var hex = r.replace(/^\s*#|\s*$/g, '');
        if(hex.length === 3){
            hex = hex.replace(/(.)/g, '$1$1');
        }
        r = parseInt(hex.substr(0, 2), 16);
        g = parseInt(hex.substr(2, 2), 16);
        b = parseInt(hex.substr(4, 2), 16);
    }

    var min, a = (255 - (min = Math.min(r, g, b))) / 255;

    return {
        r    : r = 0|(r - min) / a,
        g    : g = 0|(g - min) / a,
        b    : b = 0|(b - min) / a,
        a    : a = (0|1000*a)/1000,
        rgba : 'rgba(' + r + ', ' + g + ', ' + b + ', ' + a + ')'
    };
}

RGBtoRGBA(204, 153, 102) == RGBtoRGBA('#CC9966') == RGBtoRGBA('C96') == 
    {
       r    : 170,
       g    : 85 ,
       b    : 0  ,
       a    : 0.6,
       rgba : 'rgba(170, 85, 0, 0.6)' 
    }

Javascript Solutions


Solution 1 - Javascript

Take the lowest color component, and convert that to an alpha value. Then scale the color components by subtracting the lowest, and dividing by the alpha value.

Example:

152 converts to an alpha value of (255 - 152) / 255 ~ 0.404

152 scales using (152 - 152) / 0.404 = 0
177 scales using (177 - 152) / 0.404 ~ 62
202 scales using (202 - 152) / 0.404 ~ 123

So, rgb(152, 177, 202) displays as rgba(0, 62, 123, .404).

I have verified in Photoshop that the colors actually match perfectly.

Solution 2 - Javascript

Let r, g, and b be the input values and r', g', b', and a' be the output values, all scaled (for now, as it makes the math prettier) between 1 and 0. Then, by the formula for overlaying colors:

r = a' * r' + 1 - a'
g = a' * g' + 1 - a'
b = a' * b' + 1 - a'

The 1 - a' terms represent the background contribution, and the other terms represent the foreground. Do some algebra:

r = a' * (r' - 1) + 1
r - 1 = a' * (r' - 1)
(r - 1) / (r' - 1) = a'
(r' - 1) / (r - 1) = 1 / a'
r' - 1 = (r - 1) / a'
r' = (r - 1) / a' + 1

Intuitively, it seems that the minimum color value will be the limiting factor in the problem, so bind this to m:

m = min(r, g, b)

Set the corresponding output value, m', to zero, as we want to maximize transparency:

0 = (m - 1) / a' + 1
-1 = (m - 1) / a'
-a' = m - 1
a' = 1 - m

So, in javascript (translating from 1 to 255 along the way):

function rgba(r, g, b) {
    var a = 1 - Math.min(r, Math.min(g, b)) / 255;
    return [255 + (r - 255) / a, 255 + (g - 255) / a, 255 + (b - 255) / a, a];
}

Note that I'm assuming that a' is opacity here. It is trivial to change it to transparency - just remove the "1 -" from the formula for a'. Anothing thing to note is that this does not seem to produce exact results - it said that the opacity was 0.498 for the example you gave above (128, 128, 255). However, this is extremely close.

Solution 3 - Javascript

I'd look to RGB<->HSL conversion. I.e. luminosity == amount of white == amount of transparency.

For your example rgb( 128, 128, 255 ), we need to shift RGB values to 0 first by maximum amount, i.e. to rgb( 0, 0, 128 ) - that would be our color with as few of white as possible. And after that, using formula for luminance, we calculate amount of white we need to add to our dark color to get original color - that would be our alpha:

L = (MAX(R,G,B) + MIN(R,G,B))/2
L1 = (255 + 128) / 2 = 191.5
L2 = (128 + 0) /2 = 64
A = (191,5 - 64) / 255 = 0,5;

Hope that makes sense. :)

Solution 4 - Javascript

For those of you using SASS/SCSS, I've wrote a small SCSS function so you can easily use the algorithm described by @Guffa

@function transparentize-on-white($color)
{
    $red: red($color);
	$green: green($color);
    $blue: blue($color);
    $lowestColorComponent: min($red, $green, $blue);
	$alpha: (255 - $lowestColorComponent) / 255;

    @return rgba(
	    ($red - $lowestColorComponent) / $alpha,
	    ($green - $lowestColorComponent) / $alpha,
	    ($blue - $lowestColorComponent) / $alpha,
	    $alpha
    );
}

Solution 5 - Javascript

I'm just describing an idea for the algorithm, no full solution:

Basically, you have three numbers x, y, z and you are looking for three new numbers x', y', z' and a multiplier a in the range [0,1] such that:

x = a + (1 - a) x'
y = a + (1 - a) y'
z = a + (1 - a) z'

This is written in units where the channels also take values in the range [0,1]. In 8bit discrete values, it'd be something like this:

x = 255 a + (1 - a) x'
y = 255 a + (1 - a) y'
z = 255 a + (1 - a) z'

Moreover, you want the largest possible value a. You can solve:

a  = (x - x')/(255 - x')          x' = (x - 255 a)/(1 - a)

Etc. In real values this has infinitely many solutions, just plug in any real number a, but the problem is to find a number for which the discretization error is minimal.

Solution 6 - Javascript

This should do it:

let x = min(r,g,b)
a = 1 - x/255                    # Correction 1
r,g,b = ( (r,g,b) - x ) / a      # Correction 2

Solution 7 - Javascript

The top answer didn't work for me with low color components. For example it does not calculate correct alpha if color is #80000. Technically it should make it into #ff0000 with alpha 0.5. To resolve this, you need to use RGB -> HSL -> RGBA conversion. This is pseudo code to get the correct values:

//Convert RGB to HSL
hsl = new HSL(rgb)

//Use lightness as alpha
alpha = hsl.Lightness

//For #80000 lightness is 0.5, so we have to take this into account.
//Lightness more than 0.5 converts to alpha 1 and below is a linear ratio
if (alpha > 0.5)
{
    alpha = 1;
}
else
{
    alpha = alpha / 0.5;
    //We need to drop the lightness of the color to 0.5 to get the actual color
    //that needs to display. Alpha blending will take care of that.
    hsl.Lightness = 0.5;
}

newRgb = hsl.convertToRgb()

"newRgb" will contain the value of the new adjusted color and use "alpha" variable to control the transparency.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionNoneView Question on Stackoverflow
Solution 1 - JavascriptGuffaView Answer on Stackoverflow
Solution 2 - JavascriptgereeterView Answer on Stackoverflow
Solution 3 - JavascriptlxaView Answer on Stackoverflow
Solution 4 - JavascriptStefanView Answer on Stackoverflow
Solution 5 - JavascriptKerrek SBView Answer on Stackoverflow
Solution 6 - JavascripttruthealityView Answer on Stackoverflow
Solution 7 - JavascriptArvydas JuskeviciusView Answer on Stackoverflow