One of the features introduced in version 5 of HTML is the <canvas> element, which comes with a JavaScript API for two-dimensional graphics. (As we will see later, <canvas> is also used for WebGL, a 3D graphics API. However, some browsers, such as Internet Explorer 9, implement the 2D canvas API but do not support WebGL.) A canvas element occupies a rectangular area on a web page that can be used as a drawing surface. You can combine the canvas API with other parts of the JavaScript API to create animations and interactive applications.
A <canvas> element is included in the <body> of the web page in the usual way. It can use attributes named width and height to specify its size in pixels, and it will usually have an id attribute that can be used to access it from JavaScript. For example:
<canvas id="theCanvas" width="640" height="480"></canvas>
You can apply CSS styles to the canvas. For example, you can use CSS to add a background color or even a background image. When a canvas is first created, it is filled with fully transparent pixels, so the background color or background image will be visible. You could even use CSS to add a border, but that would affect the canvas coordinate system, so it's probably not a good idea. You can always draw a border using the canvas graphics API if you want one.
To draw on the canvas, you need a graphics context. A graphics context is an object that contains the functions that are used for drawing and also contains the graphics state (such as current color, line width, and font). The equivalent in Java is an object of type Graphics. To obtain a graphics context, call canvas.getContext("2d"), where canvas is the DOM object that represents the <canvas> element. Since not every browser supports canvas, you should check whether the operation is successful. One way to do that in in a try..catch statement:
try { canvas = document.getElementById("theCanvas"); graphics = canvas.getContext("2d"); } catch (e) { // report the error and stop processing }
Typically, you will store the graphics context in a global variable and use the same graphics context throughout your program. Note that all the drawing that is done in a canvas is cumulative. A canvas is never cleared automatically, so you need to clear it yourself when you want to start a new drawing. Note also that canvases are effectively double-buffered; this means that the drawing that you do in JavaScript is done off-screen and does not affect what the user sees until the JavaScript function ends and control is returned to the browser, which will copy the off-screen image onto the screen. In particular, you can't do animation by looping inside a JavaScript function; the user would only see the final frame of the animation.
The HTML file canvas-template.html contains the basic code needed to set up drawing in a canvas. It is meant to be used as a template that you can copy as a starting point for a canvas application.
In the rest of this section, graphics is a global variable that refers to a 2D canvas graphics context. Of course, you can use any name that you want for the variable, and you might even have several graphics contexts for drawing on several canvases on the same page. I also assume that canvas is a global variable that refers to the canvas DOM object, but in fact you could get that object from the graphics context as graphics.canvas when you need it.
Drawing on a canvas is based on stroking and filling shapes. To stroke a shape means to drag a virtual pen along the outline of the shape. Filling means to fill the interior of the shape. Stroking can be applied to any shape, including lines and to other shapes that have no interior. Filling such shapes has no effect. By default, the size of the pen that is used is one pixel, and the drawing is done in black.
The default coordinate system on a canvas is similar to Java: The unit of measure is one pixel; (0,0) is at the upper left corner; the x-coordinate increases to the right; and the y-coordinate increases downward. This coordinate system can be changed by applying transforms, as we will see later. The term "one pixel" here is not really correct. Probably, I should say something like "one nominal pixel." The unit of measure is one pixel at typical desktop resolution with no magnification. If you apply a magnification to a browser window, the unit of measure gets stretched. The unit of measure might also be different on very high resolution screens. In any case, however, the range of coordinates is from 0 to the canvas width horizontally and from 0 to the canvas height vertically.
When you stroke a shape, it's the center of the virtual pen that moves along the path. This is different from Java, where it is the upper left corner of pen that follows the path. So, for high-precision canvas drawing, it's common to use paths that pass through the centers of pixels rather than through their corners. For example, to draw a line that extends from the pixel with coordinates (100,200) to the pixel with coordinates (300,200), you would actually stroke the geometric line with endpoints (100.5,200.5) and (100.5,300.5). If you don't do this, the line will straddle two rows of pixels. Antialiasing would make such a line look like a translucent, two-pixel-wide line. However, I will not always worry about this level of precision.
Moving on to the actual drawing API, Rectangles are a special case. There are specific functions in the API for stroking and filling rectangles:
graphics.fillRect(x,y,w,h)
-- draws a filled rectangle with corner
at (x,y), with width w and with height h. If the width or
the height is less than or equal to zero, nothing is drawn.graphics.strokeRect(x,y,w,h)
-- strokes the outline of the same
rectangle.graphics.clearRect(x,y,w,h)
-- clears the rectangle by filling
it with fully transparent pixels, allowing the background of the canvas to show.If you want to clear or fill the entire canvas, you can fill a rectangle whose corner is (0,0), whose width is canvas.width and whose height is canvas.height, where canvas is the DOM object corresponding to the canvas element. (But note that this assumes that you have not applied a coordinate transformation to the graphics context.)
There are also functions for drawing text. Characters are considered to be shapes, so it is possible both to fill text and to stroke it. To stroke text means to drag the pen along the outlines of the characters. This can give an interesting effect, but it only makes sense if the font size is rather large. Functions for drawing text include
graphics.fillText(str,x,y)
-- fills the characters in the string str.
The left end of the baseline of the string is positioned at the point (x,y).graphics.strokeText(str,x,y)
-- strokes the outlines of the characters in the same string.graphics.measureText(str).width
-- returns the width of the text, the amount
of space that it will occupy horizontally when it is drawn. (There is apparently no way to
get the height of the string, but you should know that from the font that has been set for
the canvas.)All other shapes, including things as simple as lines, must be created as paths before they can be drawn. This is similar to using a Path2D in Java, except that you can't work with paths as objects in JavaScript. Instead, a graphics context has a single current path, which is part of the graphics state. Every time you want to draw a shape, you have to recreate the path. The path API includes:
graphics.beginPath()
-- start a new path. Any previous path is discarded,
and the current path in the graphics context is now empty. Note that the graphics context
also keeps track of the current point, the last point in the current path. After calling
graphics.beginPath(), the current point is undefined.graphics.moveTo(x,y)
-- move the current point to (x,y), without
adding anything to the path. This can be used for the starting point of the path or to
start a new, disconnected segment of the path.graphics.lineTo(x,y)
-- add the line from the current point to (x,y) to
the path, and move the current point to (x,y).graphics.arc(x,y,r,startAngle,endAngle,ccw)
-- draws an arc of a
circle with center (x,y) and radius r. The next two parameters give the
starting and ending angle of the arc. They are measured in radians, clockwise from the
positive direction of the x-axis. The last parameter, ccw is optional. If it is true,
then the arc extends in the counterclockwise direction from the start angle to the end angle.
If it is false or absent, then the arc extends in the clockwise direction.
The current point moves to the end of the arc. If there is a current point before
graphics.arc is called, then before the arc is drawn, a line is added to the path that extends from the
current point to the starting point of the arc. (Recall that immediately after graphics.beginPath(),
there is no current point.)graphics.quadraticCurveTo(cx,cy,x,y)
-- draws a quadratic Bezier curve
from the current point to (x,y), with control point (cx,cy). (Bezier
curves and their control points were discussed in Section 2.)graphics.bezierCurveTo(cx1,cy1,c2x,cy2,x,y)
-- draws a cubic Bezier curve
from the current point to (x,y), with control points (cx1,cy1) and
(cx2,cy2).graphics.closePath()
-- adds to the path a line from the current point
back to the starting point of the current segment of the curve. (Recall that you start
a new segment of the curve every time you use moveTo.)graphics.stroke()
-- strokes the current path.graphics.fill()
-- fills the current path. When filling a curve,
the fill is done as if graphics.closePath() was called before doing the fill; that is not
done for graphics.stroke().We should look at some examples. It takes four steps to draw a line:
graphics.beginPath(); // start a new path graphics.moveTo(100.5,200.5); // starting point of the new path graphics.lineTo(300.5,200.5); // add a line from the starting point to (300.5,200.5) graphics.stroke(); // draw the line
Remember that the line remains as part of the current path until the next time you call graphics.beginPath()! For a triangle, we just add more lines:
graphics.beginPath(); graphics.moveTo(100,300); graphics.lineTo(400,300); graphics.lineTo(200,100); graphics.closePath(); // add a line back to the starting point, (100,300); graphics.stroke();
We can draw circle with graphics.arc(), using a start angle of 0 and an end angle of 2*Math.PI. Here's a filled circle with radius 100, centered at 200,300:
graphics.beginPath(); graphics.arc( 200, 300, 100, 0, 2*Math.PI ); graphics.fill();
To draw just the outline of the circle, use graphics.stroke() in place of graphics.fill(). If you look at the details of graphics.arc(), you can see how to draw a wedge of a circle:
graphics.beginPath(); graphics.moveTo(200,300); // Move current point to center of the circle. graphics.arc(200,300,100,0,Math.PI/4); // Arc, plus line from current point. graphics.lineTo(200,300); // Line from end of arc back to center of circle. graphics.fill(); // Fill the wedge.
For a long example of using Bezier curves, see the source code of the Bezier curve demo that was mentioned in Section 2.
When you draw, you need more than shapes. You want to be able to set attributes such as color and line width. In the canvas API, these attributes are stored as properties of the graphics context object. To change the value, assign a new value to the property. The change affect drawing that is done after the change is made. You can also use the property in an expression to get its current value. Line width is pretty simple, and there are two other line properties that you might want to use:
graphics.lineWidth
-- the value of this property is a number giving
the size of the pen that is used for stroking shapes. The default value is 1.
Note that line width is subject to transformations. If you scale by a factor of two,
the line width doubles.graphics.lineCap
-- controls the appearance of the endpoints of lines.
The value is one of the strings "butt", "round", or "square". The value "round" adds
a half-circle at the endpoint; "square" adds a half square; "butt" doesn't add anything
and is the default. Using "round" can make wide lines more attractive.graphics.lineJoin
-- controls the appearance of the join between
one line and the next in a sequence of lines. The value is one of the strings
"miter", "round", or "bevel". The default, "miter", gives pointy joins. The other
two values can be more attractive for wide lines.Colors are more complicated for several reasons. First of all, there are two color properties, one used for strokes and one used for fills. And the values of these properties don't have to be simple colors; they can be things like patterns (that is, textures) and gradients. Here, however, I will only talk about colors. But even simple colors are a little complicated, since they are given as CSS color values. This means that the value that is used to represent a color is a string. It can be a color name such as "white" or "red", it can be a CSS hexadecimal color such as "#CCCC00" or "#804A3F", or it can be a CSS rgb color such as "rgb(204,204,0)" or "rgb(128,74,63)". You will probably be comfortable with the last version, which uses red/green/blue values in the range 0 to 255. Color properties include
graphics.strokeStyle
-- the style that is used for stroking shapes. The
value can be a string that specifies a CSS color value.graphics.fillStyle
-- the style that is used for filling shapes. The
value can be a string that specifies a CSS color value.graphics.globalAlpha
-- a number in the range 0.0 to 1.0 that controls
transparency. This value applies to anything that is drawn. The default value, 1.0, gives
fully opaque drawing. Smaller values introduce some transparency, and with a value of
zero, drawing is fully transparent and therefore invisible.Finally, I note that you can set the font that is used for drawing text. The
value of the property graphics.font
is a string that could be used as the value of the
CSS font property. As such, it can be fairly complicated, but the simplest versions
include a font-size (such as 20px or 150%) and a font-family
(such as serif, sans-serif, monospace, or any actual font name.)
You can add italic or bold to the front of the string, and those aren't
the only possibilities. Some examples:
graphics.font = "2cm monospace"; graphics.font = "bold 18px sans-serif"; graphics.font = "italic 150% serif";
The default is "10px sans-serif". Note that text, like all drawing, is subject to coordinate transforms, so that applying a scaling operation changes the size of the text. (But I have noticed some bugs in the implementation of this feature on my Linux computer.)
Coordinate transformations can be applied to a canvas graphics context. Translations, rotations, and scaling are supported and work pretty much as you would expect after working with them in Java. You can also apply an arbitrary transform:
graphics.scale(sx,sy)
-- scale by sx in the x-direction
and sy in the y-direction.graphics.rotate(angle)
-- rotate by angle radians about the
origin. A positive rotation is clockwise in the default coordinate system.graphics.translate(tx,ty)
-- translate by tx in the x-direction
and ty in the y-direction.graphics.transform(a,b,c,d,e,f)
-- apply the transformation
xnew = a*x + c*y + e, ynew = b*x + d*y +f. Like the
previous three methods, this is applied on top of any previous transformations.
(Recall from Section 1 that any
transformation can be written in this form. For example, you can apply a shearing transformation
by using this function.)graphics.setTransform(a,b,c,d,e,f)
-- discard the current transformation,
and set the current transformation to be xnew = a*x + c*y + e,
ynew = b*x + d*y +f.To implement hierarchical graphics, you need to be able to save the current transformation so that you can restore it later. Unfortunately, there is no reliable way to read the current transformation from a canvas graphics context. (A way to do so has been added to the standard, but it is not implemented in all browsers.) However, the graphics context itself keeps a stack of transformations and provides methods for pushing and popping the current transformation. In fact, these methods do more than save and restore the current transformation. They actually save and restore the entire state of the graphics context, including properties such as current colors, line width, and font:
graphics.save()
-- push a copy of the current state of the graphics
context, including the current transformation, onto the stack.graphics.restore()
-- remove the top item from the stack, containing
a saved state of the graphics context, and restore the graphics context to that state.Using these methods, the basic setup for drawing an object with a modeling transform becomes:
graphics.save(); // save a copy of the current state graphics.translate(a,b); // apply the modeling transformation graphics.rotate(r); graphics.scale(s,s); // ...... // Draw the object! graphics.restore(); // restore the saved state
Note that if drawing the object includes any changes to attributes such as drawing color, those changes will be also undone by the call to graphics.restore(). In hierarchical graphics, this is usually what you want, and it eliminates the need to have extra statements for saving and restoring things like color.
To draw a hierarchical model, you need to traverse a scene graph, either procedurally or as a data structure. It's pretty much the same as in Java. In fact, you should see that the basic concepts that you learned about transformations and modeling carry over to the canvas graphics API. Those concepts apply very widely and even carry over to 3D graphics APIs, with just a little added complexity.
For a non-trivial example of hierarchical modeling in a canvas, you can look at the source code for the web page cart-and-windmills.html. This is the same hierarchical scene that we looked at in Java. The web page implements it using an object-oriented scene graph API.
For really interesting graphics, you want to have animation or user interaction or both. One way to implement animation in JavaScript is with the standard setTimeout() function. (There are fancier ways that are considered to be superior.) This purpose of this functions is to schedule a task to be run at some future time. The syntax is:
setTimeout( func, millis );
Here func is the name of a function or is an anonymous function, and millis is a number giving the length of a time interval in milliseconds. The setTimeout function returns immediately, but approximately millis milliseconds later, the function will be executed. It's important to understand that setTimeout does not wait for func to run; it simply schedules func to be run later and then exits. setTimeout() returns a value that represents the timeout. You can save this value and use it to cancel the execution of func any time before it happens by calling clearTimeout(timeoutID), where timeoutID is the value returned by setTimeout().
To use setTimeout for animation, you need a repeating sequence of timeouts. An easy way to do that is to have func schedule a new timeout when it executes. To run the animation, you just need to call func once, and after that it will continually re-call itself.
Here is a framework for animating graphics that uses these ideas. I am assuming that there is a function draw() that draws one frame of the animation and a function updateFrame() that makes any changes to variables that need to change between one frame and the next. The animation can be started and stopped by calling setAnimationRunning(false) and setAnimationRunning(true).
var animationTimeout = null; // A null value means the animation is off. // Otherwise, this is the timeout ID. function frame() { // Draw one frame of the animation, and schedule the next frame. updateFrame(); draw(); animationTimeout = setTimeout(frame, 33); } function setAnimationRunning(run) { if ( run ) { if (animationTimeout == null) { // If the animation is not already running, start // it by scheduling a call to frame(). animationTimeout = setTimeout(frame, 33); } } else { if (animationTimeout != null) { // If the animation is running, stop it by // canceling the next scheduled call to frame(). clearTimeout(animationTimeout); } animationTimeout = null; // Indicates that animation is off. } }
You can start the animation running by calling setAnimationRunning(true) in the function that responds to the page's onload event. In my examples, I often provide a checkbox to control whether the animation is running. To do that, I could just add
<input type="checkbox" id="animate" onchange="setAnimationRunning(this.checked)"> <label for="animate">Run Animation</label>
to the HTML code. Providing such a checkbox is a simple example of user interaction. It's certainly possible to add any number of controls to a web page and program their interaction with a canvas. So, you can write complex applications that use canvas as their graphics component. It's also possible to program mouse interaction with the canvas by implementing mouse-handling methods.
If you just want to respond when the user clicks the mouse on the canvas, you only need to add an event listener for the "mousedown" event to the canvas. This can be done in the initialization function. Here's how it would look, using an anonymous function as the event handler:
canvas.addEventListener("mousedown", function (event) { // ... respond to the mousedown event }, false);
Information about the event is passed to the handler function in the parameter, event. Often, you will want to know the coordinates of the point where the user clicks. The parameter includes coordinates among its properties, but unfortunately they are not given in the coordinate system of the canvas. The properties evt.clientX and evt.clientY give the mouse coordinates in the client, that is, in the part of the window where the web page is shown, with (0,0) at the upper left corner of the viewing area. I have found that these can be converted to canvas coordinates as follows:
var r = canvas.getBoundingClientRect(); var x = Math.round(event.clientX - r.left); var y = Math.round(event.clientY - r.top);
canvas.getBoundingClientRect() returns the bounds of the canvas, given in client coordinates. (I think that this works in all browsers that support canvas.) The Math.round is there for Firefox, which can return non-integral values for r.left and r.right, and in fact it could probably be omitted since non-integer coordinates are not a problem for drawing with canvas graphics. If we add the above code to the beginning of the mouse event handler, then we can use x and y as the coordinates of the mouse. The event handler in the following example draws a circle centered at each point that is clicked:
canvas.addEventListener("mousedown", function (event) { var r = canvas.getBoundingClientRect(); var x = Math.round(event.clientX - r.left); var y = Math.round(event.clientY - r.top); graphics.beginPath(); graphics.arc(x,y,25,0,2*Math.PI); graphics.fill(); }, false);
When it comes to implementing mouse-drag operations, there are a few more technicalities. Whereas Java has separate mousemove and mousedrag events, JavaScript only has mousemove. So, if you want to respond to dragging, you have to use a mousemove handler that checks that the mouse is really being dragged and not just moved. A second issue is that the mouseup event that follows a mousedown in the canvas is not necessarily sent to the canvas. If the user moves the mouse out of the canvas before releasing the mouse button, then the event is not sent to the canvas. You can solve this by adding the mouse event listener to the document as a whole instead of to the canvas. There is a predefined variable document that represents the whole document, and you can add the mouseup listener to that. Finally, there is the problem that mousemove events are sent to the canvas only if the mouse is over the canvas. (In Java, mousedrag events continue to be sent to the component that was clicked, even if the mouse moves outside that component, and the mouseup that ends the drag is also sent to that component.) If you want to continue responding to a drag operation when the mouse is moved outside of the canvas, you should attach the mousemove handler to the document rather than to the canvas; attaching it to the canvas could also work, depending on the style of interaction that you want. Here is a general framework for handling mouse dragging on a canvas; I put it into a function that you can call to install the three required listeners:
function setUpMouse() { var startX, startY; // point where the mouse click occurred var prevX, prevY; // previous position of mouse during drag var dragging = false; // tells whether a drag is in progress canvas.addEventListener("mousedown", function (event) { if (dragging) { return; // Don't start a new drag if already dragging. } dragging = true; // Note: might not do this in all cases. var r = canvas.getBoundingClientRect(); startX = prevX = Math.round(event.clientX - r.left); startY = prevY = Math.round(event.clientY - r.top); // Do any other required initialization of the drag operation. }, false); document.addEventListener("mouseup", function (event) { if (dragging) { dragging = false; // Do any end-of-drag work (generally there is none). } },false); document.addEventListener("mousemove", function (event) { // (Might want to attach to canvas rather than document.) if ( ! dragging ) { return; // Only respond to move if a drag is in progress. } var r = canvas.getBoundingClientRect(); var x = Math.round(event.clientX - r.left); var y = Math.round(event.clientY - r.top); // Respond to change in mouse position during drag, using // any of the variables startX, startY, prevX, prevY, x, y. prevX = x; prevY = y; },false); }
You won't need to use startX, startY, prevX, and prevY in every application, but they are often useful. Note that there is still a possible problem with the coordinates that are used in this example: They are expressed in the default coordinate system. If you have applied a coordinate transformation to the canvas, you might want to get coordinates expressed in the transformed coordinate system. Unfortunately, the canvas API provides no easy way to do this.
For a very simple demo that uses this framework, see simple-drawing-demo.html. The demo uses one more fact about event objects: The event object for a mouse event contains information about which mouse button was pressed and which modifier keys are down. The demo uses the property event.shiftKey, which is true if the shift key is being held down.