The HTML5 canvas is fast becoming a replacement for Flash when it comes to little particle effects and artistic experiments in the browser, but sadly one favorite animation technique is still lacking: slow fade-outs of images. And it seems unlikely that the necessary cross-browser consistency will be coming soon, at least not in the standard 2D canvas context.
Below is a screencap of a canvas animation featuring a single randomly moving particle. The trail is supposed to fade to black but instead an ugly gray remnant is left behind:

These permanent gray remnants occur when we fade images by using a low-alpha black rectangle painted over the entire canvas (when animating on a black background). In theory this should slowly fade all colors to black, and if you test this in a browser like Safari this is what you will see. But in many browsers, you’ll see these gray remnants. If you would like to anticipate the exact color of these trails, I’ll show you below how to calculate it.
The problem: Different browsers do different pixel math
The problem comes down to the way different browsers compute pixel values when blending colors according to alpha. And because different browsers do different things, you cannot code your effects in a way that will look consistent in all browsers.
Since colors are ultimately stored as integer values, any calculated pixel values resulting in fractional values have to be rounded to integers. In some browsers, values are rounded to the nearest whole number, in other browsers values are always rounded down (that is, floored). But what difference would it make? Surely no one can tell the difference, right? Well, as it turns out it makes quite a big difference.
An Example
Have a look at the live canvas example below. Here, a white square has been drawn in the middle, and then a low-alpha (alpha = 0.04) black square is drawn over the top of it repeatedly (73 times). Assuming your machine works the same as mine, if you view this in Safari or Opera you’ll just see a black square. If you view it in Chrome, IE or Firefox, then you’ll see a faint gray square remaining in the middle, of color #0C0C0C. The color will be stuck here; painting the transparent black square again and again will still result in this same color.
Examples and downloads
Simple square examples:
- To see a slowly fading version with color readout below, click here.
- To see the same issue with fading using transparent white (which will create remnants in any browser), click here.
Simple particle examples:
Source
Download all source files here
What’s happening?
In the examples above, we painted a black rectangle with alpha 0.04 over the top of the display. But in truth, there is no such thing as alpha equaling 0.04. Just like color components, alpha is stored as an integer between 0 and 255, and the closest whole number with ratio 0.04 to 255 is 10. So the number 10/255 (approximately 0.0392157) is the number used for calculations when blending by alpha.
When a color \(topColor\) with alpha \(\alpha\) is painted on top of a solid color \(baseColor\), the resulting color should be
\[
(1-\alpha) \cdot baseColor + \alpha \cdot topColor
\]
(the calculation is done in this way separately on the red, green, and blue channels). But since this calculation results in a float value, not a whole number, rounding must occur. Some browsers round down all the time (Safari, Opera), other browsers round to the nearest whole number (Chrome, IE, Firefox).
But this is the effect of the rounding: In the examples above, we blend a transparent black on top of a gray color. When the gray is at the hex value #0C0C0C, each RGB component has decimal value 12. Blending the black with alpha 10/255 on top results in each RGB component being calculated as
\[
\left(1-\frac{10}{255}\right) \cdot 12 + \left(\frac{10}{255}\right) \cdot 0 \approx 11.5294.
\]
If this float value is rounded down, it will be set to the value 11. But if it is rounded to the nearest whole number, it will be rounded back up to 12, the same color we started with.
What about white?
Fading to a white background by painting low-alpha white is no better. In fact, this will leave behind gray remnants in any browser. In case you missed it above, click here for an example.
What color will the remnants be?
I’ll provide some explanation below, but if you want to skip the details here are some formulas. Suppose \(c\) is the red, green, or blue component value of a solid color, and we paint a transparent black or white over the top of it. Then:
- When painting black with alpha \(0 \leq \alpha \leq 1\), a color component \(c\) will be rounded back to the same value \(c\) if \[c \leq \frac{1}{2\alpha}.\]
- When painting white with alpha \(\alpha\), a color component \(c\) will be rounded back to the same value \(c\) if \[c \gt 255 – \frac{1}{2\alpha}.\]
- When painting black with alpha \(\alpha\), all color components will reduce to a lower value (which after repeated paintings will fade images to complete black),
- When painting white with alpha \(\alpha\), a color component \(c\) will be rounded back to the same value \(c\) if \[c \gt 255 – \frac{1}{\alpha}.\]
This means, for example, that if you make an animation featuring white particles on a black background, and have them fade out to black by drawing black with alpha \(10/255\) over the top, then the gray remnants will have value 12, because this is the brightest color value gray below \(255/(2\cdot 10)\ = 12.75\). Thus the remnants will have hex color #0C0C0C.
The math
I will only derive one of the formulas above; the rest are obtained in a similar way. Consider the example of painting transparent white with alpha \(\alpha\) over the top of an opaque color with the value \(c\) (which could be a red, green, or blue value). And suppose we are doing this in Safari, where colors are always rounded down. Then rounding will produce the same resulting color \(c\) whenever the computed color lies in between \(c\) and \(c+1\), that is, when
\[
c \leq (1-\alpha)c + \alpha \cdot 255 \lt c+1.
\]
It is easy to check that the first inequality is automatically held by any color value \(c\). And a little algebra will turn the second inequality into
\[
\alpha(255 – c) \lt 1,
\]
and some more algebra (being careful to flip the inequality when multiplying by a negative) produces
\[
c > 255 – \frac{1}{\alpha}.
\]
Thus any color component of value greater than or equal to \(255-1/\alpha\) will remain unchanged when the transparent white is painted over the top.
Even more trouble…a bug in Chrome
While testing some examples for this post, I discovered a bug in the current version (26) of Chrome. When canvases are smaller than 255×255 in size, using fillRect
to paint a transparent color over a solid (alpha = 1) color results in a final color with less than 100% alpha! I submitted a bug report with more information here.
Solutions?
At this point, this cross-browser nightmare is enough to send you screaming back to Flash! But if we want to use the HTML5 canvas, we will have to find some workarounds.
Use a gray background
One option is to simply paint the background of your canvas the same color as the anticipated gray trails. If you choose the right fade color, this color can be close enough to pure black (or pure white) so as to not be noticeable. Of course, this color will be different in different browsers. But if you paint the gray color, in a few frames it should equalize to a consistent color. If you missed it above, here is a particle example where the background is colored the same gray as the trail remnants. The page background is made a lighter gray to trick you into thinking the canvas gray background is actually pure black.
Manipulate the pixels directlly
Another option is to take charge of the bitmap data of the canvas, and explicitly dim each pixel according to whatever math you want to apply. For example, you can reduce the alpha of each pixel by a fixed amount on each animation frame, producing a slow fade. I presented some particle examples using this method in one of my early posts here. But this method is very heavy on the CPU. If a large canvas is used with many animated particles, things can really slow down.
Do you really need that fading effect?
Maybe give up on the idea of slowly fading images, and just cleanly erase the canvas on each frame.
The future?
Certainly there will be other options in the future. CSS filters (still not widely supported) would allow us to darken the entire canvas by a few bits, bringing those gray remnants down to black. Or perhaps hardware accelearted graphics can allow the type of alpha subtraction described above to take place with minimal CPU impact.
Comments?
Do you have any other ideas or projections? I’d love to hear from you in the comments below!
Thanks for validating, and quantifying this phenomenon. You mention “guessing” (which browser is user-agent) but sniffing the browser and conditionally assigning #0c0c0c as the canvas bgcolor if UA is Chrome, FIrefox, or MSIE should eliminate the guesswork, I reckon.
For clearing the canvas between animation frames rather than fading to a background color, I was under the impression that calling resize() was faster than calling fillRect(). Seems so for Firefox, but I’ve never tested using Safari nor Opera.
May 2, 2013 @ 9:19 pm
|Hi sammi,
You could use browser sniffing to determine background, but if you just paint the gray background at the beginning, after a few animation frames in browsers like Safari the color will fade to black anyway, which might be good enough.
As for clearing the canvas, there are a few ways of doing this and I’m not sure what is the fastest.
May 4, 2013 @ 1:23 pm
|Hi there,
With two different rounding algorithms, which is the “correct” one? That is, what does the spec say? If it is one or the other, it is probably more valuable to have a polyfill so that this works consistently on any browser. Any idea?
Andrei Mouravski.
May 4, 2013 @ 10:00 pm
|I haven’t examined the specs so I don’t know what is the “correct” way to round, but I suspect that this is not specified. Would be interesting to have a look. As for a pollyfill, I’m not sure what to do except find appropriate workarounds on a case-by-case basis. Perhaps the best method is direct manipulation of pixel data, rather than relying on inaccurate alpha blending.
May 5, 2013 @ 12:21 pm
|One solution that should work for black and white backgrounds would be to use blending modes: http://blogs.adobe.com/webplatform/2013/01/28/blending-features-in-canvas/
// Fading to black
ctx.save()
ctx.globalCompositeOperation = ‘multiply’
ctx.fillStyle = ‘rgb(253,253)’
ctx.fillRect(0, 0, canvas.width, canvas.height)
ctx.restore()
// Fading to white
ctx.save()
ctx.globalCompositeOperation = ‘screen’
ctx.fillStyle = ‘rgb(2,2,2)’
ctx.fillRect(0, 0, canvas.width, canvas.height)
ctx.restore()
I haven’t tested it but it’s worth a look!
May 14, 2013 @ 5:24 am
|It looks like this is being proposed as a future specification, but presently it’s not supported in all browsers. it would be very, very cool if blend modes like this were to finally become available in the canvas. Let’s hope it happens soon!
Of course, there’s no guarantee that the multiply method will work, though. It might suffer from the same rounding issue.
I dream of a day when I can once again use a colorMatrixFilter and have it supported in all browsers!
May 14, 2013 @ 10:19 am
|a direct pixel manipulatin is a way. Typed array uint18Array and so on. Like in C.
May 26, 2013 @ 6:19 pm
|I tried direct pixel manipulation and it works great in theory, but not at 30fps.
July 9, 2013 @ 3:09 pm
|Yeah, not so good. I did it in some earlier examples posted here, but for larger canvases it’s painfully slow. I think the black rectangle is the best option, or else no fading at all. Hopefully in the future there will be better options!
July 9, 2013 @ 10:16 pm
|http://www.khronos.org/registry/typedarray/specs/latest
see for more info)
May 26, 2013 @ 6:24 pm
|Yeah, I mentioned direct pixel manipulation in the post. I did some early experiments here: http://rectangleworld.com/blog/archives/214. But it is definitely slower than painting a black rectangle over the top. But certainly, this type of direct manipulation is the “right” way to do things.
May 27, 2013 @ 4:12 pm
|Very interesting article, thanks for pointing out the effect, and what a shame there isn’t currently a perfect way to stop it happening. (Just a heads up, you made a couple of references to Safari working correctly and Firefox incorrectly when fading to black, but I’ve just tested this at my end (OSX) and Firefox works great, but Safari leaves the remnants.)
August 9, 2013 @ 11:17 pm
|Oh – I guess I tested in Safari 5.1.2 (the last available version for Windows). Maybe it has been changed for the more recent Mac versions. Thanks for letting me know!
August 10, 2013 @ 2:05 am
|