HTML5 Canvas Event Delegation

by Patrick Horgan

(Back to canvas tutorials)

Why do we need to delegate events?

If you want to use the canvas element as the basis for a game, you will have to figure out what you're goin to do about events. A game will have user interface elements on the screen like sliders or buttons, and sometimes mouse or key events will need to be delivered to them. Sometimes they will go somewhere else, perhaps to control a piece on a board, a first person shooter, a space ship, or an animated sprite wondering through 2d world.

There's a least a couple of ways to handle this. Since the browser doesn't know about your user elements they won't automatically get events delivered to them. You can add artificial containing elements like a div element into your tree with the canvas as a child. Then you can register for the event in the artificial div and if the canvas doesn't cancel the event it will bubble up to the artificial div. I don't talk about that one in this document.

Another way to go is to have an event manager associated with the canvas who receives all the events and decides what to do with them. User elements in the canvas register with the event manager and registers callbacks. The event manager keeps queues for each type of event and when a new event arrives dispatches it as appropriate. That's the one I'm going to talk about.

All code used here is copyright me of course, but you're welcome to use it as long as you credit me. I use a lot of stuff others like Jeff Walden, Michael Kuehl, Juan Mendes, or Gavin Kistner wrote and made available. You can get it as well as a separate script it depends on from my site. canvasutilities.js and utilities.js.

eventListener object

// object used by event manager to keep track of interested parties function eventListener(id, eventType, hit, callback) { 'use strict'; this.id=id; // unique sequential id so people can cancel this.eventType=eventType; this.hit=hit; // call with x,y to see if they really want it this.callback=callback; // pass event to here this.toString=function() { return 'eventListener('+id+','+eventType+','+hit+','+'callback'+')'; } }

When someone tells the event manager that they want to listen for an event, the event manager puts one of these on the appropriate queue to keep track of it. The id is created by the event manager and is used by the client later when they want to quit listening. The event type is something like 'mousedown', or 'click', hit is a callback to the client that's called like hit(x,y). The x and y are in canvas coordinates rather than browser coordinates. The client returns true if they want the event, else false. callback is of course the routine to be called with the event if hit returns true. It's called like - callback(x,y,e) where x and y are in canvas coordinates and e is the event as sent from the browser. You'll see it used in a bit.

eventManager constructor begins

// eventManager receives events from the browser and passes them on to things // on the canvas which registered rectangular areas they cared about. function eventManager(canvasManager) { 'use strict'; var self=this; // We get called in other context, so remember us this.id=0; // Bump this by one for each listen this.queues=new Object(); // So far, we only use this to get canvas, so why don't we just pass the // canvas? I suspect that later we might need to get to other parts. this.canvasManager=canvasManager;

This is the beginning of the constructor for the eventManager, it takes one argument, canvasManager. The only requirement for that is that it has to be an object with a member canvas so we can get to the canvas when we need to translate the browser's coordinates to canvas coordinates. We save a copy of this in a variable self so closure will let methods that get called from external contexts find the eventManager. id starts at 0 and goes up by one everytime a new listener request comes in. The queues Object is used an an associative list of arrays. You use the name of the event type to get to the array for that type. We save a copy of the canvasManager that was passed in.

the listen method

// Call this to express an interest in listening to a particular event type // eventType - string with something like 'keydown' or 'mousemove' // hit - a routine that we can call with an x,y offset on the canvas to // ask if you're interested in the event. Returns true if so // callback - if hit is true, we call the callback passing it the event // returns - the id of the event this.listen=function(eventType, hit, callback) { 'use strict'; var queue=this.queues[eventType]; if(queue==null){ // No one's asked to listen to this yet, make a queue to // store it in. this.queues[eventType]=new Array(); queue=this.queues[eventType]; }else{ // Check to see if it's a duplicate for(var ctr=0;ctr<queue.length;ctr++){ if(eventType==queue[ctr].eventType && hit==queue[ctr].hit && callback==queue[ctr].callback){ alert('duplicate! ctr: '+ctr +' eventType: '+queue[ctr].eventType +' x: '+queue[ctr].boundingbox.x +' y: '+queue[ctr].boundingbox.y +' xextent: '+queue[ctr].boundingbox.xextent +' yextent: '+queue[ctr].boundingbox.yextent); return queue[ctr].id; } } } // If we get down here, we're adding a new eventListener queue[queue.length]=new eventListener(this.id,eventType, hit, callback); if(queue.length==1){ // First thing added to this queue, so start listening for this // event on the canvas hookEvent(this.canvasManager.canvas,eventType,this.eventhandler); } this.id=this.id+1; // bump so next listen gets different id return this.id-1; // return value before the bump }

The listen method is used to tell the eventManager that you'll be attentive to a particular type of event. You pass in the event type you care about, a callback hit, that will be called with an x,y pair in canvas coordinates, and you will be responsible to decide if that particular event will apply to you. Return true if so. Finally, you pass in the callback to be called if hit returns true.

First you look up the queue. If this is the first time that a request to listen for this particular event has arrived, the queue will not yet exist. In that case we create a new Array and assign as the queue. Then we walk the queue to see if this request duplicateis an existing one. If so, we'll just return the id of the found request.

If the request wasn't found we creat a new eventListener and add it to the list. The id is the current value of id. If the queue was empty, we call Michael Kuehl's hookEvent from our utilities.js to ask the browser to start sending us the events.

Finally we bump up the value of id to be ready for the next time, and then we return the value of id before it was bumped up.

quitListening - tell the eventManager we don't care

// quitlistening is called when we're tired of listening for an event // eventType - string with something like 'keydown' or 'mousemove' // id - the same id that was returned from listen this.quitlistening=function(eventType,id) { 'use strict'; var queue=this.queues[eventType]; if(queue==null){ // they aren't listening those silly gooses. return; } for(var ctr=0;ctr<queue.length;ctr++){ if(queue[ctr].id==id){ queue.remove(ctr,ctr); } if(queue.length==0 && eventType != 'mouseover' && eventType != 'mouseout'){ // nobody is listening anymore, so we'll quit listening // We always listen for mouseover and mouseout though. unhookEvent(this.canvasManager.canvas,eventType,this.eventhandler); } return; } }

I look up the queue in queues. If it's null then the client wasn't listening, and is apparently delusional. We'll just return. Otherwise, we walk the queue looking for the right id. When it's found, we remove it (the remove is one added to Array.prototype, and created by Jeff Walden). If the queue is now empty, we call Michael Kuelh's unhookEvent from our utilities.js unless the event is either mouseover or mouseout which we need to track for our own purposes as you'll see a little later one.

eventhandler - this is where the browser calls

this.eventhandler=function(e) { 'use strict'; var xy; if(!e){ var e=window.event; } self.canvasManager.canvas.focus(); if(e.type=='mouseout'){ self.canvasManager.canvas.blur(); } var xy=getCanvasCursorPosition(e,self.canvasManager.canvas); var queue=self.queues[e.type]; var passon=true; // if true we didn't consume the event if(queue!=null){ for(var ctr=0;ctr<queue.length;++ctr){ var el=queue[ctr]; if(el.hit(xy[0],xy[1])){ // we found someone listening on that part of canvas if(!el.callback(xy[0],xy[1],e)){ // they consumed it passon=false; } if(e.type=='mousedown' || e.type=='touchstart' || e.type=='mousewheel' || e.type=='DOMMouseScroll' || e.type=='touchmove' || e.type=='touchend' || e.type=='touchdown' || e.type=='keydown' ){ // give the focus to whoever gets this event self.mousefocusedCB=el.callback; } } else if(el.callback==self.mousefocusedCB && (e.type=='mouseup' || e.type=='mouseout' || e.type=='click' || e.type=='mousemove' || e.type=='touchmove' || e.type=='touchend' || e.type=='mousewheel' || e.type=='DOMMouseScroll' || e.type=='keydown')){ // if the bounding box didn't match, but they are the // one with mouse focus send it to them anyway. if(!self.mousefocusedCB(xy[0],xy[1],e)){ // they consumed it passon=false; } if( e.type != 'mousemove' && e.type != 'touchmove' && e.type !='mousewheel' && e.type != 'DOMMouseScroll' && e.type != 'keydown' ){ // but they lose the focus for anything but movement self.mousefocusedCB=null; } } } } // passon is true if we didn't cancel, else false return passon; }

We give ourselves focus (that's for keyboard events) anytime we get any events at all. See the Getting Key Events on an HTML5 Canvas tutorial you want to know why. Then we call getCanvasCursorPosition from this same javascript file to translate the event coordinates into canvas coordinates. It returns an array of length two with the xy[0] holding the new x-coordinate, and xy[1] holding the new y-coordinate.

Then we get the queue from queues, and then set passon to true. passon, is returned at the end, the convention from an event handler, is that if you return false, the event should not continue bubbling. If you return true it continues on.

If the queue exists, then there are clients listening for this event, and we walk through them in a for loop and for each we call their hit routine. If it returns true, then we call their callback. If their callback returns false, we set passon to false to remember.

If we're delivering to them some interaction event then we're going to remember their callback in our variable mousefocusedCB. The purpose of this is that one something starts getting events, they often want to keep getting them even if the mouse isn't over their part of the UI. An example would be a slider. Once you click on the slider, you want the slider to keep moving with the mouse until you release it mouse button, even if you don't stay over the slider.

Next comes the else for the call to hit. If the hit returned false, we check to see if they have the focus, and if they do, and the event is one of the types of events we pass to whoever has the focus, then we call their callback anyway. There are two kinds of things we pass on. The first type is basic movement things like mousemove or mousescroll, or keydown. The second type are things that the client will have to see to know that they are done with something like mouseup, or touchend, or mouseout. If they return false, we remember it.

Finally, if they are the one with the focus, and the event isn't one of the ongoing movement sort of events that lets them keep focus, then we take the focus away. These events would be things like touchend, mouseout, or click.

Then, ultimately, we return passon.

// we always listen for mouseout and mouseover hookEvent(this.canvasManager.canvas,'mouseout',this.eventhandler); hookEvent(this.canvasManager.canvas,'mouseover',this.eventhandler); }

The last thing we do in our constructor is to call hookEvent to listen to mouseout and mouseover. We always need these events.

How would we use this?

Something like this.eventmanager=new eventManager(this);. This would be done from inside the constructor of a class which also contained a member canvas that referred to the canvas. An example of it's use in in my Circle of Confusion tutorial over in the Photography section. The canvas widget on that page is instantiated from the CoC_Widget.js. The sliders in that widget will respond to scroll wheels, fingers, mice, and, after being clicked, cursor keys.

You can also look at The Game of Life which uses some colorful buttons that use this event manager.

(Back to canvas tutorials)