Unleashing Your Inner Code Artist
After all that form drudgery, we deserve to have some fun as well, and the HTML5 <canvas> element certainly counts. Unlike the other elements, which we style with CSS and fill with text, <canvas> lets us draw anything we want inside its borders. That opens up a whole world for us that wasn't previously available: games, artwork, animations, rich interactive elements, graphs, and more.
Using <canvas> requires us to do a little bit of setup first. We'll need to create a canvas element, and assign it a height and width. You can style the element however you'd like, but the height and width will be used for the internal drawing space of the canvas, and stretch to fit its onscreen size. This can make your drawings blurry or out-of-proportion if the CSS size doesn't match the element size, so it's a good idea to start just sizing the element manually. Like a script tag, you must always close your <canvas> tag. Unlike a script tag, you can put fallback text between the opening and closing tags, and it will be shown in browsers that don't support canvas.
<canvas id="picasso" height=640 width=480></canvas>
Now that we have the element, we need to get a drawing context for it. The context is the object on which you actually issue drawing commands--the browser then takes those commands and handles painting them into the element. We want to get the 2D context. There is also a 3D context, which uses WebGL for creating graphics, but we'll stick to 2D for now. The function to get the context only works on the raw DOM element, and can't be called through jQuery, so we're going to use a regular DOM function to get it. document.querySelector works basically the same way as jQuery, but its selectors are limited only to valid CSS (no ":checked" or ":visible") and it will only grab one element.
var canvas = document.querySelector('#picasso');
var context = canvas.getContext('2d');
Now that we have the context, we can actually start drawing some pictures.
Imagine that the canvas element is a robot arm, like in a factory, but instead of a claw or a welding attachment it has a pen at the end. The person running the robot programs it with a series of moves on a screen--some with the pen down on the paper, some with it lifted for repositioning--and then presses a big green button and watches it perform the actual drawing. That's basically how canvas works: we start a drawing sequence with the beginPath() instruction, tell it where we want the pen to draw (lineTo()) and where we want it to lift (moveTo), and then we finally ask the robot to either outline the path we've described (stroke()), or fill it with color (fill()).
Here's an example that draws an X on the canvas. This code is live: changes in the JavaScript will be reflected in the canvas drawing on the right, so feel free to play with it and add your own drawing instructions.
//start a sequence of drawing commands
context.beginPath();
//move to the upper-left corner
context.moveTo(20, 20);
//draw down and over to the coordinates 100, 100
context.lineTo(100, 100);
//move to the upper-right part of the X
context.moveTo(100, 20);
//draw down to the lower-left
context.lineTo(20, 100);
//now that we have our plan, we ask the canvas to ink this path
context.stroke();
We can change the color of the line by altering the strokeStyle property of the drawing context (fillStyle affects filled shapes). Any valid CSS color is valid here: You can use color names ("green"), hex values ("#FC8CA3"), RGB and RGBA values ("rgba(255, 195, 200, .5)"), or HSL in browsers that support it. You can also change the width of the line using the lineWidth property. Here's the interesting thing about these properties: you can set them as many times as you want, but the only one that matters is the last one you set before calling stroke() or fill(). You can think of our artistic robot as using only a single marker per path: if you want to change colors, call stroke() to finish in the old color and beginPath() to start a new path before changing the stroke style.
context.beginPath();
context.moveTo(20, 20);
context.lineTo(20, 180);
context.lineTo(180, 180);
context.strokeStyle = "red";
context.fillStyle = "green";
context.lineWidth = 6;
//un-comment the next three lines to create a new path
//context.stroke();
//context.fill();
//context.beginPath();
context.moveTo(180, 180);
context.lineTo(180, 20);
context.lineTo(20, 20);
context.strokeStyle = "#08F";
context.fillStyle = "rgba(0, 0, 0, .4)";
context.lineWidth = 2;
context.stroke();
context.fill();
More Than Just Lines
By creating line-based paths and stroking or filling them, we can draw a lot of shapes. But we don't have to draw everything one line segment at a time, luckily. The canvas API provides functions for drawing circles and rectangles as well. Rectangles in particular, are fairly easy: we can call the rect() function, passing in the x and y coordinates, followed by the width and height of the rectangular path we want to create.
context.beginPath();
//draw a 30x40 rectangle at coordinates 10, 10
context.rect(10, 10, 30, 40);
context.fill();
Circles are a bit more difficult. Although it would be great to have a circle() function, the powers that be decided that it would be a "better" idea to only expose a method that creates arcs--sections of a circle, not a whole circle on its own.
context.arc(centerX, centerY, radius, startPosition, endPosition);
Even worse, the required arguments for the start and end position of the arc (meaning, where on the circle we should start and stop drawing with our canvas "pen") are not expressed in degrees. Instead, they're in radians. A full circle is 2π radians around, and a half-circle is π radians. If it were a clock, 0 on the circle would be 3 o'clock, and π would be at 9 o'clock, with midnight and 6 o'clock at .5π and 1.5π, respectively. This is obviously kind of a lot of math to draw a simple circle. Luckily, the dynamic nature of JavaScript means that we can add our own circle() function to the context, with some help from the built-in value Math.PI.
context.circle = function(x, y, radius) {
//draw a circle from 0 to 2π radians.
context.arc(x, y, radius, 0, Math.PI * 2);
};
Now we can draw circles as easily as we can draw rectangles.
context.circle = function(x, y, radius) {
context.arc(x, y, radius, 0, Math.PI * 2);
};
context.beginPath();
context.fillStyle = "red";
context.rect(20, 20, 50, 150);
context.fill();
context.beginPath();
context.strokeStyle = "blue";
context.circle(150, 80, 20);
context.stroke();
context.beginPath();
context.rect(90, 70, 20, 50);
context.circle(100, 70, 10);
context.fillStyle = "green";
context.strokeStyle = "black";
context.fill();
context.stroke();
Don't let the fact that it's annoying blind you to the uses of the arc function, however. It has plenty of uses, especially if (as below) we want to emulate certain classic arcade characters.
context.beginPath();
context.fillStyle = "#FF0";
//top and bottom of the mouth
var start = .3 * Math.PI;
var end = 1.8 * Math.PI;
context.arc(100, 100, 90, start, end);
context.lineTo(100, 100);
context.fill();
context.beginPath();
context.fillStyle = "black";
context.arc(100, 60, 10, 0, Math.PI * 2);
context.fill();
context.beginPath();
context.fillStyle = "magenta";
context.moveTo(60, 40);
context.lineTo(50, 0);
context.lineTo(20, 60);
context.lineTo(10, 20);
context.fill();
Incidentally, using the canvas API is a great way to see how functions can be useful as units of code. The code above draws a single figure at a single location. But if we were actually making an arcade game recreation, we'd probably want to do that more than once, especially for our enemy sprites. If we wrap up our drawing instructions in a function, and change all of our drawing to happen relative to some center coordinates, it becomes much easier to re-use.
var drawMsPacman = function(x, y) {
context.beginPath();
context.fillStyle = "#FF0";
//top and bottom of the mouth
var start = .3 * Math.PI;
var end = 1.8 * Math.PI;
context.arc(x, y, 90, start, end);
context.lineTo(x, y);
context.fill();
context.beginPath();
context.fillStyle = "black";
context.arc(x, y - 40, 10, 0, Math.PI * 2);
context.fill();
context.beginPath();
context.fillStyle = "magenta";
context.moveTo(x - 40, y - 60);
context.lineTo(x - 50, y - 100);
context.lineTo(x - 80, y - 40);
context.lineTo(x - 90, y - 80);
context.fill();
};
drawMsPacman(100, 100);
Further versions of this code could even accept arguments for new colors or rotation. Once we've packaged our drawing instructions up this way, we don't have to really worry about how they're implemented, or try to get them right every time they're used. We turn many lines of code into only one.
Animation
Once we have a <canvas> element where we can draw at will, it's natural to want to redraw, and thus to animate our artistic visions. Doing so means using functions again, as well as a new built-in function that we haven't seen before, called setTimeout(). We can ask the browser to call a function of our choice after a short delay using setTimeout(), by passing it the function and the number of milliseconds that we want to wait. This is not too different from the way that we pass a function to jQuery to be called for an event. Try it for yourself by running the following code on your browser console:
setTimeout( function() { console.log("Hello from the past!"); }, 1000);
In an animation, however, we want our code to repeat itself more than just one time. We wouldn't want to have to schedule hours worth of timeouts at the start, just in case someone wanted to watch our page for that long. Instead, we want each frame to trigger the next, so that when one finishes it sets the timeout for another frame. That's easy to do by giving our function a name, and having it create a timeout to call itself again. If this sounds a bit confusing, like lifting yourself up by the scruff of your own neck, it is a bit easier to understand with an example.
var echo = function() {
console.log('hello');
setTimeout(echo, 1000);
};
echo();
Once we call echo() the first time, it'll schedule itself to be called every second after that.
It's common on the Internet to see people recommending the use of setInterval for animations instead of setTimeout. The former is called the same way, with a function and a delay. But instead of scheduling a single repeat after n milliseconds, setInterval schedules your function to be called every n milliseconds going forward, forever.
Why isn't this a good idea? Surely, this is exactly what we're trying to do--to call a function over and over again, as long as the page is loaded. If setInterval can do that for us, without the goofy self-setTimeout line, why not take advantage of it? The answer lies in the very short timing of many animations, which may be called as quickly as 4 milliseconds later (that's 250 times per second) in order to be as smooth as possible. If all goes well, setInterval works fine. But consider the following code:
var broken = function() {
console.logg("Oops, we misspelled a function name.");
};
setInterval(broken, 100);
Now, ten times a second, your console will be filled with error messages, since there is no "console.logg" function to call. If you're trying to debug a crash somewhere, this flood of syntax errors will make it practically impossible to figure out--and might cause other side effects, if the part of the function before the crash did anything important. setInterval is a kind of infinite loop, and that should be cause for caution.
var notBroken = function() {
console.logg("Misspelled functions can cause no ruckus.");
setTimeout(notBroken, 100);
}
notBroken();
In this sample, by comparison, the bad function name will cause the script to stop before it schedules the next timeout. We'll get one error, and then the program will halt, waiting for us to dig in and investigate the crash. It may seem perverse, but by failing harder, our program is easier to fix and maintain.
We need one other crucial function before we can animate something properly. If we draw repeatedly to the canvas, say a moving circle, it will leave a trail behind as it travels across the screen. We need to erase each previous drawing before drawing a new circle, to create the illusion of movement. The clearRect() function on the canvas context is called the same way as rect(), but it will immediately erase everything within its bounds. You don't need to call fill() or stroke() to do so. We can erase everything on the screen by clearing a rectangle starting from 0, 0 and stretching the full size of the canvas:
context.clearRect(0, 0, canvas.width, canvas.height);
There are, in fact, a number of other ways to clear the canvas. Changing its width or height, for example, will typically cause the canvas to erase itself. These methods, however, while interesting and sometimes faster than clearRect(), do not create clear code. They hide their purpose. In the rare cases where we need an extremely fast screen erasure (such as a video game with many onscreen objects), this might be worth investigating. But for now, let's use clearRect() for the sake of legibility, if nothing else.
In the following example, we're going to create the animation of a bouncing ball. We'll start by creating an object that represents the ball, then work up from there: drawing one frame, animating many frames, checking the ball's position after each update, and clearing the previous drawing.
Practice Exercise
If you're of a certain age, that bouncing ball should probably have strong associations for you: it puts you within striking distance of the classic Pong arcade game. To practice your canvas kung fu, try creating a very simple Pong clone. Bear in mind the following requirements:
- It's easier to start off with a single-player variation on Pong--more like racquetball--where the player simply has to bounce the ball off the other side. Then you can add a computer-controlled paddle once you get that working.
- Remember to use offset() to convert mouse coordinates from a "mousemove" event into canvas coordinates, as described here.
- Remember that you don't have to do everything in the <canvas>. Game UI elements, such as scores or player names, can be easily accomplished with jQuery and elements above and below the canvas.
- Try to attack the larger problem in small steps. For example, in this case, we might break the problem of "Pong" down into:
- Create a Pong-style bouncing ball.
- Draw the paddle onscreen at one position.
- Connect the vertical paddle position to the mouse cursor.
- Check for collisions between the ball and paddle, and cause them to bounce.
- Disable bounces on the player's side of the screen, and instead give a point to the computer when that happens.