208

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)' 
    }
Dharman
  • 30,962
  • 25
  • 85
  • 135
  • Interseting! You still want it to be visible right, not fully transparent? – Mrchief Jul 12 '11 at 23:25
  • 6
    Interesting question! There's an exact opposite question at http://stackoverflow.com/questions/2049230/convert-rgba-color-to-rgb , it might help – apscience Jul 12 '11 at 23:27
  • @Mrchief - Yes, I want the color to look the same when displayed over white, but be as transparent as possible. –  Jul 12 '11 at 23:28
  • @gladoscc - yeah, the conversion from `rgba-->rgb` is easy (`r = r + (255-r) * (1-a)`), and actually how I generated the example numbers. The conversion going the other way is giving me a headache :) –  Jul 12 '11 at 23:31
  • 1
    @e100 - The specific question that triggered this was http://stackoverflow.com/questions/6658350/get-css-inset-box-shadow-to-appear-on-top-of-inner-backgrounds. 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: http://jsfiddle.net/qwySW/2/. Plus I just found it to be a fascinating question :) –  Jul 13 '11 at 19:21
  • It'd be great if you could also specify the background color. Black or gray instead of just white, for example. – Steven Vachon Oct 02 '14 at 21:48

7 Answers7

195

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.

Guffa
  • 687,336
  • 108
  • 737
  • 1,005
  • 1
    Thank you! I was doing something similar (e.g. get `.404`) but couldn't quite get the numbers to work out. –  Jul 12 '11 at 23:48
  • 3
    FYI, posted final solution based on your answer in question –  Jul 12 '11 at 23:58
  • 2
    @templatetypedef: Converting te lowest component to alpha is scaling from the range `0..255` to `0..1`, and inverting. Using `1.0 - 152 / 255` would also work. Converting the color components is simply scaling from `n..255` to `0..255` where `n` is the lowest component. – Guffa Jul 14 '11 at 12:51
  • @Guffa could you please explain how to do that on a non-white background? – Christoph May 29 '13 at 10:23
  • 1
    @Christoph: The principle would be the same, but more complicated. Instead of just using the lowest component to calculate the alpha, you would calculate the lowest possible alpha for each component (i.e. what alpha value to use to get the right color when using either 0 or 255 as the component value), then use the highest of those alpha values. From that you can calculate what color value to use for each component with that alpha value to get the right color. Note that some color combinations (for example white on black background) will give 1.0 as the lowest possible alpha value to use. – Guffa May 29 '13 at 10:57
  • I didn't fully understand that. For even colors `rgb(x,x,x)` that's simple, I just compute `var alpha = (x - lowest(r,g,b)) / x`, but what with "uneven" colors? – Christoph May 29 '13 at 12:05
  • @Christoph: For each component you have to determine if you should use a darker or brighter value against the background, then determine what lowest alpha value you can use for each component. The highest of those is the lowest alpha that you can use all over. – Guffa Jun 02 '13 at 19:22
  • 4
    I wrote a small fiddle that implements this solution. Here is the link. http://jsfiddle.net/wb5fwLoc/1/. Maybe one of you can use it. It's just a quick, not bug-free script.. it should be good enough for play around. – chsymann Dec 15 '14 at 14:25
24

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.

cobbal
  • 69,903
  • 20
  • 143
  • 156
gereeter
  • 4,731
  • 1
  • 28
  • 28
  • If you distribute the multiplied 255 in your equation you get the right result - `[255 * (r/255 - 1) / a + 1 * 255, ...] == [(255*r/255 - 255 * 1) / a + 255, ...] == [(r - 255) / a + 255, ...]` – gereeter Jul 13 '11 at 14:37
6

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
    );
}
A1rPun
  • 16,287
  • 7
  • 57
  • 90
Stefan
  • 1,028
  • 1
  • 9
  • 7
6

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

lxa
  • 3,234
  • 2
  • 30
  • 31
3

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.

cobbal
  • 69,903
  • 20
  • 143
  • 156
Kerrek SB
  • 464,522
  • 92
  • 875
  • 1,084
0

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.

  • `#800000` and `rgba( 255, 0, 0, .5 )` are nowhere near the same color. The first would be dark red, the second salmon-ish. Unless displaying over black, then I think they'd be the same. –  Oct 08 '14 at 19:06
0

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
trutheality
  • 23,114
  • 6
  • 54
  • 68
  • I actually just noticed that I had an error in the alpha calc (fixed now). So that might have had something to do with it too. – trutheality Jul 13 '11 at 18:35
  • There's still a problem introduced by your `y` and `(y-x)`. The `255 / (y-x)` incorrectly forces the largest number to `255`, so you'd always end up with a single `0`, a single `255` and then another number: `0, 22, 255`, `255, 0, 55`, etc... –  Jul 13 '11 at 19:51
  • I see, so it won't allow for darker colors... I should just make it `/a` and then it matches the correct answer. – trutheality Jul 13 '11 at 23:41