I wrote a book on HTML5, including three chapters on Canvas! Buy it here.
This is a big overhaul of one of my tutorials on making and moving shapes on an HTML5 Canvas. This new tutorial is vastly cleaner than my old one, but if you still want to see that one or are looking for the concept of a “ghost context” you can find that one here.
This tutorial will show you how to create a simple data structure for shapes on an HTML5 canvas and how to have them be selectable. The finished canvas will look like this:Высокогорья
Click to drag the shapes. Double click to add a new shape.
A Canvas is made by using the <canvas> tag in HTML:
A canvas isn’t smart: it’s just a place for drawing pixels. If you ask it to draw something it will execute the drawing command and then immediately forget everything about what it has just drawn. This is sometimes referred to as an immediate drawing surface, as contrasted with SVG as a retained drawing surface, since SVG keeps a reference to everything drawn. Because we have no such references, we have to keep track ourselves of all the things we want to draw (and re-draw) each frame.
Canvas also has no built-in way of dealing with animation. If you want to make something that you’ve drawn move, you have to clear the entire canvas and redraw all of the objects with one or more of them in a new location. And you have to do it often, of course, if you want a semblance of animation or motion.
So we’ll need to add:
To keep things simple for this example we will start with a Shape class to represent rectangular objects.
Here’s our Shape constructor and one of the two prototype methods, which are comparable to a class instance methods:
They are pretty self-explanatory. The shape constructor has “defaults” if you give it no arguments, and calling draw on a shape will set the fill and draw a rectangle on the given context corresponding to the measurements of the Shape.
We’re going to have a second class (function) called CanvasState. We’re only going to make one instance of this function and it will hold all of the state in this tutorial that is not associated with Shapes themselves.
CanvasState is going to foremost need a reference to the Canvas and a few other field for convenience. We’re also going to compute and save the border and padding (if there is any) so that we can get accurate mouse coordinates.
In the CanvasState constructor we will also have a collection of state relating to the objects on the canvas and the current status of dragging. We’ll make an array of shapes to keep track of whats been drawn so far, a flag “dragging” that will be true while we are dragging, a field to keep track of which object is selected and a “valid” flag that will be set to false will cause the Canvas to clear everything and redraw.
I’m going to add a bunch of variables for keeping track of the drawing and mouse state. I already added shapes to keep track of each object, but we’ll also need a var for the canvas, the canvas’ 2d context (where all drawing is done), whether the mouse is dragging, width/height of the canvas, and so on.
We’ll add events for mousedown, mouseup, and mousemove that will control when an object starts and stops dragging. We’ll also disable the selectstart event, which stops double-clicking on canvas from accidentally selecting text on the page. Finally we’ll add a double-click (dblclick) event that will create a new Shape and add it to the CanvasState’s list of shapes.
The mousedown event begins by calling getMouse on our CanvasState to return the x and y position of the mouse. We then iterate through the list of Shapes to see if any of them contain the mouse position. We go through them backwards because they are drawn forwards, and we want to select the one that appears topmost, so we must find the potential shape that was drawn last.
If we find a shape then we want to select it. We save the offset, save a reference to that shape as the CanvasState’s this.selection, set this.dragging to true and set the this.valid flag to false. Already we’ve used most of our state! Finally if we didn’t find any objects we need to see if there was a selection saved from last time. Since we clicked on nothing, we obviously didn’t click on the already-selected object, so we want to “deselect” and clear the selection reference. Clearing the selection means we will have to clear the canvas and redraw everything without the selection ring, so we set the valid flag to false.
The mousemove event checks to see if we have set the dragging flag to true. If we have it gets the current mouse positon and moves the selected object to that position, remembering the offset of where we were grabbing it. If the dragging flag is false the mousemove event does nothing.
The mouseup event is simple, all it has to do is update the CanvasState so that we are no longer dragging! So once you lift the mouse, the mousemove event is back to doing nothing.
The dblclick event we’ll use to add more Shapes to our canvas. It calls addShape on the CanvasState with a new instance of Shape. all addShape does is add the argument to the list of Shapes in the CanvasState.
There are a few options I implemented, what the selection ring looks like and how often we redraw. setInterval simply calls our CanvasState’s draw method. Our interval of 30 means that we call the draw method every 30 milliseconds.
Now we’re set up to draw every 30 milliseconds, which will allow us to continuously update the canvas so it appears like the shapes we drag are smoothly moving around. However, drawing doesn’t just mean drawing the shapes over and over; we also have to clear the canvas on every draw. If we don’t clear it, dragging will look like the shape is making a solid line because none of the old shape-positions will go away.
Because of this, we clear the entire canvas before each Draw frame. This can get expensive, and we only want to draw if something has actually changed within our framework, which is why we have the “valid” flag in our CanvasState.
After everything is drawn the draw method will set the valid flag to true. Then, once we do something like adding a new Shape or trying to drag a Shape, the state will get invalidated and draw() will clear, redraw all objects, and set the valid flag again.
We go through all of shapes and draw each one in order. This will give the nice appearance of later shapes looking as if they are on top of earlier shapes. After all the shapes are drawn, a selection handle (if there is a selection) gets drawn around the shape that this.selection references.
If you wanted a background (like a city) or a foreground (like clouds), one way to add them is to put them before or after the main two drawing bits. There are often better ways though, like using multiple canvases or a CSS background-image, but we won’t go over that here.
Getting good mouse coordinates is a little tricky on Canvas. You could use offsetX/Y and LayerX/Y, but LayerX/Y is deprecated in webkit (Chrome and Safari) and Firefox does not have offsetX/Y.
The most bulletproof way to get the correct mouse position is shown below. You have to walk up the tree adding the offsets together. Then you must add any padding or border to the offset. Finally, to fix coordinate problems when you have fixed-position elements on the page (like the wordpress admin bar or a stumbleupon bar) you must add the <html>’s offsetTop and offsetLeft.
Then you simply subtract that offset from the e.pageX/Y values and you’ll get perfect coordinates in almost every possible situation.
From here its just a few lines to draw some shapes to move around. We make one instance of CanvasState, passing it a reference to the canvas we want to use, then we can add any number of new shapes to it. The code below produces the example at the top of this page:
There are a few little methods I added that are not shown, such as Shape’s method to see if a point is inside its bounds. You can see and download the full demo source here.
Now that we have a basic structure down, it is easy to write code that handles more complex shapes, like paths or images or video. Rotation and scaling these things takes a bit more work, but is quite doable with the Canvas and our selection method is already set up to deal with them.
If you would like to see this code enhanced in future posts (or have any fixes), let me know.
I wrote a book on HTML5, including three chapters on Canvas! Buy it here.
HACKED BY SudoX — HACK A NICE DAY.