w3c/svgwg

Use CSS gradient rules for transitioning semi-transparent stop-colors

AmeliaBR opened this issue · 18 comments

CSS gradients have special rules to create more aesthetically pleasing results when transitioning to transparent colors. Specifically, user agents are required to transition the pre-multiplied colors, after scaling the color intensity by the alpha value. This gives the intuitive result when transitioning from a color to transparent (which is equivalent to transparent black): the color fades to transparent without visually darkening.

To create a similar result in SVG, you need to explicitly repeat the stop-color value, while changing the stop-opacity value. This makes sense if you are setting stop-color and stop-opacity independently; why would you explicitly transition stop-color to black unless that was the effect you wanted?

However, when using semi-transparent color values or the transparent keyword within the stop-color property, I would expect to then get the same results as a CSS gradient with the same colors. That is not currently specified anyware and doesn't seem to be how any browser currently implements it.

Demo of the various options as a CodePen; Screenshot:

Screenshot of the gradients from the demo, as described

  • Top: SVG gradient with constant stop-color and transitioned stop-opacity;
  • 2nd: SVG gradient with stop-color transitioning to black and stop-opacity transitioning to zero;
  • 3rd: SVG gradient with rgba colors;
  • Bottom: CSS gradient with the same rgba colors.

All transition from opaque lime to fully transparent; in all but the first SVG gradient, the final stop is transparent black. The CSS gradient scales the intensity of the color by the alpha value before transitioning, so you don't get the fade to gray effect.

Wouldn't changing that behaviour be a breaking change, though? If it did change, and someone actually wanted that "incorrect" rendering, how would they achieve it?

Would adding a new color-interpolation value be in order?

color-interpolation="premultiplied-sRGBA"

My suggestion was only to apply the CSS pre-multiplied behavior for semi-transparent values in stop-color. These have never been explicitly specified for SVG one way or the other.

I was not suggesting that the behavior of stop-opacity transitions should change. So if you wanted an explicit "fade to black" transition as well as a "fade to transparent", you could still do that by transitioning both stop-color plus stop-opacity.

That said, you're right that this is a color-interpolation issue. However, given that browsers have shown zero interest in implementing the existing color interpolation switch, I'm not sure we'd convince anyone to add an additional option (as opposed to changing rendering universally).

+1 to Amelia's suggestion. Gradients should interpolate their stop-colors with premultiplied alpha to match CSS. stop-opacity still works separately, so you can get the middle two "bad" results by transitioning stop-color from green to black and stop-opacity from 1 to 0.

FYI: The screenshot I posted above is how it looks for me in Firefox, Chrome, and Edge on Windows 10. However, CodePen's static screenshot (which I think uses PhantomJS/WebKit) looks different:

Screenshot of the demo; the third gradient stands out by transitioning to solid black instead of transparent-on-white.

It seems that the alpha channel is being completely ignored for values set in stop-color. (Also, the CSS gradient isn't using pre-multiplied colors, so it has the fade-to-gray effect.)

I'd appreciate someone confirming this result on a recent version of Safari. One more reason we need to clearly define the behavior of semi-transparent colors in stop-color.

Safari matches the CodePen static screenshot example.

Given that we have currently-unspecified behavior, with at least one major non-interoperable implementer, are there any objections to referencing the CSS rules for transitioning colors with alpha when color-interpolation: sRGB is used?

stop-opacity changes would still be applied as an independent effect, masking the color generated from the stop-color gradient.

color-interpolation: linearRGB would effectively become linearRGBA, no fancy pre-multiplication effect. Not that it matters too much to browsers, since none of them have implemented linear RGB gradients.

This achieves my main objective of having the default case consistent between CSS and SVG gradients, while still allowing for flexibility in SVG gradients.

Note that there was a thread about whether colors should be pre-multiplied. In that thread it is said that transparent could be special treated (to create two transparent color stops with the colors of the previous and next color stop) to avoid the fade-to-gray effect to avoid pre-multiplication and by that "fix" gradients between semi-transparent colors.

Sebastian

However that does mean that stop-opacity would work differently from fill-opacity and stroke-opacity - where the opacity value replaces the colour alpha component. ( @BigBadaboom )

Not at all. Fill-opacity and stroke-opacity should apply as additional effects, masking the stroke or fill, regardless of whether stroke & fill are solid colors, semi-transparent colors, or complex paint (patterns or gradients, which may be partially transparent themselves). If any browser is not multiplying the effect, that's a bug in the browser. But it seems to be well-supported based on my testing.

@SebastianZ

Thanks for that link. I didn't realize the CSS rule on pre-multiplied colors was still being debated. Since either effect can be created in SVG using stop-color and stop-opacity independently, I really don't care one way or the other whether semi-transparent stop-color gradients use pre-multiplied values or not: I only care that they are consistent with the same color values in CSS.

@tabatkins, do you have a final resolution on the matter from the CSS WG? Could you get one?

Theoretically, we could link to CSS by reference without locking in the exact details in SVG, but I'd rather not make our spec dependent on something that is still being debated.

(Incidentally, I think we've finally discovered the case where SVG linear and radial gradients will still be useful even once CSS gradient functions are well supported in SVG fill & stroke. That could be brought up as a benefit on the CSS side, too: if you want more precise, independent control over opacity & color changes, you should in future be able to use an SVG paint server instead of a CSS function.)

@AmeliaBR Deleted my comment. Must have misremembered. Apologies if it made you do unnecessary research.

Just remembered the canvas has gradients, too. Again, it would be very nice for both authors and implementers if a simple linear gradient between the same color values creates the same result.

Adapting the live demo from MDN to use the lime to transparent gradient results in this demo. All browsers I've tested (Chrome, Edge, Firefox) draw it as a simultaneous color transition to black as well as alpha transition to zero.

This is clearly defined in the latest canvas specs at WHATWG:

Between each such stop, the colours and the alpha component must be linearly interpolated over the RGBA space without premultiplying the alpha value to find the colour to use at that offset.

I'm not sure if this also means no sRGB adjustments? So a canvas gradient should behave the same as an SVG gradient with color-interpolation: linearRGB. Not sure if anyone implements it that way, but if they did, it would allow SVG to match both behaviors: pre-multiplied (like CSS) for sRGB interpolation and simple linear (like canvas) for linearRGB interpolation.

No, the spec is just being a little imprecise; the RGBA tuples for canvas pixels are sRGB, and it's saying that you just linearly interpolate those.

One way or the other, we should add some rules to the spec, if only to make clear that Safari's "ignore the alpha channel on stop-color behavior is not acceptable."

As discussed at the recent telcon, SVG gradients are now defined in the following way:

  • Calculations are done as in SVG 1.1 and differently than CSS - they are not in premultiplied space
  • If a CSS color value that includes opacity information is used on the stop-color attribute (such as an rgba() value, or the keyword 'transparent') then the opacity used at that stop is the product of the opacity component of the stop-color attribute and the stop-opacity attribute.

This means that 'transparent' really means transparent black in SVG. It is equivalent to stop-color='black' and stop-opacity='0'.

This means that 'transparent' really means transparent black in SVG. It is equivalent to stop-color='black' and stop-opacity='0'.

It was obviously not discussed whether to special case transparent to create two color stops as mentioned earlier interpolating with the previous and next stops. See also the related thread on www-style.

Sebastian

No, it wasn't. Personally I don't see the point.
The model for SVG is to separate the colour and opacity components. E.g. unlike CSS, opacity has no effect of on the influence of a colour on the gradient.
Working stop-color with an alpha component into the model was easy and elegant enough, but as far as treating 'transparent' as a special case, well, defining both a stop-color and a stop-opacity is easy enough that I don't see the point in adding the additional complexity of special cases.

I'd like to point out that the SVG 1.1 spec gives the formulation for alpha blending as the premultiplied version. https://www.w3.org/TR/SVG11/masking.html#SimpleAlphaBlending

This caused me some considerable confusion about whether the spec requires colors to be specified in premultiplied format or not.