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:

ugly gray trails

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.


Your browser does not support HTML5 canvas.

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:

  • A simple particle example with fading by transparent black, and
  • A particle example with a dirty solution: use a gray background of the correct color.

    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:

    In a browser which rounds to the nearest integer (such as Chrome, IE, or Firefox):
    • 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}.\]
    In a browser which always rounds down to the nearest integer (such as Safari or Opera):
    • 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!