[whatwg] Canvas gradients color interpolation - change to premultiplied?

Tab Atkins Jr. jackalmage at gmail.com
Tue Nov 23 12:43:39 PST 2010


Right now, canvas gradients interpolate their colors in
non-premultiplied space; that is, the raw values of r, g, b, and a are
interpolated independently.  This has the unfortunate effect that
colors darken as they transition to transparent, as "transparent" is
defined as "rgba(0,0,0,0)", a transparent black.  Under this scheme,
the color halfway between "yellow" and "transparent" is
"rgba(127,127,0,.5)", a partially-transparent dark yellow, rather than
"rgba(255,255,0,.5)".*

The rest of the platform has switched to using premultiplied colors
for interpolation, because they react better in cases like this**.
CSS transitions and CSS gradients now explicitly use premultiplied
colors, and SVG ends up interpolating similarly (they don't quite have
the same problem - they track opacity separate from color, so
transitioning from "color:yellow;opacity:1" to
"color:yellow;opacity:0" gives you "color:yellow;opacity:.5" in the
middle, which is the moral equivalent of "rgba(255,255,0,.5)").

It would be unfortunate for canvas gradients to be the only part of
the platform that interpolates colors in a different and generally
uglier way here.  I suspect that this can be changed without any real
compat impact; the change will be a minor visual tweak to the middle
half of gradients that go from a solid color to transparent (the
initial and final quarter won't be noticeably different, solid->solid
transitions aren't affected, and other transitions trace a shorter
line and thus diverge from the "pretty" behavior less).

I realize that this basically just depends on whether browsers are
willing to change their current implementation.  Implementors, does
this sounds like a change you can get behind?  We already changed
canvas shadows to match behavior with CSS shadows; this is a much
smaller change for spec-equivalence.

~TJ

* An even more obvious example can be seen by drawing a
white->transparent gradient in a white rect on canvas.  This should
pretty obviously just result in more white, right?  Instead, you get a
symmetrical fade from white to gray and back to white.  (The reason it
reverses halfway through is because it's now less than half opacity,
so the background white is winning when it's composed with the
gradient color.  The gradient is actually still getting darker until
it hits black at the end, it just contributes less and less of that
darkness to the final color of the pixels.)

** Quick primer for those who don't understand color math well enough
to follow the conversation (like me from a few days ago):

"Premultiplied" means that you multiply the color components by the
alpha component before doing compositing operations.  It turns out
that this lets you get away with some simpler and faster math when
compositing partially-transparent colors while getting equivalent
results, so it's a common optimization.

The effect of this is that the space of colors gets "compressed" as
the alpha shrinks.  If the alpha is .5, then the total value range for
the color components is only 0-127.  If the alpha is .1, the range is
0-25.  This doesn't produce a material change in behavior, due to the
way the compositing math works.  The only downside is that if you lose
precision in the exact color, if you want to extract that back out
from the premultiplied version - there are only 26^3 possible colors
at alpha=.1, as opposed to the 255^3 colors at alpha=1.

A nice benefit of this, though, is that tracing a straight line
between two colors in premultiplied space gives you a more attractive
transitions than doing so in non-premultiplied (that is, normal rgba)
space.  As I noted above, the color halfway between yellow and
transparent (defining "transparent" as transparent black, or
rgba(0,0,0,0)) is rgba(127,127,0,.5) in non-premultiplied space, which
has a dark yellow as its color component.  In premultiplied space, the
midpoint is the 4-tuple (127,127,0,.5), which translates to the color
rgba(255,255,0,.5) when you extract it back into normal rgba space.

Of course, in normal rgba space you could fix this by explicitly
transitioning from yellow to transparent yellow (in other words, from
rgba(255,255,0,1) to rgba(255,255,0,0)), but that's more work.  In
premultiplied space, all fully-transparent colors are equivalent, and
naive transitions always "do the right thing".

~TJ


More information about the whatwg mailing list