Friday, April 19, 2013

Objects in HTML5 canvas (part 2: interacting with objects)

In the previous part I have shown how to draw simple shapes to the canvas.

If you need to interact with a specific object in the canvas you have to face a problem.

Using DOM you can click on a node and the browser manages the whole interaction (triggers the event, puts the DOM node in event.target, etc.).

But, as explained before, once you have drawn an object to a canvas it become a part of the canvas. So the question is: how to detect when a user click on a specific shape?

Getting canvas related coordinates

The first obstacle to get through is getting where a user clicked on the canvas. When a user clicks you can get the mouse coordinates relative to the page and you must subtract the canvas position:

canvas.addEventListener('click', function (evt){
    var rect = canvas.getBoundingClientRect(),
        posx = evt.clientX - rect.left,
        posy = evt.clientY - rect.top;
    ...

It's not too complicate, isn't it?

Checking in reverse order

In the first part of this tutorial I put the objects to draw in an array.

var objs = [
    {name:'shape2',
     color:'green',
     x:30,
     y:50,
     angle:Math.PI/2,
     width: 40,
     height: 50
     },
    {name:'shape1',
     color:'red',
     x:40,
     y:30,
     angle:Math.PI/4,
     width: 30,
     height: 40
     }
];

Then each shape is drawn in order over the previous.
For this reason I have to check shapes in reverse order, from the object in foreground to the one in background.

        for (i = objs.length - 1; i >= 0; i--){
            obj = objs[i];
            ...

Pointer transformations

Now I have pointer coordinates and shape transformations:

    {name:'shape1',
     color:'red',
     x:40, // translation x
     y:30, // translation y
     angle:Math.PI/4, // rotation
     width: 30,
     height: 40
     }

How to detect if pointer coordinates are inside this shape?

My solution is a bit imaginative but it seems to work well.
I apply transformations to pointer coordinates. These are the opposite that I have applied to the object before drawing it to the canvas. Then I check if the new pointer coordinates are inside the object shape pretending that the shape has drawn around 0, 0.

// pointer transformation
p = new Point(posx, posy);
p.translate(-obj.x, -obj.y);
p.rotate(-obj.angle);

if (p.x > -(obj.width / 2) && p.x < obj.width / 2 && p.y > - (obj.height / 2) && p.y < obj.height / 2){
    log.innerHTML = 'clicked on ' + obj.name;
    return;
}            


I wrote this simple object to help with transformations (see part 1 for an explanation on transformations):

var Point = function (x, y){
    this.x = x;
    this.y = y;
};

Point.prototype.rotate = function (angle){
    var x, y;
    x = this.x*Math.cos(angle) - this.y * Math.sin(angle);
    y = this.x*Math.sin(angle) + this.y * Math.cos(angle);
    this.x = x;
    this.y = y;
    return this;
};

Point.prototype.translate = function (x, y){
    this.x = this.x + x;
    this.y = this.y + y;
    return this;
};

This is the whole code:

canvas.addEventListener('click', function (evt){
    var rect = canvas.getBoundingClientRect(),
        posx = evt.clientX - rect.left,
        posy = evt.clientY - rect.top,
        i, obj,p;
        // I get the position of the pointer
        // relative to the canvas
        for (i = objs.length - 1; i >= 0; i--){ // cycling in inverse order
            obj = objs[i];
            // pointer transformation
            p = new Point(posx, posy);
            p.translate(-obj.x, -obj.y);
            p.rotate(-obj.angle);

            if (p.x > -(obj.width / 2) && p.x < obj.width / 2 && p.y > - (obj.height / 2) && p.y < obj.height / 2){
                log.innerHTML = 'clicked on ' + obj.name;
                return;
            }            
            
        }
        log.innerHTML = '';

}, false);

This is the result (try to click on a shape):
Your browser doesn't support canvas!
In the next part I'll add a more fine control on images using off-screen canvases. Stay tuned!!!