In the first part of this series we set the stage by putting together our framework and then getting a simple background on screen. This article will add our rendering loop to the code and let us actually get some bubbles out there.
Loop the loop
In previous canvas demos I’ve done animation by setting up a timer using that to drive animation – update and draw on regular intervals and hope the browser can keep up. Quick and simple but not ideal – it keeps going in the background, not great for devices with batteries – and things start to fall apart if frames take longer than the timer interval.
So here we’re going to do things properly and that means using requestAnimationFrame. Essentially, what this does is tell the browser that we want to draw something. When it’s ready – and the browser aims for 60FPS – it calls the function we provided. So our “let’s get started” code becomes:
window.requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame; window.onload = world.init;
That’s a simple (if dirty) way of avoiding having to check which browser we’re in and which function to use. If our browser supports requestAnimationFrame
then nothing changes. If it only supports mozRequestAnimationFrame
then we use that, and so on.
Then later, when we want to start animating, we do this:
pub.init = function() { setupCanvas(); theme = new Theme(); background = getBackground(); window.requestAnimationFrame(update); } function update(time) { context.drawImage(background, 0, 0, width, height, 0, 0, width, height); }
Sort of. That only gives us one frame, and then we stop. If you put an alert or some console output in the update method you’ll see update
is only called once. What we need to do is request another frame once we’ve finished with our first one.
function update(time) { context.drawImage(background, 0, 0, width, height, 0, 0, width, height); window.requestAnimationFrame(update); }
Et voilà, we’ve got a rendering loop. Still not much use though, is it?
Note the time passed to the function. That tells us how long has elapsed since we started, and is necessary for calculating what has changed between frames. FPS counter anyone?
var lastFrameTime = 0; function update(time) { context.drawImage(background, 0, 0, width, height, 0, 0, width, height); frameTime = time - lastFrameTime; lastFrameTime = time; var fps; if (frameTime > 0) { fps = Math.floor(1000 / frameTime); } else { fps = "-"; } context.font = "16px Courier New"; context.fillText(fps, 10, 20 ); console.log(fps); window.requestAnimationFrame(update); }
Since each frame doesn't take a consistent amount of time - for me it's between 58 and 62ms - the counter looks a bit jerky. Here's an improved version, following our"if we're going to do something do it properly" ethos:
function FpsCounter(length) { this.times = []; this.length = length; this.lastTime = 0; this.delta = 0; } FpsCounter.prototype.update = function(time) { this.delta = time - this.lastTime; this.lastTime = time; this.times.push(this.detla); if (this.times.length > this.length) this.times.shift(); } FpsCounter.prototype.getFps = function() { if (this.times.length == 0) return "-"; var total = 0; for (var index = 0; index < this.times.length; index++) total += this.times[index]; return Math.round(1000 / (total / this.times.length)); } FpsCounter.prototype.render = function(content) { context.font = theme.counterFont; //"16px Courier New" context.fillStyle = theme.counterCol; //"black" context.fillText(this.getFps(), 10, 20); } ... function update(time) { context.drawImage(background, 0, 0, width, height, 0, 0, width, height); fpsCounter.update(time); fpsCounter.render(context); window.requestAnimationFrame(update); }
Much better. Given that so far we're doing almost nothing you should be seeing around 59-60 FPS.
Just give me some bubbles already
OK, OK. Bubbles. Bear with me one minute while we add a class to hold some of the value we'll need first:
function Config(width, height) { this.width = width; this.height = height; this.bubbleTimeOnScreen = 15000; this.baseSpeed = this.height / this.bubbleTimeOnScreen; }
width
and height
just take the values out of the World module itself. bubbleTimeOnScreen
tells us how fast a bubble should move, and baseSpeed
uses that to tell us how many pixels per second a bubble should move based on that time.
Add a config object to our World module, instantiate is in setupCanvas()
, and update all our references to width
and height
. Now we’re ready for bubbles. Almost. We want to actually draw them, and we’re keeping the styling information in our theme
class (it would be perfectly valid to say that a bubble should define it’s own theme, or that a bubble should be given a theme as a property – feel free to go your own way here):
function Theme() { this.backColTop = "#9DD9DA"; this.backColBottom = "#38828D"; this.bubbleStroke = "rgba(255, 255, 255, 0.3)"; this.bubbleFill = "rgba(255, 255, 255, 0.2)"; this.bubbleStrokeWidth = 2; this.counterFont = "16px Courier New"; this.counterCol = "black"; }
Finally, bubbles:
function Bubble(x, y) {
this.radius = 50;
this.x = x;
this.y = y + this.radius;
this.speed = config.baseSpeed * [1]Math.random() * 0.6) + 0.8);
}
Bubble.prototype.move = function(time) {
this.y -= this.speed * time;
}
Bubble.prototype.render = function(context) {
context.strokeStyle = … Continue reading;
}
This function adds a new bubble at a random point at the bottom of the canvas, ready to start heading up. One of the joys of coding this way is that actually bringing everything together suddenly only takes a few lines of code in our update
function:
for (index = 0; index < bubbles.length; index++) { bubbles[index].move(frameTime); bubbles[index].render(context); } for (index = bubbles.length - 1; index >= 0; index --) { if (bubbles[index].y < -bubbles[index].radius || bubbles[index].cX < -bubbles[index].radius || bubbles[index].cY > (config.width - bubbles[index].radius)) { bubbles.splice(index, 1); addBubble(); } }
And there we have it. A background, render loop, an FPS counter and some actual bubbles. The finished version of today’s script can be found here.
Next time: making it interactive with mouse and touch events. Bubble murder simulator.
Feet
↑1 | Math.random() * 0.6) + 0.8);
}
Bubble.prototype.move = function(time) {
this.y -= this.speed * time;
}
Bubble.prototype.render = function(context) {
context.strokeStyle = theme.bubbleStroke;
context.lineWidth = theme.bubbleStrokeWidth;
context.fillStyle = theme.bubbleFill;
context.beginPath();
context.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
context.fill();
context.stroke();
}
And now to actually add some to our canvas: function addBubble() { bubbles.push(new Bubble(Math.random() * (config.width - 200) + 100, config.height |
---|