About Talks Articles

Underwater effect with HTML5 Canvas

3 min read

For the impatients, the demo is

Combining pixel manipulation with geometric distortion can be fun, see for example the underwater effect in my Qt code example from 4 years ago (which itself was a porting of the SDL-based effect from almost 6 years ago, time flies!). This was my attempt to achieve the same feeling as in the Quake game when you submerge (into the water). The actual code itself is pretty simple, it’s a matter of shifting the pixels horizontally and vertically in a periodic manner.

Recently I ported this effect to HTML5 Canvas with pixel manipulation routine for the distortion written in JavaScript. Before I show you the relevant code fragment, let’s see first the distortion result which we want to get, applied to a checkerboard pattern to better reveal its effect (left: original, right: distorted):

The main gut of the effect is the following code (yes, it’s only a dozen lines!):

T = frames * interval * frequency / 1000;
for (x = amplitude; x < width - amplitude; ++x) {
    for (y = amplitude; y < height - amplitude; ++y) {
        xs = amplitude * Math.sin(2 * Math.PI * (3 * y / height + T));
        ys = amplitude * Math.cos(2 * Math.PI * (3 * x / width + T));
        xs = Math.round(xs);
        ys = Math.round(ys);
        dest = y * stride + x * 4;
        src = (y + ys) * stride + (x + xs) * 4;
        r[dest] = pixels[src];
        r[dest + 1] = pixels[src + 1];
        r[dest + 2] = pixels[src + 2];

For every pixel, we find out from where we shall get the pixel value due to the periodic modulation. Since it’s just a linear combination of horizontal shift and vertical shift, the computation is rather easy. Once the location is known, it’s a matter of copying 3 values (for RGB) from the pixel array to the canvas image data.

For your pleasure, try the online demo at or the embedded (via iframe) below:

The complete example code is available in the usual X2 repository, look under javascript/underwater directory (set up a web server, due to the same origin limitation). Feel free to convert the animation routine to use requestAnimationFrame instead.

Because of the rather intensive processing, forget about getting a high frame-rate with this demo, even on the desktop machines. In my test machine, Firefox 10 easily gets 30 fps, Safari 5.1 follows with 20 fps, and both Opera 11.61 and Chrome 17 struggle at 15 fps. On many mobile devices, you are lucky if you get more than 2 fps!

From the performance perspective, using WebGL and a suitably written shader is still the best approach. However, for some fun weekend hack, nothing beats a simple underwater Canvas effect.

Related posts:

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

Share this on Twitter Facebook Google+

comments powered by Disqus