ariya.io About Talks Articles

crossfading with canvas

4 min read

Dissolve effect from one image to another is easily achieved by varying the opacity properly. With CSS animation feature, this does not even require any extra JavaScript code.

The following blog post however discusses about dissolve implemented using HTML Canvas. This is not because of performance reason, only to show an example of pixel manipulation via canvas API.

Basically given two images, called source and target, we compose a third image called result. Each pixel in result is just a linear combination of the pixel in the same position from source and target. By varying the coefficients as a function of time, we achieve the crossfading.

Side note: Crossfading in RGB color space can yield a weird and unnatural result. Since usually the dissolve duration is very short, visually it does not matter much. In the future I will talk about crossfading in other color spaces.

The straightforward approach:

function tween(factor) {
    var i, p, q, compl;
    p = source.data;
    q = target.data;
    r = result.data;
    compl = 1 - factor;
    for (i = ; i < len; i += 1) {
        r[i] = p[i] * factor + q[i] * compl;
    }
    context.putImageData(result, , );
}

We can see some problems with the above code. First of all, it accesses three CanvasPixelArray objects inside the loop, namely of the source, target and result images. Since all three of them are live references, this loop becomes quite costly.

A better approach would be to prepare the source and target pixels in two normal arrays. This is done as follows:

offset = new Array(len);
delta = new Array(len);
for (i = ; i < len; i += 1) {
    offset[i] = target.data[i];
    delta[i] = source.data[i] - target.data[i];
}

The inner-loop of the tweening now looks like this:

function tween(factor) {
    var i, r;
    r = result.data;
    for (i = ; i < len; i += 4) {
        r[i] = offset[i] + delta[i] * factor;
        r[i + 1] = offset[i + 1] + delta[i + 1] * factor;
        r[i + 2] = offset[i + 2] + delta[i + 2] * factor;
    }
    context.putImageData(result, , );
}

Note that by storing the tweening information as a pair of (offset, delta) instead of (source, target), we can save one multiplication. In addition, the loop is unrolled a bit in order to skip the alpha channel, i.e. every 4th byte, which is always set to 255 (=opaque).

If you run this example with a modern browser on a powerful machine, likely you will hit the capped 60 fps. With a slower machine, we can still squeeze some more framerate by using Int32Array (wherever it is supported) instead of normal JavaScript array. A typed array has a fixed size and known static type, this makes it easier for a modern JavaScript engine to optimize the execution.

if (typeof Int32Array !== 'undefined') {
    offset = new Int32Array(len);
    delta = new Int32Array(len);
}

In fact, with a typed array above, you can get away with the alpha channel exclusion, i.e. just blindly loop through every byte and combine the pixel values linearly. Nicolas (of PhiloGL fame) gave me the hint that other typed array, e.g. Int16Array should work as well and likely better in terms of memory consumption.

The code is available in the usual X2 repository, look under javascript/crossfading directory (you need to access it via a web server as opposed to local file system, due to the same origin limitation). To keep it simple, animation is triggered via the good old setInterval. For real-world use case, you may want to use requestAnimationFrame instead.

For a live demo, check out ariya.github.com/canvas/crossfading with your favorite browsers, in particular on mobile devices. It’s also a simple benchmark test, let it run for about 7 seconds. Due to the extensive pixel manipulation, don’t expect to get double-digit fps on most smartphones. Still, share your framerate!

Related posts:

♡ this article? Explore more articles and follow me Twitter.

Share this on Twitter Facebook