Games never draw directly to the screen. It doesn't matter how fast your computer is, or how good your video card. Any direct drawing the 'live' or real video buffer will make for an unpleasant viewing experience.
The solution is called a double buffer. If you like, a second copy of the screen that isn't visible. The screen is just a large block of memory with bytes representing RGB values. Modern video cards too have special fast memory for this very purpose.
So, one does all ones drawing to the off-screen buffer, and then in one foul swoop copy the contents of that invisible buffer to the visible buffer.
Creating the off-screen buffer is simple in java-script. We just create another canvas (but not one that lives inside the DOM, the visible HTML if you like).
// create a second off-screen buffer
var offscreenCanvas = document.createElement('canvas');
offscreenCanvas.width = 1024;
offscreenCanvas.height = 768;
offscreenContext = offscreenCanvas.getContext('2d');
As you can see, this is completely compatible with the drawing routines we looked at before. I've made the "offscreenCanvas" in this example 1024 pixels wide and 768 pixels high. Usually your double buffer would be exactly the same size as the visible canvas we will be copying it to. You can however have more than one offscreen buffer. The reason you'd do this is to perhaps draw something like a split screen (e.g. two players on one screen in their own seperate areas). You could make the offscreenCanvas smaller than the destination buffer and leave some room on the destination buffer for status displays etc.
How do we copy the contents of this offscreen buffer to the real one? Simple: given that "screenContext" is the 2d context of the canvas on the screen, we use yet another variant of drawImage. This one takes the offscreen CANVAS as a parameter, and the location of this buffer inside the other one. (0,0) is the top left corner. It is important to not get confused between the CANVAS and the CONTEXT.
The CONTEXT is the thing that gets drawn into. The CANVAS is the thing that is displayed. The first parameter in the double buffer copy is the offscreen CANVAS, NOT its CONTEXT. A CANVAS also has a "width" and a "height" (dimensions). The CONTEXT doesn't!
screenContext.drawImage( offscreenCanvas, 0, 0);
So lets create a simple sample using this. We need to use a timer, because we want to have something move around.
Each frame/timer event we:
1. Clear the offscreen buffer with fillRect()
2. Draw whatever we need to inside the offscreen buffer
3. Copy the offscreen buffer to the screen buffer (if they are the same size we won't need to do any cleaning of the screen buffer since it will be cleared by the content of the offscreen buffer.
In this sample I will make red "balls" move around the screen. To do this we need to introduce a new drawing function for drawing circles. Its called "arc". "arc" takes 5 parameters, they are:
context.arc( x, y, startAngle, endAngle, counterClockwise );
where: x, y, is the center of the circle
startAngle the start angle in radians
endAngle tne end angle in radians
counterClockwise a boolean variable requesting the circle be drawn counter clockwise (or clockwise if false)
"arc" is actually a very versatile function that can draw many different kind of shapes. In particular it is good at drawing pac-man. We only need it to draw a circle though (sorry to spoil the fun, the fun comes later!)
We introduce two classes. The first class is our BouncyBall class, shown below.
// a bouncy ball
function BouncyBall( x, y, colour, speed, size, width, height, canvas )
{
var self = this;
var myX = x;
var myY = y;
var myCanvas = canvas;
var ballColour = colour;
var mySize = size;
var directionX = speed;
var directionY = speed;
var myWidth = width;
var myHeight = height;
// draw the ball
this.draw = function()
{
//draw a circle
myCanvas.beginPath();
myCanvas.fillStyle = ballColour;
myCanvas.arc( myX, myY, mySize, 0, Math.PI*2, true);
myCanvas.closePath();
myCanvas.fill();
};
this.move = function()
{
myX = myX + directionX;
myY = myY + directionY;
if ( directionX < 0 )
{
if ( myX < 0 )
{
directionX = speed;
}
}
else
{
if ( myX > myWidth )
{
directionX = -speed;
}
}
if ( directionY < 0 )
{
if ( myY < 0 )
{
directionY = speed;
}
}
else
{
if ( myY > myHeight )
{
directionY = -speed;
}
}
};
}
Now you're going to get another lecture in object oriented programming. The class above is a self contained class that can both move, and draw a ball. Nothing too complex. It is always important to seperate the MOVING (the logic) from the DRAWING (the rendering). The real power of the object oriented-ness of this class will come about through the Controller class below.
Creating the ball takes the following parameters (x, y, colour, speed, size, width, height, canvas). The x,y is its starting position on the screen. The colour is its colour. The speed how fast it moves. The size, how big it is. The width and height are the CONTEXT's size - so that the ball knows when its hit the edges of the screen. Canvas, the last parameter, is where to draw the ball (the offscreen buffer in this case.
The second class is the "controller" (or scene) class. It draws the balls and moves the balls. It coordinates everything and is the thing that is called by the timer.
// the glue/logic for this little demo
function Controller( numBalls, screenCtx, offscreenCanvas, offscreenCtx )
{
// store the two buffers
var offscreenCtx = offscreenCtx;
var offscreenCanvas = offscreenCanvas;
var screenBuf = screenCtx;
var myWidth = offscreenCanvas.width;
var myHeight = offscreenCanvas.height;
// a list of balls (empty initially, setup() populates it)
var ballList = [];
this.setup = function()
{
for ( var i = 0; i < numBalls; i++ )
{
// create balls at different positions of different sizes and speeds
ballList.push( new BouncyBall( 2 * i, 2 * i, "#ff0000", 2 + i / 10,
25 + i / 2, myWidth, myHeight, offscreenCtx ) );
}
};
this.drawAndMove = function()
{
// clear the offscreen canvas
offscreenContext.fillStyle = "#000000"; // colour black
offscreenContext.fillRect( 0, 0, myWidth, myHeight );
// draw and move the balls all in one go
for ( var i = 0; i < ballList.length; i++ )
{
ballList[i].draw();
ballList[i].move();
}
// do the double buffering
// scene complete - copy offscreen buffer
screenBuf.drawImage( offscreenCanvas, 0, 0);
};
};
I mentioned "object oriented" and "power" in the same sentence. This wasn't an oxymoron! The controller class with just a few lines of code can control 100s of balls all at once. Our example only uses 25 balls. I'd encourage you to download the sample below and play with it.
Finally - the "scene" and the timer are created
// create a controller with 25 balls moving all at once
var ctrl = new Controller( 25, screenContext,
offscreenCanvas, offscreenContext );
// initialise the balls (only needed once)
ctrl.setup();
// callback the controller's drawAndMove() function
// 20 times per second (20 x 50 = 1000 ms = 1 second)
window.setInterval( ctrl.drawAndMove, 50 );
The java-script and html for this sample can be
downloaded here.
The final little ball demo is quite mesmerizing. Different sized balls moving at different speeds and all moving in different directions.