Canvas Wave

While toying around with the drawing aspect of canvas I started using Quadratic Curve’s to make a line bob up and down. Putting them side by side in reverse, mimicked a line waving.By creating more than one and placing them on top of each other it replicated a waving ocean.

Full Tutorial – Continue to read…

Let’s get started with the HTML for the page. This will stay the same through out the tutorial so I’ll just give you all the content you need for it in one.

<html>
<head>
    <title>My Wave</title>
    <script type="text/javascript" src="waves.js"></script>
    <style type="text/css">  
        canvas {
            background-color: black;
        }  
    </style>
    <script>
        var canvas = null;
        var waves = null;
        function loadCanvas( ) {
            canvas = document.getElementById( 'canvas' );
            waves = new Waves( canvas,660,417 );
            setInterval( "run()", 80 );
        }
        function run(){
            waves.update( );
            waves.draw( );
        }
        window.addEventListener( 'load', loadCanvas, false );
    </script>
</head>
<body>
<canvas id="canvas" width="660" height="417"></canvas>
</body>
</html>

We begin by including the main javascript class that handles the waves behavior (waves.js). Then style the canvas to give it a black background.

Then we move into the initialising stage. Add an event listener to the page the calls loadCanvas() when the page is loaded. This function grabs the canvas by its id and creates an instance of the Waves object and passes it the to canvas to draw to, its width (660) and height (417). Finally creates an interval that calls run() every 80 milliseconds.

The run function calls the waves object update() followed by the draw() functions. This is how many games are developed as we’ll see later. Its not just a case of moving a line, it requires clearing the screen and redrawing it frame by frame.

Now lets move into the wave.js. Create a javascript document and save it into the same folder as the HTML file, calling it waves.js.

The Wave Object

Lets start off by making the Wave object:

function Wave( $canvas, $y, $colour ){
    this.ctx = $canvas.getContext( '2d' );
    this.force = 0;
    this.wavePower = 40;
    this.count = $y;
    this.y = $y + Waves.globalY;
    this.alpha = 0.1;
}

As you can see the wave object takes three parameters. A reference to the canvas for it to draw straight onto, a Y position to create the wave on and a colour. Now we define a few parameters such as, force (which will be applied in the update function), wavePower (relates to how much the wave rises and lowers) and alpha which is set to 0.1 allowing the colours to multiply as they lay on top of each other it makes it look like the ocean is fading to darkness.

Each object needs an update and draw function if its displaying something. Each of these functions are placed inside the main object function. Lets add the wave’s update function:

this.update = function(){
    this.y = $y + Waves.globalY;
    this.force = Math.sin(this.count);
    this.count += 0.05;
}

We update the y variable by adding the globalY. This allows the handler class to move the waves up and down together. We manipulate the force using the sine function, which produces a number from -1 to 1 and by updating the force by a sine of a steady increasing number we get a climb to 1 then back to -1.

Now onto the draw function:

this.draw = function(){
    this.ctx.fillStyle = "rgba(0, 0, 0, 0.1)";
    this.ctx.fillRect(0,0,Waves.width,Waves.height);
    this.ctx.fillStyle = "rgba("+$colour+", "+this.alpha+")";
    this.ctx.beginPath();
    this.ctx.moveTo(0, this.y);
    this.ctx.quadraticCurveTo(Waves.width / 4, this.y + ( this.wavePower * this.force ), Waves.width / 2, this.y);
    this.ctx.quadraticCurveTo(Waves.width * 0.75, this.y - ( this.wavePower * this.force ), Waves.width, this.y);
    this.ctx.lineTo(Waves.width, Waves.height);
    this.ctx.lineTo(0, Waves.height);
    this.ctx.lineTo(0, this.y);
    this.ctx.closePath();
    this.ctx.fill();
}

The first two lines of this function draws a black box over the whole screen with an opacity of 0.1 (10%). This leaves a ghost of the wave as it moves which fades with each redraw. I used this instead of clearing the whole screen as its leaves a nice effect. To clear the screen use:

this.ctx.clearRect(0, 0, Waves.width,Waves.height);

We begin to draw the new wave, setting the fill colour to the randomly generated one and the alpha of the wave, in this case is 0.1. The beginPath function starts a new path. We move to the starting postion which is the the left most side of the canvas and the waves y position.

The quadraticCurveTo function takes four variables. The first two are the X and Y position of the control point. This is followed by the X and Y of the end point.

This first curve will cover half the screen from left most point to the center. We set the first control points X position to a quarter the way across the canvas and its Y position based the on waves Y plus the wavePower (40) times by the force (-1 to 1). Its end points are the center of the canvas on the waves Y position.

Now we do the same curve starting from the center of the screen and ending at the far right side of the canvas. This time were minusing the wavePower times force. Finish by using lineTo to draw to the bottom right of the canvas then to the bottom left of the then up to the waves y position. The path can be closed using closePath. Finally fill the drawn area.

That’s it! We have a full wave object that can be updated and re-drawn to our liking. So lets move on to the handler class which I’ve called Waves.

The Waves Object

This is the object that is created in the HTML and controls the waves. The Waves function takes three parameters as discussed before. A reference to the canvas, a width and a height to set the waves.

function Waves( $canvas, $width, $height ){
    this.numberOfWaves = 10;
    this.waveGap = 20;
    this.width = Waves.width = $width;
    this.height = Waves.height = $height;
    Waves.globalY = 0;
    this.move = 1;
    this.ctx = $canvas.getContext( '2d' );
   
    this.colour = Math.round(Math.random()*255)+", "+Math.round(Math.random()*255)+", "+Math.round(Math.random()*255);
   
    this.wavesArray = new Array();
   
    this.beginingY = Waves.height / 2;
    while(this.numberOfWaves--){
        this.wavesArray.push(new Wave($canvas, this.beginingY, this.colour));
        this.beginingY += this.waveGap;
    }
}

Here is the main part of the Waves object that constructs an array of Wave objects. There are a few variables which dictate the behaviour of the “ocean”. numberOfWaves is pretty straight forward, its the number of waves that are created and displayed. waveGap is the amount of pixels between each wave. Were defining a random colour which is passed the waves to draw. By giving them different colours can result in fun looking oceans.

We are looping through the numberOfWaves creating instances of the Wave object and storing them in the wavesArray.

But remember the javascript on the HTML page are calling waves.update( ) and waves.draw(). These still need to be created. These functions are much easier than the update and draw functions in the wave object. These functions just need to loop through the wavesArray calling the wave objects update and draw functions. Here they are:

this.update = function(){
    var bL = this.wavesArray.length;
    while( bL-- ){
        this.wavesArray[ bL ].update( );
    }
    Waves.globalY += this.move;
    if(Waves.globalY > (Waves.height / 2)-50){
        this.move = -1;
    }else if(Waves.globalY < -(Waves.height / 2)){
        this.move = 1;
    }
}
   
this.draw = function(){
    this.ctx.save();
    var bL = this.wavesArray.length;
    while( bL-- ){
        this.wavesArray[ bL ].draw( );
    }
    this.ctx.restore();
}

The only part that is unexpected here is changing the globalY variable in the update function. If you remember this is added to the waves Y position as they are drawn. And there we have it! A Wave object created and controlled by a Waves object. I encourage you to play around with the variables and even add some of your own and see what results you get! Post below with your own twist on this wave class. I look forward to seeing what you can come up with.

The full waves.js code:

function Waves( $canvas, $width, $height ){
    this.numberOfWaves = 10;
    this.waveGap = 20;
    this.width = Waves.width = $width;
    this.height = Waves.height = $height;
    Waves.globalY = 0;
    this.move = 1;
    this.ctx = $canvas.getContext( '2d' );
   
    this.colour = Math.round(Math.random()*255)+", "+Math.round(Math.random()*255)+", "+Math.round(Math.random()*255);
   
    this.wavesArray = new Array();
   
    this.beginingY = Waves.height / 2;
    while(this.numberOfWaves--){
        this.wavesArray.push(new Wave($canvas, this.beginingY, this.colour));
        this.beginingY += this.waveGap;
    }
   
    this.update = function(){
        var bL = this.wavesArray.length;
        while( bL-- ){
            this.wavesArray[ bL ].update( );
        }
        Waves.globalY += this.move;
        if(Waves.globalY > (Waves.height / 2)-50){
            this.move = -1;
        }else if(Waves.globalY < -(Waves.height / 2)){
            this.move = 1;
        }
    }
   
    this.draw = function(){
        this.ctx.save();
        var bL = this.wavesArray.length;
        while( bL-- ){
            this.wavesArray[ bL ].draw( );
        }
        this.ctx.restore();
    }
}

function Wave( $canvas, $y, $colour ){
    this.ctx = $canvas.getContext( '2d' );
    this.force = 0;
    this.wavePower = 40;
    this.count = $y;
    this.y = $y + Waves.globalY;
    this.alpha = 0.1;
   
    this.update = function(){
        this.y = $y + Waves.globalY;
        this.force = Math.sin(this.count);
        this.count += 0.05;
    }
   
    this.draw = function(){
        this.ctx.fillStyle = "rgba(0, 0, 0, 0.1)";
        this.ctx.fillRect(0,0,Waves.width,Waves.height);
        this.ctx.fillStyle = "rgba("+$colour+", "+this.alpha+")";
        this.ctx.beginPath();
        this.ctx.moveTo(0, this.y);
        this.ctx.quadraticCurveTo(Waves.width / 4, this.y + ( this.wavePower * this.force ), Waves.width / 2, this.y);
        this.ctx.quadraticCurveTo(Waves.width * 0.75, this.y - ( this.wavePower * this.force ), Waves.width, this.y);
        this.ctx.lineTo(Waves.width, Waves.height);
        this.ctx.lineTo(0, Waves.height);
        this.ctx.lineTo(0, this.y);
        this.ctx.closePath();
        this.ctx.fill();
    }
}

This entry was posted in Blogs. Bookmark the permalink.

7 Responses to Canvas Wave

  1. Stacey Church says:

    Help, this does not work in IE.

    Any help with any hack to make it work in IE

  2. Adam says:

    Great canvas tutorial Anthony. Is it possible to have the canvas transparent but still maintain the wave effect on a background-image? If so how would you go about this?

    Thank You,
    Adam

  3. Anthony Dillon says:

    Hi Adam,

    Yes of course we can have a transparent canvas with background-image showing through. It only needs a few tweaks to the code. The changes you’ll have to make to the original code are below:

    1. Lets apply a background image to the canvas.
    Change:

    canvas {
        background-color: black;
    }

    To:

    canvas {
        background:url(canvas-background.jpg) no-repeat;
    }

    2. We need to clear the screen with each update to hide the old waves as we draw the new ones.
    Change:

    this.draw = function(){
        this.ctx.save();
        var bL = this.wavesArray.length;
        while( bL-- ){
            this.wavesArray[ bL ].draw( );
        }
        this.ctx.restore();
    }

    To:

    this.draw = function(){
        this.ctx.clearRect(0,0,this.width,this.height);
        this.ctx.save();
        var bL = this.wavesArray.length;
        while( bL-- ){
            this.wavesArray[ bL ].draw( );
        }
        this.ctx.restore();
    }

    3. Each Wave will have to have a dynamic alpha now, so lets send that through to the Wave.
    Change:

    while(this.numberOfWaves--){
        this.wavesArray.push(new Wave($canvas, this.beginingY, this.colour));
        this.beginingY += this.waveGap;
    }

    To:

    while(this.numberOfWaves--){
        this.wavesArray.push(new Wave($canvas, this.beginingY, this.colour, 1 / this.numberOfWaves));
        this.beginingY += this.waveGap;
    }

    4. Now we are sending new parameter to Wave we need to setup Wave to accept it.
    Change:

    function Wave( $canvas, $y, $colour ){

    To:

    function Wave($canvas, $y, $colour, $alpha ){

    5. We need to set the alpha of the wave by simply setting this.alpha with the past parameter $alpha.
    Change:

    this.alpha = 0.1;

    To:

    this.alpha = $alpha;

    6. Finally we need to remove the section that draws a black box over the whole screen with an opacity of 0.1 (10%).
    Change:

    this.draw = function(){
        this.ctx.fillStyle = "rgba(0, 0, 0, 0.1)";
        this.ctx.fillRect(0,0,Waves.width,Waves.height);
        this.ctx.fillStyle = "rgba("+$colour+", "+this.alpha+")";
        this.ctx.beginPath();
        this.ctx.moveTo(0, this.y);
        this.ctx.quadraticCurveTo(Waves.width / 4, this.y + ( this.wavePower * this.force ), Waves.width / 2, this.y);
        this.ctx.quadraticCurveTo(Waves.width * 0.75, this.y - ( this.wavePower * this.force ), Waves.width, this.y);
        this.ctx.lineTo(Waves.width, Waves.height);
        this.ctx.lineTo(0, Waves.height);
        this.ctx.lineTo(0, this.y);
        this.ctx.closePath();
        this.ctx.fill();
    }

    To:

    this.draw = function(){
        this.ctx.fillStyle = "rgba("+$colour+", "+this.alpha+")";
        this.ctx.beginPath();
        this.ctx.moveTo(0, this.y);
        this.ctx.quadraticCurveTo(Waves.width / 4, this.y + ( this.wavePower * this.force ), Waves.width / 2, this.y);
        this.ctx.quadraticCurveTo(Waves.width * 0.75, this.y - ( this.wavePower * this.force ), Waves.width, this.y);
        this.ctx.lineTo(Waves.width, Waves.height);
        this.ctx.lineTo(0, Waves.height);
        this.ctx.lineTo(0, this.y);
        this.ctx.closePath();
        this.ctx.fill();
    }

    There we have it Adam, that should do the trick. Hope this helps and any other problems let me know and I’ll have a go at solving them.

    Thanks,
    Anthony.

  4. Anthony Dillon says:

    Stacey, It depends which IE you are using? You need IE9+. You could also use Chrome or Firefox to view canvas elements.

  5. Jason Meller says:

    how do you get the waves to come in from the top?

  6. Anthony Dillon says:

    Hi Jason,

    In the update function I increase or decrease the globalY varible based on the location of the waves:

    Waves.globalY += this.move;
    if(Waves.globalY > (Waves.height / 2)-50){
        this.move = -1;
    }else if(Waves.globalY < -(Waves.height / 2)){
        this.move = 1;
    }
  7. Lars says:

    one wave is not displayed, you should write :

    this.wavesArray.push(new Wave($canvas, this.beginingY, this.colour, 1/(this.numberOfWaves+1)));

Leave a Reply to Jason Meller Cancel reply

Your email address will not be published. Required fields are marked *

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>