201
votes

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)' 
    }
7
Interseting! You still want it to be visible right, not fully transparent?Mrchief
Interesting question! There's an exact opposite question at stackoverflow.com/questions/2049230/convert-rgba-color-to-rgb , it might helpapscience
@Mrchief - Yes, I want the color to look the same when displayed over white, but be as transparent as possible.Mark Kahn
Great question, I've always wondered how to do this, but usually shied away from it ;) +1Dominic K
@e100 - The specific question that triggered this was stackoverflow.com/questions/6658350/…. Basically I wanted to be able to overlay CSS shadows on top of elements with a background-color without blocking the click-through by putting a shadow element on top of everything. I've also wanted to do this for a while so I can do random things like: jsfiddle.net/qwySW/2. Plus I just found it to be a fascinating question :)Mark Kahn

7 Answers

189
votes

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.

25
votes

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.

6
votes

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. :)

6
votes

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
    );
}
3
votes

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.

0
votes

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
-1
votes

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.