In this lesson, listening for and handling mouse and keyboard events in Canvas is covered.
On a basic level, there's nothing really special about user interaction in HTML5 canvas. You're using the standard DOM UI events such as mousedown, mouseup, keydown, keyup, et cetera. But how you interpret and use those events is sometimes quite a bit different.
Let's look at mouse events first, then later we'll look at keyboard events. The main mouse events we'll be using are click, mousedown, mousemove and mouseup. If you like your app to work on touchscreen devices as well, you want to look at touch events.
There are different methods for listening to touch events, and it varies a bit in different browsers. But once you get the touch event, the concepts that we'll cover here are the same. For mouse events, you're generally best to listen for the events right on the canvas.
Usually, you're only interested in mouse events that occur when the mouse is on top of the canvas. Listening for events on the canvas will keep you from executing code when the user moves or clicks outside of the canvas' footprint.
We can simply listen for a click event by saying, "Canvas addEventListener click," and passing in a function. That function gets an event object that has information about the event. Probably the most useful properties of that object are client X and client Y.
These define the location of the mouse at the time that the event occurred. The location is generally in relation to the client area of the browser that the page is loaded into. Here, though, jsbin uses iFrame, so the client X and Y will be in relation to the top-left corner of that iFrame -- not the entire client window.
In the handler function, let's draw a circle at that clicked location. When we click, notice that the circle is not drawn exactly at the mouse position but down into the right. The problem is that the client X and Y are in relation to the client area, but the canvas is not aligned with that exactly.
That's usually due to the document body having some initial margin. To prove this, let's go into the CSS and change body margin to zero. Now, the canvas is exactly aligned with the top-left corner of the client, and when we click, the circle is right-on.
If we changed this margin to, say, 100 pixels, now we're way off. We could compensate for this by subtracting 100 from client X and client Y, and using that to draw the circle. That works, but in a real web page, the canvas could be nested inside one or more divs. Each with padding, margins, various alignments, et cetera.
It could get pretty complex to try to figure out how much to offset the client X and Y, to represent where the mouse is on the canvas. Fortunately, there's a useful function called getBoundingClientRect.
This gives you the rectangle for the element it's called on, in relation to the client area -- no matter what the structure of the document or what styles are in play.
We can call this on the canvas and get the rect for that element. That rect's top and left property will tell us how far it is offset from the client's origin. We can then subtract those from client X and Y, and again, we're right-on.
That works for a body margin of a hundred. If we set the margin to 10, it still compensates for that. Setting you rect down to zero, we're still perfect. Now that we've handled that, I'll get rid of the CSS and go back to the default.
With what we know now, we can create a simple drawing app. To draw, we'll need to know when the mouse goes down to start drawing when the mouse moves, but only after it's down. So we can draw, and when it goes back up, so we can stop drawing.
I'll create three functions on mousedown, on mousemove and on mouseup. Each one of these gets an event object. Initially, I'll only listen for the mousedown event on the canvas. Because until that happens, we don't want to do anything.
This will naturally get on mousedown as a handler. I'm also going to get that BoundingClientRect and save it at the start of the script. In a more complex app where the layout could change in real-time, you might want to do this each time you need that offset. So it's always up-to-date.
But here, there layout isn't going to change at all, so we'll be fine getting that once at the start. I'll also create mouse X and mouse Y variables to hold the current mouse position. In OnMouseDown, I'll calculate mouse X and Y by subtracting rect left and top from client X and Y.
I'll listen for mousemove events on the canvas using OnMouseMove as the handler. Finally, I'll listen for a mouseup event on the document body. I'll do this on the body because it's possible that the user could start drawing on the canvas and draw right off the side of it, and then release the mouse.
If we're only listening for mouseup on the canvas, we miss that up event. By listening for it on the document body, we'll always get it. In OnMouseMove, we'll begin a path and move to the current mouse X and Y. Then we use the same code we used in OnMouseDown to calculate the new mouse X and Y, and draw a line to that. And, of course, stroke.
We'll continue drawing line segments from their previous mouse position to the new one, every time the mouse moves at all. To wrap things up, in OnMouseUp, we'll remove the listeners from mousemove and mouseup.
They'll get out and back in next time the OnMouseDown runs. This gives us the basic pattern that's used for almost any freehand drawing program ever created. From here, you just need to build a UI to allow the user to change the color, stroke size, et cetera, and incorporate those choices into your context stroke style, line width and so on.
Another operation you might want to do with canvas mouse events is to create user interface controls. I wouldn't go too crazy trying to make a complex UI canvas, but now and then, you might want to have a clickable button or something like that.
Canvas offers no real help here other than the events we've just seen. You'll need to keep track of where your controls are, write the code that detects any mouse interaction with them and write the code that deals with the different states of the button and renders those states.
Let's make a very simple toggle button. I'll create a rectangle for the button, putting it at 100-100 and making it 100 pixels wide and 50 high. I'll also give it a selective property initialized to false. Let's create a draw button function.
This first sets the fill style to either red or gray, based on whether or not the button is selected. Then it draws a filled rectangle within that defined area. I'll call the function and verify that we're getting a button drawn on the canvas.
Next, we'll add a mouseclick handler to the canvas. This will get the BoundingClientRect and complete a mouse X and Y relative to the canvas itself as we just covered. Now it checks to see that XY is within that button rectangle, checking if it's greater than or equal to the button X and less than or equal to its X plus width. The same strategy on Y.
If all those checks evaluate to true, then we clicked inside the button rectangle. We'll toggle the selected property of the button and then draw the button. Simple enough. Over on the canvas, when I clicked the button, it toggles from red to grey and back again.
Of course, this is a very simple button. If you wanted one with up, over and down states in addition to selected, you'd have to write all the code to handle all those as well. One thing this is lacking is any indication that it is a button.
In native HTML controls, the mouse cursor often changes when you hover over some actionable item. But we can actually do that here. I'll copy and paste this whole click handle block, and change the event to mousemove.
Now, we're checking to see if the mouse is over the button rectangle every time the mouse moves on the canvas. Instead of toggling and drawing a button when it is, we'll change the canvas' cursor style, setting it to pointer.
This should change it into a cursor that looks like a hand with a pointing finger. If it's not over the button, we'll set the style back to auto. Now, whenever the mouse moves over the button, the cursor changes to show that it's clickable.
This actually feels a lot more like a button now. It's up to you how far you want to go on this path, but if you ever need to make some simple controls in canvas alone, this should get you started. Before we end off here, I'll mention keyboard events.
To be honest, there's not anything real special about using keyboard's event with canvas. You just listen for the keyboard event you want, interpret it as you want and manipulate your canvas as you want. In general, I found that it's best to listen for keyboard events on the document body.
In order to get keyboard events on the canvas itself, the canvas has to be made focusable and then given input focus. Even after all that, it's very easy for it to lose focus, disrupting your event stream. Listening for keyboard events on the document body means you'll always get them.
Here, I've done just that. We'll want to know which key was pressed. There are many properties on this keyboard event that gets passed into the handler. The key property is supposed to be the new standard, but it's not supported at all in Chrome.
The key code property is deprecated but currently well supported, so we'll use that in the switch statement. The key code for left arrow key is 37 and for the right arrow key is 39. I've said X and Y properties both do 100 at the top of the file.
Here, if we press 37, the left key, I'll decrement X and call a draw function. If the right arrow key is pressed, I'll increment X and, again, call a draw. The draw function will clear the screen and draw a circle at XY, nothing more.
If I press and hold the left arrow key, the circle moves left. The same for the right arrow key. You can add in handlers for the up and down arrow keys and change the Y as well. Again, there's nothing very canvas-specific in handling keyboard events, but they can be useful in games or any other canvas-based applications.