Linear gradient color shifts are not as expected when opacity changes (want gradients to interpolate in non-premultiplied colors, contrary to spec)

NEW
Unassigned

Status

()

defect
3 years ago
3 years ago

People

(Reporter: mark, Unassigned)

Tracking

({regression})

36 Branch
Points:
---

Firefox Tracking Flags

(Not tracked)

Details

(URL)

Attachments

(2 attachments)

(Reporter)

Description

3 years ago
Posted file HTML testcase
Ever since bug 591600 landed, linear gradients are calculated using premultiplied colors. There is a problem with this approach since it leads to unexpected color shift ranges when going from opaque to transparent (either partially or completely).

The attached testcase gives a good demonstration of this. When going from yellow to red, one would expect this color shift to happen, but it doesn't at all (in the case of alpha 0) or doesn't to the expected extent (larger alpha values). It even introduces some ugly banding and is definitely not a smooth gradient.

As a webdesigner, one would expect the color shift to be *independent* of the opacity value and to occur linearly - something that did happen before the mentioned bug landed.
As it is done currently, this seems like an issue introduced to make "transparent" work as intended, but breaking any other case in the process.

The spec also doesn't say one way or another what is considered intended, only that premultiplied "tends to create more attractive gradients" (obviously only thinking about ->transparent), so I think a better approach here would be to use rgba space (not premultiplied) for the gradient, and make the "transparent" keyword a special case (make it equal to the color of the adjacent color stop, with alpha 0).

e.g. red -> transparent -> yellow
255,0,0,1 -> 255,0,0,0|255,255,0,0 -> 255,255,0,1

Comment 1

3 years ago
This seems to be correct behaviour. It is now consistent with Webkit/Blink/Edge, who also use premultiplied colors.
(Reporter)

Comment 2

3 years ago
How is not having a color shift when you specifically tell it to, correct behavior?

Comment 3

3 years ago
The gradient runs to the end result, which is transparent, and transparent has no color. If you want to have a color in between, add a stop with thatcolor and give it 0.5 alpha.
(Reporter)

Comment 4

3 years ago
You miss my point, Erwin. This is about any value with any opacity, not just pure transparent.

The gradient's colors are directly influenced by whatever opacity you give it in a stop. The more opaque your value, the more color shift is seen. This isn't what you'd expect and doesn't give you the expected result. If you want to go from yellow to red linearly, then you'd expect the half-way point to be color shifted half-way. This doesn't happen if it's anything but the exact same opacity as the starting color.
Going from yellow opaque to red 0.5 at the end, the half-way point is NOT shifted to orange 50%, but half that (because of premultiplication making the final stop be only 0.5*255) and even less at 0.25, and so on.

Manually adding color stops won't help here, either. The end result will still be wrong the same way.

Comment 5

3 years ago
I disagree... I do see what I expect, namely a gradient that transitions towards the calculated end point. And by the way, with some opacity in the end point, the gradient *does* go through a red shift.

I any case, since all browsers now have consistent behaviour regarding gradients, is your complaint really targeting Mozilla, or the standard recommended by W3C? They also expect to see RGBa premultipied colors.[1] Mozilla is just following standards here.

[1] https://www.w3.org/TR/css3-images/#linear-gradients
(Reporter)

Comment 7

3 years ago
I looked in the spec exactly there, and the reason one might choose premultiplied color space is mentioned in the note there.

===
Note: The definition and implications of "premultiplied" color spaces are given elsewhere in the technical literature, but a quick primer is given here to illuminate the process. Given a color expressed as an rgba() 4-tuple, one can convert this to a premultiplied representation by multiplying the red, green, and blue components by the alpha component. For example, a partially-transparent blue may be given as rgba(0,0,255,.5), which would then be expressed as [0, 0, 127.5, .5] in its premultiplied representation. Interpolating colors using the premultiplied representations rather than the plain rgba representations tends to produce more attractive transitions, particularly when transitioning from a fully opaque color to fully transparent.
===

"tend to produce more attractive transitions" only applies to going from a color to transparent black, but not to any other color at various opacities.
I guess my complaint is targeting the W3C if they make premultiplied space the required space to work in in their CR, and this would be a spec bug, then?

Right now, with this, it is *impossible* to have a properly-defined gradient that is actually linear in both color and opacity as one would expect.
Unless someone can explain how I would achieve a linear gradient from yellow opaque to red@50% opacity that actually linearly transitions from yellow to red over the size of the gradient as well as in opacity.

Feel free to direct me to the proper people to address this to if Mozilla doesn't consider this a bug (although it seems to be a direct result of the discussion in the linked bug).
(Reporter)

Comment 8

3 years ago
I guess I'll toss it up on the w3c www-style mailing list. If you have any suggestions otherwise, please let me know.

Comment 9

3 years ago
(In reply to Erwin Dokter from comment #5)
> I disagree... I do see what I expect, namely a gradient that transitions
> towards the calculated end point. 

The goal is to have a linear transition between two colors while also having a linear transition of transparency.


Assuming its supposed to be a transition from opaque yellow to transparent red then neither setting the midpoint to a semi-transparent orange or a semi-transparent red will achieve the goal.
The former fails to have a reddish transparent on the right side, the latter fails to have an orange center.

There does not seem to be a simple way to achieve this. With non-premultiplied colors it would be possible to achieve those results while still having an "aesthetically pleasing" transition to the transparent keyword by special-casing it as Mark suggested.

If I recall correctly the spec was changed back and forth several times to document converging browser behavior, not necessarily to mandate sane behavior.
(Reporter)

Comment 10

3 years ago
There has been some discussion on the mailinglist, where it's the general consensus that using non-premultiplied is better (for a few reasons) and that there isn't really a compelling reason to keep premultiplied color space. Rik Cabanier (Adobe) being very much in favor of what I proposed, although Tab Atkins Jr. (Google) didn't seem to want to touch existing implementations.

On the Mozilla side, Xidorn indicated that there's already special keyword passing done from the CSS parser for color hinting in gradients, so adding a flag for transparent stops shouldn't be much work.

The actual special-casing is very simple; in nsCSSRendering::PaintGradient after creating the enumerated list of stops as an array, it's a simple and single loop over the list and change/insert color stops where needed. Some code to demonstrate:

  // Special case for 'transparent'
  for (uint32_t i = 0; i < stops.Length(); ++i) {
    if (stops[i].mTransparent) {
      if (i > 0) {
        // Change stop color to adjacent-previous (color->T)
        color = stops[i - 1].mColor;
        color.a = 0;
        stops[i].mColor = color;
        if (i < stops.Length() - 1) {
          // We're in the middle somewhere: insert stop adjacent-next (T->color)
          gfxRGBA color2 = stops[i + 1].mColor;
          color2.a = 0;
          if (color != color2) {
            // Only insert an extra stop if c1 is different than c2 in c1->T->c2
            stops.InsertElementAt(i + 1,ColorStop(stops[i].mPosition, color2));
            i++;
          }
        }
      } else if (i < stops.Length() - 1) {
        // Change stop color to adjacent-next (T->color)
        color = stops[i + 1].mColor;
        color.a = 0;
        stops[i].mColor = color;
      }
    }
  }

Concerns about changing the spec impacting web compatibility seem to be low:
Rik:
> Given that a major browser has a different implementation and no bug reports,
> that number is likely small.
> Also, when we made the change in the other browsers, there were no user
> reported bugs that we closed.

Re: implementation
Xidorn:
> ... we already have an additional bit for interpolation hint, so I guess
> we are not very concerned about adding another bit for transparent keyword
> if the spec changes that way.

I don't know what the normal milling time is in the W3C for a spec change like this, but I'd prefer to see it sooner rather than later, myself.
(In reply to The 8472 from comment #9)
> If I recall correctly the spec was changed back and forth several times to
> document converging browser behavior, not necessarily to mandate sane
> behavior.

It was specified as premultiplied at a time when *no* browsers did that, because premultiplied was believed to be better behavior.
Summary: Linear gradient color shifts are not as expected when opacity changes → Linear gradient color shifts are not as expected when opacity changes (want gradients to interpolate in non-premultiplied colors, contrary to spec)
(Reporter)

Comment 12

3 years ago
(In reply to David Baron [:dbaron] ⌚️UTC+11 (busy, returning 8 February) from comment #11)
> 
> It was specified as premultiplied at a time when *no* browsers did that,
> because premultiplied was believed to be better behavior.

AFAICT it was specified because it was the quickest way to achieve a pleasing "fade to transparent" with `transparent` being defined as a shorthand for `transparent black`. See bug #591600 -- which, by the way, seems to be the cause of other things like bug #1242145 ironically enough hit when trying to make a demonstration page for this very bug.

Comment 13

3 years ago
Posted file brown sludge.html
Another issue is that even with constant alpha color blending is not physically correct. If you look at a chromacity diagram[1] then yellow sits between red and green. Yet the gradient creates a brown sludge instead.

I assume this is because blending happens in the non-linear (gamma curve) srgb space instead of linear rgb.

So blending is basically doubly wrong, premultiplied and on the gamma curve.
  

[1] https://upload.wikimedia.org/wikipedia/commons/6/60/Cie_Chart_with_sRGB_gamut_by_spigget.png
(Reporter)

Comment 14

3 years ago
(In reply to The 8472 from comment #13)
> Another issue is that even with constant alpha color blending is not
> physically correct.

That's not related to this issue at all. I think that would be better discussed in a separate bug.

Comment 15

3 years ago
Both affect the same math (rgba interpolation between two reference colors). Should there be a meta-bug covering both then?
(Reporter)

Comment 16

3 years ago
FWIW: what you're talking about would be changing the math *away* from linear rgb by using perceptual brightness values of intermediate colors. The mid point in linear RGB is, after all, (128,128,0) which is a "brown" (low intensity yellow).
That is still something completely different than what this bug was opened for, where the color gradient is directly influenced by the opacity value of the stop, and even if it would touch the same gradient math, it would affect different parts of it.

I don't think a meta for just 2 aspects is needed, but it shouldn't be lumped together in a single bug either.

Comment 17

3 years ago
ok, i filed bug 1248178

> The mid point in linear RGB is, after all, (128,128,0) 

Have a look at http://davengrace.com/cgi-bin/cspace.pl and compare sRGB'(0.5,0.5,0) and sRGB(0.5,0.5,0)
You need to log in before you can comment on or make changes to this bug.