Jan
10
2012

HTML5 canvas in detail (with Snake game demo)

Introduction

HTML5 Canvas is around for some time now. There are lot of things which could be created very easily in HTML5 making it easier to create basic graphic/styles/animation with little code which has inbuilt support from browser. Earlier to create such thins, we had to rely on external plugins (like flash) or heavy Javascript/DHTML.

In this article, I will demonstrate creating basic shapes and animation using HTML5 Canvas and in the later part I will put things together to create classic old mobile snake game.

Article Body

If you are reading this page just to see how this snake game looks (as title suggests) and you simply need the code and your sure of understanding code just by having a look, then you can jump to "snake game" section on this page or check out the demo here.

The w3g specification lists all details about canvas implementation here. We will look at the some of them.

Basics of HTML5 canvas drawing

The new HTML5 canvas element tag is declared as this

<canvas id="myCanvas" width="978" height="1300">
</canvas>

As it is evident height and width and only two attributes available for canvas element. Apart from that, following methods are defined for canvas element

//To convert canvas generated visual image into base 64 string
string toDataURL(optional String Imagetype, any other args);

//to serialize canvas generated visual image into file and invokes callback
void toBlob(FileCallback? callback, optional String Imagetype, any other args);

//returns the context of canvas for given context id. 
object getContext(DOMString contextId, any other args);

The "any other args" in above every method is to accomodate any addition to argument list in future.

To start drawing in canvas, the 2D context has to be initialize first in JavaScript.

var canvas = document.getElementById("myCanvas"); //create canvas object
var context = canvas.getContext("2d");   //get canvas 2D context

Next, call the context beginPath method. The beginPath empty's the list of subpaths so that the context once again has zero subpaths

context.beginPath();

Define the starting point (x,y axis) to start drawing shape.

context.moveTo(50, 50);

Note: The x and y arguments of moveTo or any other canvas related method should be less than height and width defined for canvas element. Otherwise the drawing shape would get out of canvas boundary and out of view.

The strokesStyle attribute represents the color/style used for lines around shapes (boundaries). While fillStyle attribute indicates the colr/style value used to fill the internal of shape.

context.strokeStyle = "blue";

context.fillStyle = "blue"; // fill color

Finally the stroke method/ Fill method calculates strokes of all subpaths created after last call beginPath() and then fill the combined stroke area using strokestyle/fillstyle attributes.

context.fill();

context.stroke();

To facilitate drawing of complex shapes, HTML5 specification provides many other shape drawing methods like

void lineTo(double x, double y);
void quadraticCurveTo(double cpx, double cpy, double x, double y);
void bezierCurveTo(double cp1x, double cp1y, double cp2x, double cp2y, double x, double y);
void arcTo(double x1, double y1, double x2, double y2, double radius);
void rect(double x, double y, double w, double h);
void arc(double x, double y, double radius, double startAngle, double endAngle, optional boolean anticlockwise);

It also has transformation methods as

void scale(double x, double y);
void rotate(double angle);
void translate(double x, double y);
void transform(double a, double b, double c, double d, double e, double f);
void setTransform(double a, double b, double c, double d, double e, double f);

There are two additional global attributes which once defined take effect across all drawing's in current context. The Alpha value defines opaqueness of the drawing and it can vary from 0 to 1. For more details about globalCompositeOperation see this specification detail

attribute double globalAlpha; // (default 1.0)
attribute String globalCompositeOperation; // (default source-over)

Apart from strokeStyle and fillStyle attributes, there are below additional styling methods available for canvas.

CanvasGradient createLinearGradient(double x0, double y0, double x1, double y1);
CanvasGradient createRadialGradient(double x0, double y0, double r0, double x1, double y1, double r1);
CanvasPattern createPattern(HTMLImageElement image, DOMString repetition);
CanvasPattern createPattern(HTMLCanvasElement image, DOMString repetition);
CanvasPattern createPattern(HTMLVideoElement image, DOMString repetition);

And here is the text related attributes and methods

attribute DOMString font; // (default 10px sans-serif)
attribute DOMString textAlign; // "start", "end", "left", "right", "center" (default: "start")
attribute DOMString textBaseline; // "top", "hanging", "middle", "alphabetic", "ideographic", "bottom" (default: "alphabetic")
void fillText(DOMString text, double x, double y, optional double maxWidth);
void strokeText(DOMString text, double x, double y, optional double maxWidth);

You can use JSFiddle to easily test the various HTML5 canvas features. In the below JSFiddle window, I have included some of the basic tests. Feel free to play around and see for yourself.

Creating basic animation using timing control support of browsers

The specification "Timing control for script-based animations" defines API for script based animation which is to be implemented in browsers. The API provides advantage over traditional animation technique where Javascript timeout method is used to call animation function continuously after certain time interval.

The API provides browser way to take control of rate of frames per second in all animations. so that the browser can reduce the frame rate of certain animation when there are too many animations going on the page and optimize the resource usage. Also, browser can halt the frame updates when animation area is out of view.

The requestAnimationFrame method is used to signal to the user agent that a script-based animation needs to be resampled.

Since, the name of this method is non standard and all borwsers have used different function name in their implementations, we will need to include names used by all major browsers also, additional callback argument which includes setTimeout function so that our animation would still work in case of old, non compatible browsers.

 window.requestAnimFrame = (function(){
      return  window.requestAnimationFrame       ||
              window.webkitRequestAnimationFrame ||
              window.mozRequestAnimationFrame    ||
              window.oRequestAnimationFrame      ||
              window.msRequestAnimationFrame     ||
              function(callback){
                window.setTimeout(callback, 1000/60 );
              };
    })();

To create a animation, we now just need create a function which will call this requestAnimFrame function and draw the shape at incremental position evey time. See the below fiddle JavaScript code and result window to see the detailed code

Creating more complex example

we have had a look at the creating basic shape using HTML5 Canvas and then we looked at animation API. Based on this, we can create some complex animation logic. We will only need to have moderate JavaScript understanding.

I was playing around with HTML5 canvas. I thought of creating some demo stuff to test various aspects of HTML5 canvas. I recently came across very good site html5canvastutorials.com by Eric Rowell. He has very good tutorials, examples and some other stuff there.

I had look at the crazy snake tutorial and it reminded me about old mobile game called Snake. To explain HTML5 canvas feature, I have extended the original work by Eric

The game consists of moving snake which can be directed using direction keys of your keypad. A snake food will appear on game area at random places. The snake should go over the food which will increase the score by 1 and will create food shape on some other random place.

The snake could not hit the game boundary's. The only game rule I have left implementing is that snake can hit its own body and still can continue. Have a look and let me know :).

 

<html>
<head id="Head1" >
    <title>
       HTML 5 Canvas example - Snake Game (by KK)
    </title>
        <style>
            body {
                margin: 0px;
                padding: 0px;
            }
            
            #myCanvas {
                border: 1px solid #9C9898;
            }
            .clear { clear: both;}
        </style>
         
        <script type="text/javascript">
            //*************************************************************************************************************************************//
            //* This HTML5 Canvas example is created by Kedar Kulkarni (www.bluelemoncode.com).                                                   *//
            //* The below example uses basic code posted by Eric Rowell on http://www.html5canvastutorials.com/labs/html5-canvas-arc-crazy-snake/ *//
            //* While reusing this code elsewhere, Please keep this credit information                                                            *//
            //*************************************************************************************************************************************//

            //declare global variables
            var canvas;
            var snake;
            var keyPressed = false;
            var upTurned = false;
            var downTurned = false;
            var leftTurned = false;
            var rightTurned = false;
            var gameOver = false;
            var targetPresent = false;
            var timeout;
            var targetX = 0;
            var targetY = 0;
            var canvasWidth = 578;
            var canvasheight = 300;
            var snakeWidth = 8;
            var intScore = 0;
            var pointPerCatch = 10;
            var paused = false;
            var displayPauseText = 0;
            var snakePositionX = [];
            var snakePositionY = [];
            var record = 0;
            var recordTime;

            //add event listener to read up-down, left-right and escape key press
            //window.addEventListener('keydown', doKeyDown, true);

            if (window.addEventListener) {
                window.addEventListener('keydown', doKeyDown, true);
            } else if (window.attachEvent) {
                window.attachEvent('keydown', doKeyDown);
            } 
            
            //
            window.requestAnimFrame = (function (callback) {
                return window.requestAnimationFrame ||
                window.webkitRequestAnimationFrame1 ||
                window.mozRequestAnimationFrame ||
                window.oRequestAnimationFrame ||
                window.msRequestAnimationFrame ||
                function (callback) {
                    if (keyPressed == false) {
                        timeout = window.setTimeout(callback, 1000 / 60);
                    }
                };
            })();


            function getRandTheta() {
                return Math.random() * 2 * Math.PI;
            }

            function doKeyDown(evt) {
                switch (evt.keyCode) {
                    case 38:  /* Up arrow was pressed */
                        UpPressed();
                        break;
                    case 40:  /* Down arrow was pressed */
                        DownPressed();
                        break;
                    case 37:  /* Left arrow was pressed */
                        LeftPressed();
                        break;
                    case 39:  /* Right arrow was pressed */
                        RightPressed();
                        break;
                    case 27:  /* Escape key was pressed */
                        EscapePressed();
                        break;
                }
            }

            //*************************************************************************//
            //** Arrow key press events **//
            //upon arrow key pressed, cancel out all other arrow key press (ex. right, up, down) and change direction to left //
            function LeftPressed() {
                if (rightTurned == false) {
                    keyPressed = true;
                    var direction = snake.direction;
                    direction.y = direction.x - 1;

                    leftTurned = true;
                    rightTurned = false;
                    upTurned = false;
                    downTurned = false;

                    keyPressed = false;
                }
            }

            function RightPressed() {
                if (leftTurned == false) {
                    keyPressed = true;
                    var direction = snake.direction;
                    var old = direction.y;
                    direction.y = direction.x - 1;

                    leftTurned = false;
                    rightTurned = true;
                    upTurned = false;
                    downTurned = false;

                    keyPressed = false;
                }
            }

            function UpPressed() {
                if (downTurned == false) {
                    keyPressed = true;
                    var direction = snake.direction;
                    var old = direction.y;
                    direction.y = direction.y - 1;

                    leftTurned = false;
                    rightTurned = false;
                    upTurned = true;
                    downTurned = false;

                    keyPressed = false;
                }
            }

            function DownPressed() {
                if (upTurned == false) {
                    keyPressed = true;
                    var direction = snake.direction;
                    var old = direction.y;
                    direction.y = direction.y + 1;

                    leftTurned = false;
                    rightTurned = false;
                    upTurned = false;
                    downTurned = true;

                    keyPressed = false;
                }
            }

            //** Arrow key press events completed**//
            /*********************************************************/

            // If escape key is pressed then just set global variable paused to false
            function EscapePressed() {
                if (paused == false)
                    paused = true;
                else
                    paused = false;
            }

            //This is main function which takes care of updating position of snake on each frame updation.
            // The function is called iteratively and position of snake is updated.  
            function updateSnake(canvas, snake, context) {

                //If any valid key was pressed then dont update snake position for that instance
                if (keyPressed == false) {

                    var maxVariance = 0.2;
                    var snakeSpeed = 500; //px / s
                    var segmentsPerSecond = snakeSpeed / snake.segmentLength;
                    var segments = snake.segments;
                    var date = new Date();
                    var time = date.getTime();
                    var timeDiff = (time - snake.lastUpdateTime);
                    if (timeDiff > 1000 / segmentsPerSecond) {
                        var head = segments[segments.length - 1];
                        var neck = segments[segments.length - 2];

                        var direction = snake.direction;
                        var newHeadX;
                        var newHeadY;
                        //based on direcction key pressed, chnage new position of snake start
                        if (downTurned == false && upTurned == false) {
                            if (leftTurned == true)
                                newHeadX = head.x - direction.x * snake.segmentLength;
                            else if (rightTurned == true)
                                newHeadX = head.x + direction.x * snake.segmentLength;
                            else
                                newHeadX = head.x + direction.x * snake.segmentLength;
                        }
                        else {
                            newHeadX = head.x;

                        }

                        if (leftTurned == false && rightTurned == false) {
                            newHeadY = head.y + direction.y * snake.segmentLength;
                        }
                        else {
                            newHeadY = head.y;
                        }

                        // if snake hit boundary of frame(either side boundary or top/bottom) then call stopgame function and return
                        if (newHeadX > canvas.width || newHeadX < 0) {
                            StopGame();
                            return;
                            direction.x *= -1;
                        }
                        if (newHeadY > canvas.height || newHeadY < 0) {
                            StopGame();
                            return;
                            direction.y *= -1;
                        }

                        // add new segment
                        segments.push({
                            x: newHeadX,
                            y: newHeadY
                        });

                        if (segments.length > snake.numSegments) {
                            segments.shift();
                        }

                    }

                }
                snake.lastUpdateTime = time;
            }


            //The function is called iteratively which in turn calls other functions to update snake position values 
            //and drawing snake on canvas and drawing snake food
            function animate(canvas, snake) {
                var context = canvas.getContext("2d");
                //If game was not paused then cotinue
                if (paused == false) {

                    // update
                    updateSnake(canvas, snake, context);

                    // clear
                    context.clearRect(0, 0, canvas.width, canvas.height);

                    // draw
                    drawSnake(context, snake);

                    // Draw snake food as target
                    drawSnakeFood(context, snake);
                }
                //If game was paused then draw pause window and stop the game
                else {
                    // clear
                    context.clearRect(0, 0, canvas.width, canvas.height);

                    // draw
                    drawSnake(context, snake);

                    // Draw snake food as target
                    drawSnakeFood(context, snake);

                    context.globalAlpha = 0.5;
                    context.beginPath();
                    context.rect(10, 10, canvas.width - 20, canvas.height - 20);
                    context.fillStyle = "blue";
                    context.fill();
                    context.globalAlpha = 1;

                    context.font = "30pt Verdana";
                    context.fillStyle = "red";
                    context.lineWidth = 1;
                    context.fillText("Paused", 210, 150);
                    context.font = "10pt Verdana";
                    context.fillStyle = "green";
                    context.fillText("Press Escape again to continue", 180, 180);
                    context.stroke();

                }

                //If game is not over then continue (call animate function recursively) else call stopgame function
                requestAnimFrame(function () {
                    if (gameOver == false)
                        animate(canvas, snake);
                    else
                        StopGame();
                });
            }

            //Draw snake using HTML5 canvas and values present in snake segment (which was filled in Drawsnake function)
            function drawSnake(context, snake) {
                var segments = snake.segments;
                var tail = segments[0];
                context.beginPath();
                context.moveTo(tail.x, tail.y);
                var tempX = [];
                var tempY = [];
                for (var n = 1; n < segments.length; n++) {
                    var segment = segments[n];
                    context.lineTo(segment.x, segment.y);

                }
                context.lineWidth = snakeWidth;
                context.lineCap = "round";
                context.lineJoin = "round";
                context.strokeStyle = "blue";
                context.stroke();

            }


            //Function to draw snake food at random location in canvas
            //it also take care of increasing the game score of axis of snake segment hit the snake food location
            function drawSnakeFood(context, snake) {

                if (targetX == 0 && targetY == 0) {
                    targetX = Math.round(Math.random() * (canvasWidth - snakeWidth));
                    targetY = Math.round(Math.random() * (canvasheight - snakeWidth));
                }

                var segments = snake.segments;
                for (var n = 1; n < segments.length; n++) {
                    var segment = segments[n];
                    if ((segment.x >= targetX && segment.y >= targetY) && (segment.x <= targetX + snakeWidth && segment.y <= targetY + snakeWidth)) {
                        targetX = Math.round(Math.random() * (canvasWidth - snakeWidth));
                        targetY = Math.round(Math.random() * (canvasheight - snakeWidth));
                        intScore = intScore + pointPerCatch;
                        document.getElementById("lblScore").innerHTML = intScore;
                        snake.numSegments = snake.numSegments + 5;
                    }
                }
                context.beginPath();
                context.rect(targetX, targetY, snakeWidth, snakeWidth);

                context.fillStyle = "black";
                context.fill();
                context.lineWidth = 0;
            }


            // stopGame function is called explitly whenever the snake hit canvas boundary
            // The function clears then canvas content and draw game over screen.
            // It also draws a Play Again button and add event handler in button area to start the game again
            function StopGame() {
                gameOver = true;
                clearTimeout(timeout);
                sleep(1000);
                var canvas = document.getElementById("myCanvas");
                var context = canvas.getContext("2d");
                context.clearRect(0, 0, canvas.width, canvas.height);
                canvas.addEventListener("click", StartGame, false);
                context.beginPath();
                var myRectangle = {
                    x: 30,
                    y: 30,
                    width: 520,
                    height: 230,
                    borderWidth: 2
                };

                var StartGameRect = {
                    x: 250,
                    y: 180,
                    width: 100,
                    height: 30,
                    borderWidth: 1
                };

                context.rect(myRectangle.x, myRectangle.y, myRectangle.width, myRectangle.height);

                context.fillStyle = "#336699";
                context.fill();
                context.lineWidth = myRectangle.borderWidth;
                context.strokeStyle = "black";
                context.stroke();
                context.font = "20pt Calibri";
                context.fillStyle = "White";
                context.fillText("Game Over", canvas.width / 2 - 50, canvas.height / 2);
                context.stroke();
                context.lineWidth = StartGameRect.borderWidth;
                context.strokeStyle = "black";
                context.fillStyle = "#006600";
                context.strokeStyle = "black";
                context.fillRect(StartGameRect.x, StartGameRect.y, StartGameRect.width, StartGameRect.height);
                context.stroke();
                context.font = "10pt Verdana";
                context.fillStyle = "white";
                context.lineWidth = 1;
                context.fillText("Play Again", 265, 200);
                context.stroke();

            }

            // Function is used to initialise the snake again and start the aniation
            function StartGame(e) {
                var x;
                var y;

                if (e.pageX || e.pageY) {
                    
                    x = e.pageX;
                    y = e.pageY;
                    
                    if ((x > 250 && y > 240) && (x < 350 && y < 270)) {
                        
                        var canvas = document.getElementById("myCanvas");
                        var context = canvas.getContext("2d");
                        
                        var segmentLength = 2; // px
                        var headX = canvas.width / 2;
                        var headY = canvas.height / 2;
                        snake.direction.x = 1;
                        snake.direction.y = 0;
                        snake.lastUpdateTime = 0;
                        snake.segmentLength = 2;
                        snake.numSegments = 50;                        
                        snake.segments.length=0;
                        
                        snake.segments.push({
                            x: headX + segmentLength,
                            y: headY
                        });

                        snake.segments.push({
                            x: headX,
                            y: headY
                        });
                        
                        leftTurned = false;
                        rightTurned = false;
                        upTurned = false;
                        downTurned = false;
                        gameOver = false;
                        intScore = 0;
                        document.getElementById("lblScore").innerHTML = '0';

                        keyPressed = false;                        
                        animate(canvas, snake);
                    }
                }
            }

            //sleep function is used to halt the game before game over screen when snake hit the canvas boundary
            function sleep(milliSeconds) {
                var startTime = new Date().getTime(); // get the current time 
                while (new Date().getTime() < startTime + milliSeconds); // hog cpu 
            }

            //The snake segment and current position is initialised on body load and animation is started
            window.onload = function () {
                var canvas = document.getElementById("myCanvas");
                var segmentLength = 2; // px
                var headX = canvas.width / 2;
                var headY = canvas.height / 2;

                snake = {
                    segmentLength: 2,
                    lastUpdateTime: 0,
                    numSegments: 50,
                    // moving to the right
                    direction: {
                        x: 1,
                        y: 0
                    },
                    segments: [{
                        // tail
                        x: headX + segmentLength,
                        y: headY
                    }, {
                        // head
                        x: headX,
                        y: headY
                    }]
                };
                document.getElementById("lblScore").innerHTML = '0';
                animate(canvas, snake);
            };
        </script>
</head>
<body>
    <form id="form1">
    <div style="width:605px;height:20px;border:1px;border-color:black;border-style:solid;background-color:White;color:#336699;font-size:smaller;font-family:Verdana;vertical-align:middle;text-align:center"  >
       This game is developed by KedarRKulkarni for HTML5 canvas functionality demo
    </div>
        <div style="width:605px;height:380px;border:1px;border-color:black;border-style:solid;background-color:Gray;" >
        <div style="width:578px;height:20px;border:1px;border-color:black;border-style:solid;background-color:#336699;margin-left:10px;margin-top:10px;vertical-align:middle;color:White;font-family:Verdana;font-size:x-small;font-weight:bold">
            Press Escape to pause the game
        </div>                      

        <!-- HTML5 Cansas tag -->
        <canvas id="myCanvas" width="578" height="300" style="border:solid 1px #000000;background-color:White;margin-left:10px;margin-top:5px">
        </canvas>

        <br />
        <div style="text-align:justify;width:578px;height:30px;border:1px;border-color:black;
            border-style:solid;background-color:#336699;margin-left:10px;vertical-align:middle;
            color:White;font-family:Verdana; font-size:smaller;font-weight:bold">            
            <div style="width:189px;height:30px;text-align:left; float: left;">
                Your score : <label id="lblScore"  style="line-height:30px"></label> 
            </div>        
        </div>
        <div class"clear"></div>
        <label id="Label1"  style="line-height:30px"></label>
        </div>
    </form>
</body>
</html>

Conclusion

HTML 5 canvas is new and not still in draft version. However it can be seen that almost all major browser's are adopting it in their new updates. Since the supports for canvas comes from within the user agent, it is very easy to use.

However, to make our web apps compatible with old browsers, fallback options should also be available.

Thanks for visiting my web site. If you would like to have more information about some part of this article then please let me know or if you just want to be critical then you are most wel-come. I will try to improve :)

About Me

You are visiting personal website of Kedar (KK)

Please go here to know more about me

Disclaimer

The opinions expressed here represent my own and not those of my past or present employers.

The concept/code provided on this site may not work as described. If you are using any code provided on this site. Then, please test it thoroughly. I shall not be responsible for any issues arising in the code. 

Month List