寻找具有不透明度的“等效”颜色

假设我有一个背景颜色与“丝带”运行在它的另一种纯色。现在,我希望丝带是部分透明,让一些细节混合通过,但仍然保持丝带的“相同的颜色”在背景上。

有没有一种方法可以(容易地)确定,对于给定的不透明度/alpha < 100% 的色带颜色,什么样的 RGB 值应该与背景上的100% 不透明度的颜色相同?

这有张照片。背景是 rgb(72, 28, 97)带状 rgb(45, 34, 70)。我想要一个 rgba(r, g, b, a)的丝带,使它看起来相同的这个纯色。

enter image description here

39899 次浏览

Color blending is just a linear interpolation per channel, right? So the math is pretty simple. If you have RGBA1 over RGB2, the effective visual result RGB3 will be:

r3 = r2 + (r1-r2)*a1
g3 = g2 + (g1-g2)*a1
b3 = b2 + (b1-b2)*a1

…where the alpha channel is from 0.0 to 1.0.

Sanity check: if the alpha is 0, is RGB3 the same as RGB2? Yes. If the alpha is 1, is RGB3 the same as RGB1? Yes.

If you locked down only the background color and final color, there are a large number of RGBA colors (infinite, in floating-point space) that could satisfy the requirements. So you have to pick either the color of the bar or the opacity level you want, and find out the value of the other.

Picking the Color Based on Alpha

If you know RGB3 (the final desired color), RGB2 (the background color), and A1 (how much opacity you want), and you are just looking for RGB1, then we can re-arrange the equations thusly:

r1 = (r3 - r2 + r2*a1)/a1
g1 = (g3 - g2 + g2*a1)/a1
b1 = (b3 - b2 + b2*a1)/a1

There are some color combinations which are theoretically possible, but impossible given the standard RGBA range. For example, if the background is pure black, the desired perceived color is pure white, and the desired alpha is 1%, then you would need:

r1 = g1 = b1 = 255/0.01 = 25500

…a super-bright white 100× brighter than any available.

Picking the Alpha Based on Colors

If you know RGB3 (the final desired color), RGB2 (the background color), and RGB1 (the color you have that you want to vary the opacity of), and you are just looking for A1, then we can re-arrange the equations thusly:

a1 = (r3-r2) / (r1-r2)
a1 = (g3-g2) / (g1-g2)
a1 = (b3-b2) / (b1-b2)

If these give different values, then you can't make it match exactly, but you can average the alphas to get as close as possible. For example, there's no opacity in the world that will let you put green over red to get blue.

From @Phrogz' answer:

r3 = r2 + (r1-r2)*a1
g3 = g2 + (g1-g2)*a1
b3 = b2 + (b1-b2)*a1

So:

r3 - r2 = (r1-r2)*a1
g3 - g2 = (g1-g2)*a1
b3 - b2 = (b1-b2)*a1

So:

r1 = (r3 - r2) / a1 + r2
g1 = (g3 - g2) / a1 + g2
b1 = (b3 - b2) / a1 + b2

Note you can pick any value of a1, and this will find the corresponding values of r1, g1, and b1 required. For example, picking an alpha of 1 tells you that you need RGB1 = RGB3, but picking an alpha of 0 gives no solution (obviously).

i made a LESS mixin using Phrogz' answer. you input:

  1. how the colour should look
  2. with a certain alpha
  3. on a given background (default being white)

Here's the code:

.bg_alpha_calc (@desired_colour, @desired_alpha, @background_colour: white) {
@r3: red(@desired_colour);
@g3: green(@desired_colour);
@b3: blue(@desired_colour);


@r2: red(@background_colour);
@g2: green(@background_colour);
@b2: blue(@background_colour);


// r1 = (r3 - r2 + r2 * a1) / a1
@r1: ( @r3 - @r2 + (@r2 * @desired_alpha) ) / @desired_alpha;
@g1: ( @g3 - @g2 + (@g2 * @desired_alpha) ) / @desired_alpha;
@b1: ( @b3 - @b2 + (@b2 * @desired_alpha) ) / @desired_alpha;


background-color: @desired_colour;
background-color: rgba(@r1, @g1, @b1, @desired_alpha);


}

Usage like so:

@mycolour: #abc;
@another_colour: blue;
.box_overlay {
// example:
.bg_alpha_calc (@mycolour, 0.97, @another_colour);
// or (for white bg) just:
.bg_alpha_calc (@mycolour, 0.97);
}

Obviously doesn't work for impossible combinations (as mentioned by Phrogz), that means only mild levels of transparency are supported. See how you go with it.

Thanks to Phrogz's and ephemer's great answers, here is a SASS function that automagically computes the best equivalent RGBA color.

You call it with the desired color and the existing background, and it will compute the best (meaning most transparent) equivalent RGBA color that gives the desired result within ±1/256 of each RGB component (due to rounding errors):

@function alphaize($desired-color, $background-color) {


$r1: red($background-color);
$g1: green($background-color);
$b1: blue($background-color);


$r2: red($desired-color);
$g2: green($desired-color);
$b2: blue($desired-color);


$alpha: 0;
$r: -1;
$g: -1;
$b: -1;


@while $alpha < 1 and ($r < 0 or $g < 0 or $b < 0
or $r > 255 or $g > 255 or $b > 255) {
$alpha: $alpha + 1/256;
$inv: 1 / $alpha;
$r: $r2 * $inv + $r1 * (1 - $inv);
$g: $g2 * $inv + $g1 * (1 - $inv);
$b: $b2 * $inv + $b1 * (1 - $inv);
}


@return rgba($r, $g, $b, $alpha);
}

I just tested it against a number of combinations (all the Bootswatch themes) and it works a treat, both for dark-on-light and light-on-dark results.

PS: If you need better than ±1/256 precision in the resulting color, you will need to know what kind of rounding algorithm browsers apply when blending rgba colors (I don't know if that is standardized or not) and add a suitable condition to the existing @while, so that it keeps increasing $alpha until it achieves the desired precision.

To expand on @Tobia's answer and allow for specifying the opacity more like transparentify:

@function rgbaMorph($desired-color, $background-color: rgb(255,255,255), $desired-alpha: 0) {


$r1: red($desired-color);
$g1: green($desired-color);
$b1: blue($desired-color);


$r2: red($background-color);
$g2: green($background-color);
$b2: blue($background-color);


$r: -1;
$g: -1;
$b: -1;


@if ($desired-alpha != 0) {
$r: ( $r1 - $r2 + ($r2 * $desired-alpha) ) / $desired-alpha;
$g: ( $g1 - $g2 + ($g2 * $desired-alpha) ) / $desired-alpha;
$b: ( $b1 - $b2 + ($b2 * $desired-alpha) ) / $desired-alpha;
}


@if (($desired-alpha == 0) or ($r < 0 or $g < 0 or $b < 0
or $r > 255 or $g > 255 or $b > 255)) {
//if alpha not attainable, this will find lowest alpha that is


$alpha: $desired-alpha;
@while $alpha < 1 and ($r < 0 or $g < 0 or $b < 0
or $r > 255 or $g > 255 or $b > 255) {
$alpha: $alpha + 1/256;
$inv: 1 / $alpha;
$r: $r1 * $inv + $r2 * (1 - $inv);
$g: $g1 * $inv + $g2 * (1 - $inv);
$b: $b1 * $inv + $b2 * (1 - $inv);
}
@debug "Color not attainable at opacity using alpha: " $alpha " instead of: " $desired-alpha;


$desired-alpha: $alpha;
}


@return rgba($r, $g, $b, $desired-alpha);
}

The most practical solution I found so far: just measure the resulting color with a color picker tool and then use the measured color for overlay. This method gives a perfect color match.

I've tried using different mixins but due to rounding error, I was still able to see the difference with a naked eye

I wrote a util that I can use from JSS to help me make a color transparent. It uses utility methods from material-ui which can be found here.
Here is the code for my utility class (in Typescript). Note that it is not possible to make some colors transparent while keeping their color the same as the target color.
Excuse my lack of codepen or runnable code. I hope someone finds this and gets some use out of it!

import {
decomposeColor,
recomposeColor,
ColorObject,
} from '@material-ui/core/styles/colorManipulator';


/**
* Take a non transparent color and create a transparent color that looks identical visually.
* Using formula from this SO post https://stackoverflow.com/questions/12228548/finding-equivalent-color-with-opacity
* Assuming a white background
* @param origColor The color you want to match.
* @param value Transparency value between 0 and 1. Cannot be too low because it is mathematically not possible (usually <0.2 but depends on the color)
*/
export const makeTransparent = (origColor: string, value: number): string => {
const origColorObj: ColorObject = decomposeColor(origColor);


if (value >= 1 || value <= 0)
throw new Error('makeTransparent: invalid value provided');


if (origColorObj.values[3] != null && origColorObj.values[3] !== 1)
throw new Error('makeTransparent: origColor cannot be transparent');


const newColorObj: ColorObject = {
type: 'rgba',
values: [0, 0, 0, value],
};


for (let i = 0; i < 3; i++) {
const rgbNum: number = (255 * value + origColorObj.values[i] - 255) / value;


if (rgbNum < 0 || rgbNum > 255)
throw new Error('makeTransparent: Transparency value too low');


newColorObj.values[i] = rgbNum;
}


return recomposeColor(newColorObj);
};