Operation between color with alpha and number #2694

Closed
SomMeri opened this Issue Sep 15, 2015 · 24 comments

Projects

None yet

5 participants

@SomMeri
Member
SomMeri commented Sep 15, 2015

I noticed that operation between color with alpha and number always erases alpha. Is that as it should be?

Example input:

color: rgba(4, 5, 6, 0.2) + 0.6;

output:

color: #050607;
@SomMeri
Member
SomMeri commented Sep 15, 2015

Another thing I am not sure about are operations on colors with alpha. No matter what operation, alpha is always calculated the same way.

color-1: rgba(4, 5, 6, 0.2) + rgba(4, 5, 6, 0.2);
color-2: rgba(4, 5, 6, 0.2) - rgba(1, 1, 1, 0.2);
color-3: rgba(4, 5, 6, 0.2) * rgba(4, 5, 6, 0.2);

Output:

color-1: rgba(8, 10, 12, 0.36);
color-2: rgba(3, 4, 5, 0.36);
color-3: rgba(16, 25, 36, 0.36);

Is that how it should be? I was thinking about describing color operations into docs and do not want to specify bugs in there.

@seven-phases-max
Member

I was trying to make it at the times of #1675 (see also #2291), but decided to give it up since I was not able to make it the way I want it to be (making all ops "reversible", e.g. so that A * B = C -> C / B = A etc., i.e. not using exactly the plain "simple alpha compositing" like it is in current blend funcs, though I tried to retain some compatibility to a certain degree... Probably my idea was too overengineered (If you're interested I'll share my prototype code next week - currently I'm away of my PC).

@matthew-dean
Member

It would seem intuitive to me that when adding or subtracting colors to create a resulting color, that alpha would be unchanged (0.2). However, when multiplying colors, the intuitive step might go in one of two directions. Multiplying the exact values is pretty much not ever useful, since you would get 0.04, and I don't see how anyone would actually want that. But I could see averaging:

alpha 0.2 * 0.2 = 0.2 
0.2 * 0.4 = 0.3
0.2 * 1.0 = 0.6

Or the second option that might make sense is to multiply them as if they're whole numbers (maxing out at 1.0 of course).

0.2 * 0.2 = 0.4
0.2 * 0.4 = 0.8
0.2 * 1.0 = 1.0

In that way, multiplying as an operation would be a type of "blend". It's intuitive because the number values for RGB increase when multiplying. While mathematically a different operation, it would feel weird to multiply colors, and have color values increase, and opacity decrease.

I don't think there's any right answer. Although I'm kinda curious what math makes 0.36 appear. o_O

@seven-phases-max
Member

It would seem intuitive to me that when adding or subtracting colors to create a resulting color, that alpha would be unchanged (0.2).

It seems to be quite common assumption (#2291), but in fact this does not have any reasonable rationale from an engineering point of view. E.g. alpha: 0.0 + 1.0 = 0.5? Based on what? (in our universe mixing fully transparent green to fully opaque red can't never produce semi-transparent yellow :)

@matthew-dean
Member

@seven-phases-max Treating colors as a simple matter of mathematics won't really work. Visual processing is way too complex for that. So the numbers, at best, vaguely approximate visual phenomena we're used to seeing.

In this case, I believe it's more like thinking what the intuitive result would be, and then work our way backwards into making that predictable and easy to understand. Multiplying opacities seems like it would make something more opaque. Adding is... tricky because it's....sort of like mixing two translucent sheets, but is that defined as "adding" or "multiplying"? And, if we're "adding" and describing that as some kind of physical material, what does it really mean to subtract? This is where your brain falls apart because that really doesn't make any sense. Computer color values and color functions are, to some degree, metaphors. The can have a formulated result, but that doesn't mean they have an exact parallel to either properties of light or properties of colored materials.

But not to get into that a bunch. Let's not break our brains or work too hard on it. This is really about making a decision that makes sense and, as @SomMeri said, being able to explain it in a way that the result will be intuitive / predictable.

@seven-phases-max
Member

So the numbers, at best, vaguely approximate visual phenomena we're used to seeing.

Yes, but it's not yet a reason to choose a random operation ("average"?) for the alpha compositing. "Simple alpha compositing", i.e. as in 0.2 + 0.2 = 0.36, is far from ideal (for the arithmetic ops), but it does have some rationale behind (by #1673 times it was considered to be just less weird than alpha: 1 + 1 -> 2).

For the particular snippet:

rgba(4, 5, 6, 0.2) + 0.6;

the result is opaque just because the right side operand is converted to fully opaque color before the addition, i.e. 0.6 -> rgba(255 * .6, 255 * .6, 255 * .6, 1.0) (in other words in this case scalar numbers are just shortcuts for non-transparent shades of gray).


This is where your brain falls apart because that really doesn't make any sense.

sqrt(-1) also seems like a madness from a "common sense" point of view but still is quite widely used to describe physical phenomenas :)


Either way to stress my position: I'm absolutely fine with 0.2 + 0.2 -> 0.2 as soon as anyone can explain why exactly it should result in 0.2
(though better if one could define and describe rgba(255, 0, 0, 1) + rgba(0, 255, 0, 0) -> rgba(255, 255, 0, ?)...).

@SomMeri
Member
SomMeri commented Sep 16, 2015

I tried to google colors arithmetic and it seem like nobody is doing it with alpha. All I found was about normal rgb colors.

@seven-phases-max I read this comment. What was your physically strict model/algorithm and why did multiplication and division became too difficult? Are they described somewhere? If it is specified somewhere I might give it a try.

If it is not specified anywhere and no library software does arithmetic on alpha, we may just put "alpha channel operations are underspecified because there is no common understanding about what they should do" and leave it at that until artists decide what might be useful for them.

@Synchro
Member
Synchro commented Sep 16, 2015

The problem is that you can't handle alphas in isolation. What colours you end up with depends on what's behind them, i.e. things that are not amongst the values available for calculation. For example rgba(255, 0, 0, 1) and rgba(255, 0, 0, 0) appear identical when placed over a background of rgb(255,0,0), but entirely different when over rgb(0,0,0), but since that information is not available when we only have the original colours in isolation, we can't produce a value that makes any sense because we have no context. That's why I gave up on handling this in the contrast function.

As far as analogies go, overlaying semi-transparent sheets is multiplication in a subtractive model - overlaying two 50% transparent sheets will result in 25% transparency; 0.5 * 0.5 = 0.25. It works very much like the unity gain approach in audio mixing. It gets confusing because there may be circumstances where subtractive (transparent sheets, inks) and additive (lights, glowing pixels) colour mixing models are used at the same time and it's hard to reconcile the two and stay sane. An addition operation is not a transparent sheet situation, so it makes no sense to think of the alpha as a transparency, but more as a 'contribution' factor. I think it should work like this if you're going to include alpha in the addition calculation:

rgba(255, 0, 0, 1) + rgba(0, 255, 0, 0) -> rgba(255, 0, 0, 1)

i.e. the second colour makes no contribution to the result because its values are reduced by its 'opacity', in this case 0. Similarly, this would make sense to me:

rgba(255, 0, 0, 1) + rgba(0, 255, 0, 0.5) -> rgba(255, 127, 0, 1)

There are obviously holes in that too, but I'm not sure how you would deal with mathematical oddities that don't have a real-life parallel!

I may just be thinking out loud...

@seven-phases-max
Member

rgba(255, 0, 0, 1) + rgba(0, 255, 0, 0) -> rgba(255, 0, 0, 1)

Yep, that way we'd better then just copy "blending-normal" code to + handling that does exactly that (this would be a breaking change, but...) I think I was about to make this when I was trying, more over I was able to derive some equations for - so that A + B = C and C - B = A too except maybe some edge cases (I can't recall for sure now). And the show stopper for me were those mul/div only, because having good alpha handling for -/+ and in the same not having one for *// seemed to me quite inconsistent and weird that time.)

P.S. @SomMeri I'll post more details when I get back to city next week (I just don't have that code where I'm now and I can't remember all the stuff I came to).

@matthew-dean
Member

@SomMeri Just so we're talking about the same thing with this:

0.5 * 0.5 = 0.25

That would result in 25% transparency which would be an opacity value of 0.75. So, if we're talking about multiplication / subtraction etc that's what we have to be careful about in comparing to real-world scenarios. The numbers are actually backwards from how we normally think about it. I assume you were implying this but wasn't sure.

Unfortunately, it ends up being a, what quadratic curve?

For instance, if you were multiplying transparencies, and your opacity values were 0.8 and 0.8, your transparency values would be 0.2 and 0.2, and multiplying them would be a transparency value of 0.04 or an opacity value of 0.96.

Ohhhhhhhh.... is THAT where 0.36 comes from when multiplying opacities of 0.2 and 0.2? The opacity is is 1 - (1 - 0.2)^2. DERP. NOW I get it. So 0.36 is actually correct then with multiplication?

@rjgotten
Contributor

My $0.02:

It makes zero sense to perform numerical additions on rgb or rgba colors as if they were vectors. They aren't. They carry a completely different meaning and any kind of operation taking place on them requires additional context in the form of a selected blending mode and compositing mode.

If you're going to introduce a breaking change anyway (and changing the result of a numerical operator certainly is such) then I pose that the best way to handle things would be to have the Less compiler throw a compilation error in places where authors attempt to use numerical operators on colors.

@matthew-dean
Member

@rjgotten It's a good point. And further to what you said, just based on this conversation, it's not really intuitive what the outcome would be. Multiplication seems to be the only metaphor that makes some sense.

That said, Less has historical let you multiply things and units that don't really make any sense. And, in reality, color addition is a standard part of many Less libraries. So it wouldn't just be a small breaking change.

So, maybe, with no good solution, the best solution might be to leave it alone. Except we might do well to explicitly say what kind of color operation is performed based on the mathematical operation.

@matthew-dean
Member

Except, specifically, in regards to alpha, I think 0.2 + 0.2 = 0.2 might be best. And 0.2 * 0.2 = 0.36 how it is right now.

@rjgotten
Contributor

Except, specifically, in regards to alpha, I think 0.2 + 0.2 = 0.2 might be best. And 0.2 * 0.2 = 0.36 how it is right now.

Yes, 0.2 should be the resulting alpha of the addition operation, but probably for different reasons than you are thinking. Allow me to explain.

First off; the latter multiplication example.
That example works because whether using post- or pre-multiplied multiplicative blending operations, the resulting blended alpha component is well-defined and computed as:
aout = asrc + adst × (1 - asrc)

And substituing for your given example that gives:
0.2 + 0.2 × (1 - 0.2) = 0.2 + 0.2 × 0.8 = 0.2 + 0.16 = 0.36

There's no such definition of blending the alpha channel in the context of additive blending, which is the logical parallel of a numerical addition operator, without choosing whether the source or destination color of the blending operation takes the lead.

This is because in additive blending, colors that carry alpha values require that you take one color (either source or destination) as the lead color; use its alpha to establish its weighted contribution to the end color; and then discard the alpha component on the second color, setting its weighted contribution to a full 1.0 instead.

Usually, the lead color is the source color. E.g. in OpenGL you would traditionally set the following for additive blending:

glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE);

The resulting alpha of the color in question is the alpha of the source color.

And there's the problem with mapping additive blending onto numerical addition: like multiplication, numerical addition is commutative. That is: 0.2 + 0.4 = 0.4 + 0.2 and 0.2 × 0.4 = 0.4 × 0.2

Multiplicative blending is commutative as well, so things work out there. However, additive blending is not commutative, as I've described clearly above; it requires picking one color from which the alpha will be inherited. As such, an attempt at shoehorning it onto a commutative numerical addition operator will give people the wrong idea and will almost surely lead to some problem down the road for someone.

In case of your example, it works only because both source and destination (left- and right-hand operand) are the same. So whichever has its alpha component used for the output is irrelevant.

@matthew-dean
Member

@rjgotten You broke my brain with math. So.... we agree then? On the result?

Oh --

it requires picking one color from which the alpha will be inherited

Then, IMO it would be the second (or last) value. I still think we're spending too much brain juice on it. The behavior is undefined so just pick a simple rule and go with it.

@rjgotten
Contributor

@matthew-dean

Then, IMO it would be the second (or last) value.

Canonically it would be the source color and as most people would read A + B as adding B to A, that would make A the destination and B the source. Meaning, you are correct: you inherit the alpha from the right-hand operand, i.e. , "the second (or last) value."

Though I still believe it's in general a bad idea to introduce non-commutative behavior to a commutative operator, in this case only, you're looking at enough of an edge case that you could probably get away with introducing it. Still, it would need some very well documented conditions attached, or it will blow up in someone's face somewhere.

You broke my brain with math

Sorry about the mental BSODs, but color blending and compositing do tend to lead to headaches in the math department. I'm pretty sure your brain is still salvagable though. Nothing some duct tape can't patch back together, right? ;-)

@seven-phases-max
Member

Well, to argue with

without choosing whether the source or destination color of the blending operation takes the lead.

Arithmetic addition may only be abstracted via some associative thing so that A + B = B + A, so neither operand should be a lead. For that reason none of OpenGL blending modes (at least those of v1.x) can actually fit. Meanwhile W3C "blending normal" op fits + just perfect (notice that after result opacity normalisation the whole thing becomes associative... e.g. try Less average function with whatever two args in either order... + would be simply the same except / 2)

For the same reason I will argue (well, sorry, yet again just a brief remark, and more details later) with:

requires additional context in the form of a selected blending mode and compositing mode.

There's the only blending and compositing mode that can represent the addition op (thus you don't need to "select" any). All other (GL or W3C) modes are no more than artificial fancy effects having very few to do with the addition as an arithmetic operation.

@SomMeri
Member
SomMeri commented Sep 21, 2015

No matter what operation, alpha currently does this: alpha1 * (1.0 - alpha2) + alpha2 which is not commutative nor associative.

Then again there is no standard of what that should evaluate to and this does not seem like a place where we should spend too much time with. Based on this discussion, artists and designers are more likely to use real blending functions or filters instead of operations like this.

Does anyone object if I just document it as it is and close this issue? Even if we come up with something clever, people are unlikely to use it since that clever thing will be on an unexpected place.

@Synchro
Member
Synchro commented Sep 21, 2015

I agree - no point in spending much time trying to make something nonsensical make sense!

@seven-phases-max
Member

alpha1 * (1.0 - alpha2) + alpha2 which is not commutative nor associative.

alpha1 * (1.0 - alpha2) + alpha2 is associative, as it's just "optimized" a1 + a2 - a1 * a2. But never mind.


Speaking of the documentation, I think it's fine to just state that: for arithmetic ops, if either of operands is a transparent color, the result is undefined/unspecified and a subject to change in future versions (..or a sort of. I believe this would be the most fair and should cut any confusion).

@SomMeri
Member
SomMeri commented Sep 21, 2015

@seven-phases-max Math fail.

@rjgotten
Contributor

Speaking of the documentation, I think it's fine to just state that: for arithmetic ops, if either of operands is a transparent color, the result is undefined/unspecified and a subject to change in future versions (..or a sort of. I believe this would be the most fair and should cut any confusion).

I'm going to +1 that. 👍

@matthew-dean
Member

Agree. I think in general we should just steer people towards Less's actual color manipulation functions, since there's like a billion.

@SomMeri
Member
SomMeri commented Sep 24, 2015

Thank you all for discussion and answers. I updated docs in pull request and will close this so it does not clutter tracker.

@SomMeri SomMeri closed this Sep 24, 2015
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment