.

Tags:

For the impatients, the demo is ariya.github.com/canvas/underwater.

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 ariya.github.com/canvas/underwater 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.

Ariya Hidayat

Software Provocateur
These days, I promote software craftsmanship around web technologies. If you like this article, read also other similar blog posts and follow me @ariyahidayat.

Latest posts by Ariya Hidayat (see all)

  • Emrah Atılkan

    slow, slow, slow

    • http://ariya.ofilabs.com/ Ariya Hidayat

      As expected!

  • Emrah Atılkan

    slow, slow, slow

    • http://ariya.ofilabs.com/ Ariya Hidayat

      As expected!

  • http://qoal.110mb.com Quiche_on_a_leash

    Tweaked the code a little bit, and now it’s faster. (was 18fps, now 33fps for me)

            T = frames * interval * frequency / 1000;
          var twoPI = 2 * Math.PI, maxY = height – amplitude, maxX = width – amplitude;
            for (x = amplitude; x < maxX; ++x) {
            ys = Math.round(amplitude * Math.cos(twoPI * (3 * x / width + T)));
                for (y = amplitude; y < maxY; ++y) {
                    xs = Math.round(amplitude * Math.sin(twoPI * (3 * y / height + T)));
                    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];
                }
            }

  • http://qoal.110mb.com Quiche_on_a_leash

    Tweaked the code a little bit, and now it’s faster. (was 18fps, now 33fps for me)

            T = frames * interval * frequency / 1000;
          var twoPI = 2 * Math.PI, maxY = height – amplitude, maxX = width – amplitude;
            for (x = amplitude; x < maxX; ++x) {
            ys = Math.round(amplitude * Math.cos(twoPI * (3 * x / width + T)));
                for (y = amplitude; y < maxY; ++y) {
                    xs = Math.round(amplitude * Math.sin(twoPI * (3 * y / height + T)));
                    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];
                }
            }

  • http://georgedina.ro/ George Dina

    It’s slow but it’s awesome.
    I’ll dig into this tonight.

  • http://georgedina.ro/ George Dina

    It’s slow but it’s awesome.
    I’ll dig into this tonight.

  • Josh Forde

    I wonder what makes Chrome execute the animation so much slower than the others.  Doesn’t Google promote its JavaScript engine (the V8 engine) as being the fastest JavaScript engine around?  (Not a rhetorical question; I’m actually interested to know what’s throttling Chrome’s performance here)

    • http://ariya.ofilabs.com/ Ariya Hidayat

      This is not a JavaScript execution issue. The bottleneck is most probably in setting the color value for each pixel, i.e. bridging with the Canvas. Though this problem will go away in the near future, I suspect that right now crossing the JavaScript HTML world in a multi-process environment is still not as fast as it could be.

      • Josh Forde

        Ah I see, interesting. :)

  • Josh Forde

    I wonder what makes Chrome execute the animation so much slower than the others.  Doesn’t Google promote its JavaScript engine (the V8 engine) as being the fastest JavaScript engine around?  (Not a rhetorical question; I’m actually interested to know what’s throttling Chrome’s performance here)

    • http://ariya.ofilabs.com/ Ariya Hidayat

      This is not a JavaScript execution issue. The bottleneck is most probably in setting the color value for each pixel, i.e. bridging with the Canvas. Though this problem will go away in the near future, I suspect that right now crossing the JavaScript HTML world in a multi-process environment is still not as fast as it could be.

      • Josh Forde

        Ah I see, interesting. :)

  • me

    I recently used KineticJS (see: kineticjs.com) would be great to see if this animation done with that …

  • me

    I recently used KineticJS (see: kineticjs.com) would be great to see if this animation done with that …

  • Steven Roussey

    How is it on IE9 and IE10?

    • http://ariya.ofilabs.com/ Ariya Hidayat

      In my machine, IE9 is quite slow. It gets one third of Chrome 18 frame rate.

  • Pingback: Mozilla Hacks Weekly, March 15th 2012 ✩ Mozilla Hacks – the Web developer blog