In this tutorial I’ll run through how I created the 2-dimensional wave effect seen on Other Half Full. It’s built with the HTML5 canvas element and javascript. I also used jQuery, but you can get by without it.

Creating the wave turned out to be an interesting problem. I wanted the wave to originate at the position of the cursor, so using pure sine waves was not an option…the best you can create using only a sine wave looks something like this:

What I eventually arrived at uses a sine function only to determine the y value of the midpoint (which uses the cursor’s position for its x value). The script then spawns a variable amount of other points, and uses bezier curves to connect them. It looks something like this:

As you can see from the above graphic, each peak and valley is a point with two handles that create the curve connecting them.

The loop spawns a new point on the canvas whenever the midpoint reaches its highest or lowest value (when our counter is equal to 90 or 270, respectively). New waves are spawned at the y value of the midpoint, so as the midpoint diminishes each wave is subsequently smaller as well. The speed (and acceleration) that the waves radiate out at is controlled by a variable, as is the amplitude of the wave.

To keep this blog post under control, I’m only going to go over the javascript function that actually does the drawing. You’ll want to check out the source code for the rest of it…it’s pretty well commented and should be easy to follow. There are already great tutorials out there that cover html5 canvas drawing and animation in more detail than I will here. Here is a very helpful canvas drawing tutorial from the friendly folks at Mozilla.

Enough talk, let’s get started! Here is the drawShape() function which is called from an interval (this runs 180 times in my code, about 33 times per second). The counter variable increments every time the function runs, and thus all animation is built around this variable.

function drawShape(){

    midPointY = Math.sin(counter*10*C)*((dur-counter)*ampMultiplier); //Calculates the y value of the midpoint

    if (counter <= dur){
      points[midPointIndex] = new Array(0,midPointY);

      //Check if counter has reached 90deg or 270deg, if so, time to spawn another point
      if (counter%9 == 0 && counter%2 == 1){
        points[point] = Array(-1,midPointY);
        point++;
      }
    }

    ctx.clearRect(0,0,canvas.width,canvas.height); //Clear the canvas

    ctx.beginPath();
    ctx.moveTo(0, waterLevel); //Start on the left side

    //Will contain the previous point to help each point set its bezier curve
    var lastPoint = new Array(0,waterLevel);

    //Loop through the array of points
    //Calculates the proper x and y values of each point
    //Does the actual drawing
    for (var pt = 0; pt < totalPoints; pt++){
      if (points[pt]){
        if (pt < midPointIndex){
          points[pt][0] = (points[pt][0]*spreadAccelleration)-spreadSpeed; //Move points away from the center point
          points[(midPointIndex-pt)+midPointIndex] = new Array(-points[pt][0],points[pt][1]); //Create an opposite point
        }

        var x = points[pt][0]+midPointX;
        var y = points[pt][1]+waterLevel;

        var bezHandle1 = ((x-lastPoint[0])/2)+lastPoint[0];
        var bezHandle2 = x-((x-lastPoint[0])/2);
        ctx.bezierCurveTo(bezHandle1, lastPoint[1], bezHandle2, y, x, y);

        lastPoint[0] = x;
        lastPoint[1] = y;
      }
    };

    //The Water Level has been rendered, draw the rest of the container
    ctx.lineTo(cWidth, waterLevel);
    ctx.lineTo(cWidth, 0);
    ctx.lineTo(0,0);
    ctx.closePath();

    ctx.fill();

    counter++;
    if ( counter == dur) animationActive = false;
    if (counter >= (dur*2)) stopAni();         

  }

Alright…I know that’s a lot to digest…let’s take a look at that all-important first line…

    midPointY = Math.sin(counter*10*C)*((dur-counter)*ampMultiplier); //Calculates the y value of the midpoint

The entire ripple is built around the midpoint. To calculate the y value of the midpoint, we pass it the counter variable.

If we look at a basic sine function function sin(x)*y, changing x will change the wavelength and y controls the amplitude.  I use the value (dur-counter)*ampMultipler because dur-counter (read: duration-counter) will be a decreasing value, thus it grounds out the amplitude, making each peak and valley of the midpoint smaller than its predecessor.

Continuing…

    if (counter <= dur){
      points[midPointIndex] = new Array(0,midPointY);

      //Check if counter has reached 90deg or 270deg, if so, time to spawn another point
      if (counter%9 == 0 && counter%2 == 1){
        points[point] = Array(-1,midPointY);
        point++;
      }
    }

This code first checks that counter hasn’t reached the duration, at which point it will stop spawning new points.

The points array is rebuilt in each frame, so we now add our midPoint. The code then checks whether or not it should create additional new points…it creates these points when the midPointY value is at a peak or valley-which conveniently happens when counter is equal to either 9 or 27 (90deg or 270deg). In this case we add a new point to the ponts array, and increment the point variable (which stores the index of the next point to be added). In this way the wave begins building from the outside, so you might have a points array with values a 0, 1, 6, 11 & 12 while skipping the points in between until a later frame in the animation.

Now let’s skip ahead to the part that actually does the drawing:

    ctx.clearRect(0,0,canvas.width,canvas.height); //Clear the canvas

    ctx.beginPath();
    ctx.moveTo(0, waterLevel); //Start on the left side

    //Will contain the previous point to help each point set its bezier curve
    var lastPoint = new Array(0,waterLevel);

    //Loop through the array of points
    //Calculates the proper x and y values of each point
    //Does the actual drawing
    for (var pt = 0; pt < totalPoints; pt++){
      if (points[pt]){
        if (pt < midPointIndex){
          points[pt][0] = (points[pt][0]*spreadAccelleration)-spreadSpeed; //Move points away from the center point
          points[(midPointIndex-pt)+midPointIndex] = new Array(-points[pt][0],points[pt][1]); //Create an opposite point
        }

        var x = points[pt][0]+midPointX;
        var y = points[pt][1]+waterLevel;

        var bezHandle1 = ((x-lastPoint[0])/2)+lastPoint[0];
        var bezHandle2 = x-((x-lastPoint[0])/2);
        ctx.bezierCurveTo(bezHandle1, lastPoint[1], bezHandle2, y, x, y);

        lastPoint[0] = x;
        lastPoint[1] = y;
      }
    };

    //The Water Level has been rendered, draw the rest of the container
    ctx.lineTo(cWidth, waterLevel);
    ctx.lineTo(cWidth, 0);
    ctx.lineTo(0,0);
    ctx.closePath();

    ctx.fill();

This section begins by clearing the canvas, otherwise it will draw the next frame on top of the last. We traverse the points array checking each position to see if it has a point set. I originally wrote this loop using for (var pt in points) , which is the javascript equivilant of a php foreach() loop…but I later switched to a for loop with condition for efficiency purposes.

Essentially, what this loop does is search through the first half of the points array and calculate the proper x value for each child point based upon the spreadSpeed and spreadAccelleration variables. Then the code mirrors each point to the other side of the midpoint, a more efficient method of calculating both points.

Next, it uses the value of the previous point to calculate a smooth bezier curve.

Once the ‘surface’ of the ripple is drawn, the code simply finishes drawing the box…and we have a sweet ripple on our canvas.

And that’s it for the drawing! Obviously you’ll have to download the source to actually get anything working.

A couple of other points: Getting the canvas element to fill 100% of the browser window (or to fit into any fluid width) cannot be done with css or by setting width to 100%. You’ll have to use a little bit of javascript like this:

var viewportWidth = $('#trailing-header').width()
      $("#ripple").attr('width',viewportWidth).attr('height',cHeight);
      cWidth = viewportWidth;

I get the width of a fluid element rather than $(‘document’) because it works better with scroll bars. Additionally, I store the cWidth variable for use later. I call this on $(window).resize() so the ripple is always set to 100% width.

Last thing worth mentioning…filling the html5 canvas element with a gradient caused a significant performance decrease, and adding a drop shadow practically nuked the whole thing. I guess we can’t have our cake and eat it too.

That’s it! Hope you found this html5 tutorial useful. Here is the source code. If you use this script on your website, links back are always appreciated.


16 Responses to “HTML5 Canvas Tutorial: Creating a 2-dimensional wave”  

  1. 1 Zeno Rocha

    Hey, take i look what i did http://beerblogging.org/ =]

  2. 2 jpezzetti

    Nice application Zeno!

    Backlinks are always appreciated =) –

  3. 3 Charles

    Need to modify it some more but its nice

    Have any good ideas on how to make it twitch on it own?

  4. 4 jpezzetti

    You can call it on anything you like Charles, I have it called when the mouse hovers over a trigger. You can change this to anything you like. You can have it flow all day long if you want but you’d have to change the function so it doesn’t degrade.

  5. 5 konstantinos

    Awesome! Keep up the good work.

  6. 6 arturohm

    HI it’s great your work. Is possible the same effect with jquery only ? without HTML

  7. 7 arturohm

    … without Html5

  8. 8 jpezzetti

    While I’m sure it could be done with jQuery alone it would be pretty damn messy…this code relies upon the HTML5 Canvas element.

  9. 9 adm351

    Has anyone tried to make the water level vertical? I tried but to no avail…

  10. 10 jpezzetti

    @adm351: Doable, but it will take some work on your part. Good luck, let me know how it goes.

  11. 11 Ben

    Hi, nicely done. I’ve been having fun with this and got a bit stuck, I am trying to change it to a timer based event, not on mouse hover. Any hints on how to remove the hover?

  12. 12 jpezzetti

    @Ben: Haven’t looked at this source code in awhile, but what you’re proposing shouldn’t be too difficult. The script uses the x value of the mouse cursor to define the centerpoint of the wave, my guess is that’s where you’re stuck, as you’ll need to hard code that value or come up with it somewhere else. Good luck!

  13. 13 Ben

    Thanks John, that was where I was stuck. I have changed the midPointX to a changing random value (within the page width) and it’s working great, it appears as a almost continuous ripple. The is only a very short moment of flat line as the ripple restarts.

  14. 14 Felix

    Hey John,
    This is a great tutorial!
    I’m trying to make the wave move on their on with no hover, something like an animation can you help me on that? I’m not that great with this

  15. 15 Felipe Haack Schmitz

    Hello, so cool this effect ;)

    I try to add an image in the bottom the wave, (this image: http://shinyuufansub.com.br/felipehs/agua2.png), but I don’t have any idea how I can to do it.

    Can u help me?

  16. 16 jpezzetti

    Hi Felipe,

    If you’re trying to animate that particular image then this tutorial won’t help you much. Your best bet using a version of my code would be to create multiple overlayed waves and try to achieve a more photo realistic effect, as in your image. Otherwise you’re better off looking for code that will create a ripple effect on a bitmap, which I’m sure you can find with a google search. Good luck.

Leave a Reply


Current month ye@r day *