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)'
}
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 :)
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.
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.
[255 * (r/255 - 1) / a + 1 * 255, ...] == [(255*r/255 - 255 * 1) / a + 255, ...] == [(r - 255) / a + 255, ...]
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. :)
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
);
}
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.
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
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...
/a
and then it matches the correct answer.
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.
Success story sharing
.404
) but couldn't quite get the numbers to work out.0..255
to0..1
, and inverting. Using1.0 - 152 / 255
would also work. Converting the color components is simply scaling fromn..255
to0..255
wheren
is the lowest component.