Course notes 24
syllabus | schedule | exercises | assignments | class notes | resources | students | ARTC courses

Javascript Events

Javascript represents the behavior layer of website architecture and provides a myriad of methods for facilitating interaction between the contents of the page and the user. Traditional methods involve the use of inline event handlers, like onclick="" and onmouseover="", which are placed directly into HTML tags. While effective, this technique has several limitations when trying to achieve a more dynamic environment. The introduction of Event Listeners provides an alternative method which overcomes several of these issues.

element.addEventListener("event", function, capture);

An event listener can be attached to any HTML element. The listener function takes three parameters. First is the event which is being listened for. This is the equivalent of the onclick or onmouseover. Some common events are :

  • click
  • mousemove
  • mouseover
  • mouseout
  • keyup
  • keydown
  • focus
  • blur
  • select
  • load

The second parameter is the callback function which should be executed when the event happens. The third and final parameter is a boolean reference pertaining to how the event order is handled. For our purpose, this should always be false. For more information regarding the third parameter, see quirksmode's Event Ordering .

NOTE: the addEventListener model is not supported by Internet Explorer. Microsoft has implemented their own attachEvent model, which operates similarly but with notable differences. Cross-Browser situations would need to account for both methods.

There are several advantages to attaching code-based event listeners over inline handlers.

  1. Multiple Listeners - more than one instance of the same event can be attached to a single element. This allows for more modular code, and combined with the next benefit, can allow for some creative layering of interactions.
  2. Listeners Can Be Removed - at any time an event listener can be removed by calling removeEventListener with the same parameters on the same element.
  3. Internal Reference - The callback function executed maintains the reference to the element on which the event executed within the this variable.

The next example is a simple application of an event listener to a canvas. It demonstrates how the event object can be passed to the callback function and used to provide details on the interaction.


function init(){
    var canvas = document.getElementById('myCanvas');
    canvas.addEventListener('mouseover', mouseAlert, false);
}

//notice that when we call the function we are not explicitly passing anything

function mouseAlert(e) {
    alert(e.type + ' on ' + this.id);
}
Inside of events (and other objects) there is information that can be retrieved by using .type, etc.
The .type tells what the event is. 'This' refers to the object that the mouse is over.
.id refers to the unique identifier for the object.

When adding interactions to a Canvas, the only element an event can be attached to is the canvas itself. To achieve more complex interactions, a loop structure similar to the animation process must be used. The callback function is used to update values, clear the canvas, and redraw with the new settings.

The below example listens for a click event on the canvas and then increments a rotation variable before redrawing the surface.

var canvas, ctx, rotateNum = Math.PI/8;
function init(){
    canvas = document.getElementById('myCanvas');
    ctx = canvas.getContext('2d');
    ctx.fillRect(canvas.width/2-50,canvas.height/2-50,100,100);

    canvas.addEventListener('click',rotateRect,false);
}	

function rotateRect(e) {
    ctx.clearRect(0,0,canvas.width,canvas.height);
    ctx.save();    //saves the default state of the coordinate system before it gets transformed
    ctx.translate(canvas.width/2,canvas.height/2);  //move the origin to the center for rotation
    ctx.rotate(rotateNum);                          //rotate the canvas
    ctx.fillRect(-50,-50,100,100);
    ctx.restore();                                  //sets the transform of the canvas back to normal
    rotateNum+=Math.PI/8;                           //increments the variable
}

Determining the Mouse Position

The event object provides several properties which reveal the position of the mouse at the time of the event's activation. event.pageX and event.pageY provide the mouse coordinates relative to the top-left corner of the document, thus accounting for any scroll offset. They are supported in all majors browsers except for Internet Explorer.

For this to be useful in canvas calculations, it must be converted to a value relative to the canvas element. To accomplish this we subtract the canvas offset from the mouse offset. Unfortunately there is no direct method of calculating an element's offset. However, you can loop through an element and its ancestors, accumulating a total offset value. The following function, based on code from quirksmode.com, does just this:

function findOffset(obj) {
    var curX = 0;
    var curY = 0;
    if (obj.offsetParent) {   //if the browser supports offsetParent then we can use it
        do {
            curX += obj.offsetLeft;  //get left position of the obj and add it to the var.
            curY += obj.offsetTop;   //gets top position and add it to the var.
        } while (obj = obj.offsetParent);  // keep doing this until 
        
        return {x:curX, y:curY};  //this is a function that returns two values
	}
}

The following example employs the above function in determining the local mouse coordinates and uses them to draw a circle under the mouse's position.

var canvas;
function init(){
    canvas = document.getElementById('myCanvas');
    ctx = canvas.getContext('2d');
       
    canvas.addEventListener('mousemove', followMouse, false);  //everytime the mouse moves it calls
                                                               //the followMouse function 
}

function findOffset(obj) {
    var curX = curY = 0;
    if (obj.offsetParent) {
        do {
            curX += obj.offsetLeft;
            curY += obj.offsetTop;
        } while (obj = obj.offsetParent);
    return {x:curX,y:curY};
    }
}

function followMouse(e){
    
    ctx.clearRect(0,0,canvas.width, canvas.height); //clear canvas
    
    ctx.beginPath();
    var offset = findOffset(canvas);   //get the offset of the canvas relative to the page
    
    var posX = e.pageX - offset.x;     //find the x position of the mouse
    var posY = e.pageY - offset.y;     //find the y position of the mouse
    
    ctx.arc(posX,posY,50,0,Math.PI*2,false);   //draw a circle
    ctx.fill();	
}

Nesting Listeners

The ability to attach and removes listeners at any point allows for the development of complex events. By combining and chaining different listeners, otherwise basic events can be layered to produce more natural, intuitive behavior. The following example combines mouseup,mousedown and mousemove events to simulate a dragging effect on the canvas. Two listeners are attached during initialization. When the mousedown event is executed, the callback function adds a listener for mousemove, which performs the visual effects of the interaction. The mouseup handler in turn removes the previously attached mousemove. The nested listeners thus create a more complex interaction for the user.

var canvas,ctx, mouseX, loc = 20;

function init(){
    canvas = document.getElementById('myCanvas');
    ctx = canvas.getContext('2d');
    drawCanvas();
    
    canvas.addEventListener('mousedown', startTracking, false);
    canvas.addEventListener('mouseup', stopTracking, false);
}

function findOffset(obj) {
    var curX = curY = 0;
    if (obj.offsetParent) {
        do {
           curX += obj.offsetLeft;
           curY += obj.offsetTop;
        } while (obj = obj.offsetParent);
        return {x:curX,y:curY};
    }
}

function startTracking(e){
    var pos = findOffset(canvas);
    mouseX = e.pageX - pos.x;
    canvas.addEventListener('mousemove', rotateRect, false);
}

function stopTracking(){
	canvas.removeEventListener('mousemove', rotateRect, false);
}

function rotateRect(e){
    var pos = findOffset(canvas);
    
    if(mouseX){
       var dX = ( e.pageX - pos.x ) - mouseX;
    }
    if(dX > 0) {    // moved right
       if(loc < canvas.width-20) loc += dX;
    } else {    //moved left
       if(loc > 20) loc += dX;  //this adds a negative number to the location variable
    }
    
    // re-constrain the location 
    if(loc < 20) loc = 20;
    if(loc > canvas.width - 20) loc = canvas.width-20;
    
    mouseX = e.pageX - pos.x;
    
    drawCanvas();
}

function drawCanvas(){
    var ctx = canvas.getContext('2d');
    ctx.clearRect(0,0,canvas.width,canvas.height);
    
    // draw line
    ctx.beginPath();
    ctx.moveTo(20,canvas.height-10);
    ctx.lineTo(canvas.width-20,canvas.height-10);
    ctx.stroke();
    
    // draw marker
    ctx.beginPath();
    ctx.moveTo(loc,canvas.height-10);
    ctx.lineTo(loc+6,canvas.height-20);
    ctx.lineTo(loc-6,canvas.height-20);
    ctx.closePath();
    ctx.fill();
    
    //draw rectangle
    ctx.save();
    // normalize loc and convert to rotation
    var rot = (loc-20)/((canvas.width-20)-20) * (Math.PI*2);
    ctx.translate(canvas.width/2,canvas.height/2);
    ctx.rotate(rot);
    ctx.fillStyle = 'purple';
    ctx.fillRect(-80,-80,160,160);
    
    ctx.restore();
}