HTML5 Canvas Game

Introduction

The <canvas> element is one of the most popular elements of HTML5. It is widely supported by popular browsers like Firefox, Chrome, Safari, and Opera (Internet Explorer supports it in their IE9 beta version).

This article describes the use of the HTML5′s <canvas> element through a fun example of bouncing multiple balls.

What is Canvas?

The <canvas> element is used to draw graphics on a web page by scripting.

The <canvas> element is only a container for graphics. You must use a script to actually draw the graphics. Canvas has several methods for drawing paths, boxes, circles, characters, and adding images.

The <canvas> element has only two attributes, width, and height. These are both optional and can also be set using DOM properties. When no width and height attributes are specified, the canvas will initially be 300 pixels wide and 150 pixels high. The element can be sized arbitrarily by CSS, but during rendering, the image is scaled to fit its layout size.

The <canvas> element can be styled just like any normal image (margin, border, background, etc).

Canvas Element

Let's add our <canvas> element inside the <body> tag. Though we only have one <canvas> element in our HTML for this example, I have still assigned an ID to it (myCanvas) just to make it easier to select it later on via JavaScript. I also defined the element's dimensions (width and height), however, you could just as well do that via CSS by targeting the #myCanvas ID.

JavaScript

Let's begin with our actual work of creating our shapes in JavaScript.

Drawing Balls

We will draw the balls using the arc( ) and fill( ) methods. Basically, we define the context, initiate the drawing, then we use color and style methods to fill in the color and dictate the shape (using a Math object for the ball).

The script code has a set of variables and functions: initBalls( ), getMousePos( ), updateBalls( ), Ball( ) and animate( ).

getMousePos( ) method

It gets the position of the Canvas. It then gets the relative mouse position.

function getMousePos(canvas, evt) {
    // get canvas position
    var obj = canvas;
    var top = 0;
    var left = 0;
    while (obj.tagName != 'BODY') {
        top += obj.offsetTop;
        left += obj.offsetLeft;
        obj = obj.offsetParent;
    }
    // return relative mouse position
    var mouseX = evt.clientX - left + window.pageXOffset;
    var mouseY = evt.clientY - top + window.pageYOffset;
    return {
        x: mouseX,
        y: mouseY
    };
}

// updateBalls() method
// In this method, we will set the following features:
// Set the position of the balls based on velocity
// ball.y += ball.vy;
// ball.x += ball.vx;
// Setting the Restore Force
// if (ball.x > ball.origX) {
// ball.vx -= restoreForce;
// } else {
// ball.vx += restoreForce;
// }
// if (ball.y > ball.origY) {
// ball.vy -= restoreForce;
// } else {
// ball.vy += restoreForce;
// }
// Setting the mouse force
// var mouseX = mousePos.x;
// var mouseY = mousePos.y;
// var distX = ball.x - mouseX;
// var distY = ball.y - mouseY;
// var radius = Math.sqrt(Math.pow(distX, 2) + Math.pow(distY, 2));
// var totalDist = Math.abs(distX) + Math.abs(distY);
// var forceX = (Math.abs(distX) / totalDist) * (1 / radius) * mouseForceMultiplier;
// var forceY = (Math.abs(distY) / totalDist) * (1 / radius) * mouseForceMultiplier;
// Setting the mouse on the left of the Ball
// if (distX > 0) {
// ball.vx += forceX;
// } else {
// ball.vx -= forceX;
// }
// Setting the mouse on the Right of the ball
// if (distY > 0) {
// ball.vy += forceY;
// } else {
// ball.vy -= forceY;
// }
// Setting the floor friction
// if (ball.vx > 0) {
// ball.vx -= floorFriction;
// } else if (ball.vx < 0) {
// ball.vx += floorFriction;
// }
// if (ball.vy > 0) {
// ball.vy -= floorFriction;
// } else if (ball.vy < 0) {
// ball.vy += floorFriction;
// }

Limiting the area of the boundary

Now it's time to bounce the ball off the corners of the <canvas> element.

Setting up the limit for the floor condition.

if (ball.y > (canvas.height - ball.radius)) {
    ball.y = canvas.height - ball.radius - 2;
    ball.vy *= -1;
    ball.vy *= (1 - collisionDamper);
}

// Setting up the limit for the ceiling condition

if (ball.y < ball.radius) {
    ball.y = ball.radius + 2;
    ball.vy *= -1;
    ball.vy *= (1 - collisionDamper);
}

Setting up the limit for the right wall condition.

if (ball.x > (canvas.width - ball.radius)) {
    ball.x = canvas.width - ball.radius - 2;
    ball.vx *= -1;
    ball.vx *= (1 - collisionDamper);
}

// Setting up the limit for the left wall condition

if (ball.x < ball.radius) {
    ball.x = ball.radius + 2;
    ball.vx *= -1;
    ball.vx *= (1 - collisionDamper);
}

// Creating the ball constructor
function Ball(x, y, vx, vy, color) {
    this.x = x;
    this.y = y;
    this.vx = vx;
    this.vy = vy;
    this.color = color;
    this.origX = x;
    this.origY = y;
    this.radius = 10;
}

Setting the mouse position really far away so the mouse forces are nearly obsolete.

var mousePos = { x: 9999, y: 9999 };

Move the Balls

Now that we have the balls, let's try to move them. We'll replace the hardcoded values of the coordinates in the .arc method with variables x and y, that we will then increment by an amount of dx and dy.

// move balls
ball.y += ball.vy;
ball.x += ball.vx;

// draw it
context.arc(ball.x, ball.y, ball.radius, 0, 2 * Math.PI, false);
context.fillStyle = ball.color;
context.fill();

Example

<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="utf-8" />
    <title>Canvas Game</title>
    <style>
        body {
            margin: 0px;
            padding: 0px;
        }
    </style>
</head>
<body>
    <canvas id="myCanvas" width="578" height="200" style="border: 2px solid black;"></canvas>
    <script>
        window.requestAnimFrame = (function (callback) {
            return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame ||
            function (callback) {
                window.setTimeout(callback, 1000 / 60);
            };
        })();

        function initBalls() {
            balls = [];

            var blue = '#3A5BCD';
            var red = '#EF2B36';
            var yellow = '#FFC636';
            var green = '#02A817';

            // G
            balls.push(new Ball(173, 63, 0, 0, blue));
            balls.push(new Ball(158, 53, 0, 0, blue));
            balls.push(new Ball(143, 52, 0, 0, blue));
            balls.push(new Ball(130, 53, 0, 0, blue));
            balls.push(new Ball(117, 58, 0, 0, blue));
            balls.push(new Ball(110, 70, 0, 0, blue));
            balls.push(new Ball(102, 82, 0, 0, blue));
            balls.push(new Ball(104, 96, 0, 0, blue));
            balls.push(new Ball(105, 107, 0, 0, blue));
            balls.push(new Ball(110, 120, 0, 0, blue));
            balls.push(new Ball(124, 130, 0, 0, blue));
            balls.push(new Ball(139, 136, 0, 0, blue));
            balls.push(new Ball(152, 136, 0, 0, blue));
            balls.push(new Ball(166, 136, 0, 0, blue));
            balls.push(new Ball(174, 127, 0, 0, blue));
            balls.push(new Ball(179, 110, 0, 0, blue));
            balls.push(new Ball(166, 109, 0, 0, blue));
            balls.push(new Ball(156, 110, 0, 0, blue));

            // O
            balls.push(new Ball(210, 81, 0, 0, red));
            balls.push(new Ball(197, 91, 0, 0, red));
            balls.push(new Ball(196, 103, 0, 0, red));
            balls.push(new Ball(200, 116, 0, 0, red));
            balls.push(new Ball(209, 127, 0, 0, red));
            balls.push(new Ball(223, 130, 0, 0, red));
            balls.push(new Ball(237, 127, 0, 0, red));
            balls.push(new Ball(244, 114, 0, 0, red));
            balls.push(new Ball(242, 98, 0, 0, red));
            balls.push(new Ball(237, 86, 0, 0, red));
            balls.push(new Ball(225, 81, 0, 0, red));

            // O
            var oOffset = 67;
            balls.push(new Ball(oOffset + 210, 81, 0, 0, yellow));
            balls.push(new Ball(oOffset + 197, 91, 0, 0, yellow));
            balls.push(new Ball(oOffset + 196, 103, 0, 0, yellow));
            balls.push(new Ball(oOffset + 200, 116, 0, 0, yellow));
            balls.push(new Ball(oOffset + 209, 127, 0, 0, yellow));
            balls.push(new Ball(oOffset + 223, 130, 0, 0, yellow));
            balls.push(new Ball(oOffset + 237, 127, 0, 0, yellow));
            balls.push(new Ball(oOffset + 244, 114, 0, 0, yellow));
            balls.push(new Ball(oOffset + 242, 98, 0, 0, yellow));
            balls.push(new Ball(oOffset + 237, 86, 0, 0, yellow));
            balls.push(new Ball(oOffset + 225, 81, 0, 0, yellow));

            // G
            balls.push(new Ball(370, 80, 0, 0, blue));
            balls.push(new Ball(358, 79, 0, 0, blue));
            balls.push(new Ball(346, 79, 0, 0, blue));
            balls.push(new Ball(335, 84, 0, 0, blue));
            balls.push(new Ball(330, 98, 0, 0, blue));
            balls.push(new Ball(334, 111, 0, 0, blue));
            balls.push(new Ball(348, 116, 0, 0, blue));
            balls.push(new Ball(362, 109, 0, 0, blue));
            balls.push(new Ball(362, 94, 0, 0, blue));
            balls.push(new Ball(355, 128, 0, 0, blue));
            balls.push(new Ball(340, 135, 0, 0, blue));
            balls.push(new Ball(327, 142, 0, 0, blue));
            balls.push(new Ball(325, 155, 0, 0, blue));
            balls.push(new Ball(339, 165, 0, 0, blue));
            balls.push(new Ball(352, 166, 0, 0, blue));
            balls.push(new Ball(367, 161, 0, 0, blue));
            balls.push(new Ball(371, 149, 0, 0, blue));
            balls.push(new Ball(366, 137, 0, 0, blue));

            // L
            balls.push(new Ball(394, 49, 0, 0, green));
            balls.push(new Ball(381, 50, 0, 0, green));
            balls.push(new Ball(391, 61, 0, 0, green));
            balls.push(new Ball(390, 73, 0, 0, green));
            balls.push(new Ball(392, 89, 0, 0, green));
            balls.push(new Ball(390, 105, 0, 0, green));
            balls.push(new Ball(390, 118, 0, 0, green));
            balls.push(new Ball(388, 128, 0, 0, green));
            balls.push(new Ball(400, 128, 0, 0, green));

            // E
            balls.push(new Ball(426, 101, 0, 0, red));
            balls.push(new Ball(436, 98, 0, 0, red));
            balls.push(new Ball(451, 95, 0, 0, red));
            balls.push(new Ball(449, 83, 0, 0, red));
            balls.push(new Ball(443, 78, 0, 0, red));
            balls.push(new Ball(430, 77, 0, 0, red));
            balls.push(new Ball(418, 82, 0, 0, red));
            balls.push(new Ball(414, 93, 0, 0, red));
            balls.push(new Ball(412, 108, 0, 0, red));
            balls.push(new Ball(420, 120, 0, 0, red));
            balls.push(new Ball(430, 127, 0, 0, red));
            balls.push(new Ball(442, 130, 0, 0, red));
            balls.push(new Ball(450, 125, 0, 0, red));

            return balls;
        }

        function getMousePos(canvas, evt) {

            var obj = canvas;
            var top = 0;
            var left = 0;
            while (obj.tagName != 'BODY') {
                top += obj.offsetTop;
                left += obj.offsetLeft;
                objobj = obj.offsetParent;
            }

            var mouseX = evt.clientX - left + window.pageXOffset;
            var mouseY = evt.clientY - top + window.pageYOffset;
            return {
                x: mouseX,
                y: mouseY
            };
        }

        function updateBalls(canvas, balls, timeDiff, mousePos) {
            var context = canvas.getContext('2d');
            var collisionDamper = 0.3;
            var floorFriction = 0.0005 * timeDiff;
            var mouseForceMultiplier = 1 * timeDiff;
            var restoreForce = 0.002 * timeDiff;

            for (var n = 0; n < balls.length; n++) {
                var ball = balls[n];

                ball.y += ball.vy;
                ball.x += ball.vx;

                if (ball.x + ball.radius > canvas.width) {
                    ball.vx = -ball.vx;
                    ball.x = canvas.width - ball.radius;
                } else if (ball.x - ball.radius < 0) {
                    ball.vx = -ball.vx;
                    ball.x = ball.radius;
                }

                if (ball.y + ball.radius > canvas.height) {
                    ball.vy = -ball.vy;
                    ball.y = canvas.height - ball.radius;
                } else if (ball.y - ball.radius < 0) {
                    ball.vy = -ball.vy;
                    ball.y = ball.radius;
                }

                var ballMouseDiffX = ball.x - mousePos.x;
                var ballMouseDiffY = ball.y - mousePos.y;
                var ballMouseDist = Math.sqrt(ballMouseDiffX * ballMouseDiffX + ballMouseDiffY * ballMouseDiffY);

                var mouseForce = Math.max(Math.min(20000 / (ballMouseDist * ballMouseDist), 10), 1);

                if (ballMouseDist < 150) {
                    ball.vx += mouseForceMultiplier * mouseForce * ballMouseDiffX / ballMouseDist;
                    ball.vy += mouseForceMultiplier * mouseForce * ballMouseDiffY / ballMouseDist;
                }

                var dampingFactor = 1 - (collisionDamper * (1 - floorFriction));
                ball.vx *= dampingFactor;
                ball.vy *= dampingFactor;

                ball.vx += restoreForce * ball.restoreForceX;
                ball.vy += restoreForce * ball.restoreForceY;

                ball.restoreForceX = 0;
                ball.restoreForceY = 0;
            }
        }

        function Ball(x, y, vx, vy, color) {
            this.x = x;
            this.y = y;
            this.vx = vx;
            this.vy = vy;
            this.color = color;
            this.radius = 10;
            this.restoreForceX = 0;
            this.restoreForceY = 0;
        }

        function drawBalls(context, balls) {
            for (var n = 0; n < balls.length; n++) {
                var ball = balls[n];
                context.beginPath();
                context.arc(ball.x, ball.y, ball.radius, 0, 2 * Math.PI, false);
                context.fillStyle = ball.color;
                context.fill();
            }
        }

        function animate(canvas, context, lastTime, balls, mousePos) {
            if (!lastTime) {
                lastTime = Date.now();
            }
            var time = Date.now();
            var timeDiff = time - lastTime;

            updateBalls(canvas, balls, timeDiff, mousePos);
            drawBalls(context, balls);

            lastTime = time;

            requestAnimFrame(function () {
                animate(canvas, context, lastTime, balls, mousePos);
            });
        }

        var canvas = document.getElementById('myCanvas');
        var context = canvas.getContext('2d');
        var balls = initBalls();
        var mousePos = {
            x: 400,
            y: 300
        };

        canvas.addEventListener('mousemove', function (evt) {
            var mousePosTemp = getMousePos(canvas, evt);
            mousePos.x = mousePosTemp.x;
            mousePos.y = mousePosTemp.y;
        }, false);

        animate(canvas, context, false, balls, mousePos);
    </script>
</body>
</html>

Output

Initially, at the beginning we get

Game1

On mouseHover we get

game2

Finally, at last we get

Game3