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!!!

Friday, April 12, 2013

Objects in HTML5 canvas (part 1: drawing shapes)

Canvas is an HTML5 element which can be used to draw graphics in the browser. Its API is very low level and it has no notion of shapes or paths after they are drawn.
It contains just a bidimensional matrix of points.

This little guide introduces you to use effectively this API to write shapes and control them with the mouse.

In this simple example we are going to draw some rectangular shapes.

Save the shapes to draw

The first thing to do is to save the shapes inside 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
     }
];
Objects inside the array will be drawn one after the another with the first object in the background and the last object in foreground.

var i, obj;
ctx.clearRect(0, 0, canvas.width, canvas.height); //empty the canvas
for (i = 0;i < objs.length;i++){
    obj = objs[i];
    ctx.save(); // save context

    ctx.translate(obj.x, obj.y); // apply transformations (I'll explain later)

    ctx.rotate(obj.angle);

    ctx.fillStyle = obj.color; // draw
    ctx.fillRect(
            -(obj.width / 2),
            -(obj.height / 2),
            obj.width,
            obj.height
        );
    
    ctx.restore(); // restore the context saved

}
The canvas coordinates start with 0,0 on the upper left corner. but you usually don't need to calculate the position of each shape. The canvas API gives the power to move the axis instead, and then draw the shapes around the point 0,0.

Introducing transformations

Transformations moves X and Y axis in the space. In the bidimensional world there are 3 main type of transformations:

  • translation
  • skew
  • scale

They are usually represented with a matrix

x   a1,b1,c1
y   a2,b2,c2
1    0,0,1

that express these three equations

x' = a1x + b1y + c1
y' = a2x + b2y + c2
1 = 0x + 0y + 1

These equations transform the original (x, y) coordinates in a new pair of coordinates (x', y').
In bidimensional transformations the last equation is an identity (It is alway true).
If you pay enough attention you will notice that you can define a matrix that doesn't change the original coordinates. This is called the identity matrix:

x   1,0,0
y   0,1,0
1   0,0,1

x' = 1x + 0y + 0 = x
y' = 0x + 1y + 0 = y
1 = 0x + 0y + 1

Now starting with the identity matrix I'll try to explain transformations (I am not a mathematician so forgive me if the explanation is not formally correct or inaccurate).

t1 and t2 are translations (in the x and y axis respectively). They move the axis left/right and up/down.
A translation of 0 means no translation.
In fact:

x   1,0,t1
y   0,1,t2
1   0,0,1

x' = 1x + 0y + t1 = x + t1
y' = 0x + 1y + t2 = y + t2
1 = 0x + 0y + 1

s1 and s2 means scale. s1 scales the x axis while s2 scales the y axis. A multiplication of 1 means no scale.

x   s1,0,0
y   0,s2,0
1   0,0,1

x' = s1x + 0y + 0 = s1*x
y' = 0x + s2y + 0 = s2*y
1 = 0x + 0y + 1

sk2 and sk1 skew respectively the x and y axis

x   1,sk1,0
y   sk2,1,0
1   0,0,1

x' = 1x + sk1y + 0 = x + sk1*y
y' = sk2x + 1y + 0 = y + sk2*x
1 = 0x + 0y + 1

Now the question is: and the rotation ?

The rotation is a combination of two transformations: skew and scale.
The formula is

x   cos(angle),-sin(angle),0
y   sin(angle),cos(angle),0
1   0,0,1

x = x * cos(angle) + y * -sin(angle)
y = x * sin(angle) + y * cos(angle)

The order in which you apply the tranformation is very important:
In fact if we first rotate and then translate we are translating the axis using the new rotated axis.

As a rule of thumb, we usually need to:
  • translate
  • rotate
  • scale

Back to canvas

Canvas has a complete API for transformations. 
You can get more information on MDN.
In the previous example we applied translate and rotate tranformations.

These functions change the state of our drawing context. And affects all drawing operations so, in order to apply different transformations on each shape, we need to reset the drawing context every time.
Otherwise the next transformation would be applied over the previous.


Your browser doesn't support canvas!
Next part I'll show how to interact with objects.

Friday, January 11, 2013

Backbone.occamsrazor.js

Despite of the awful name this is one of the nicest experiments I ever done (at least in Javascript :-) ).
I used occamsrazor.js to enable heterogeneous collections in backbone.js.

What ?

Backbone.js api is based on a RESTful architecture. This architecture is based on "resources" and "verbs". Resources are atomic unit of information identified by URLS. Verbs are an unified interface to make basic operations on resources (GET, PUT, POST, DELETE). 
3 of these VERBS operates on a single resource (GET, PUT and DELETE). POST operate on a special resource called "collection". A POST request on a collection causes the creation of a new resource. Backbone.js simplify the work allowing to issue a GET request to a collection. It is very useful to fetch all the models contained in a collection.
All of these client/server exchanges use basic JSON objects. These objects are just a bunch of attributes put together without a notions of the original model.
In order to transform this bunch of attributes in a full fledged object (with methods, prototype etc.) the backbone collection makes a simple assumption: all the object contained in a collection must be of the same type (model).

A positive side effect: models and views are now plugins!

occamsrazor.js enable collections formed by different models. As a (positive) side effect the models and model views are now plugins. I wrap the results in a simple backbone ehnancement.

Models and collections

Let's start with a simple empty collection:
    var shapesCollection = new Backbone.Occamsrazor.Collection;
With classic backbone collection you should define the model of the objects contained.
With Backbone.Occamsrazor.Collection instead you set what kinds of models it could contain. But first of all define some validator:
    var hasWidth = function (obj){
            if (obj instanceof Backbone.Model){
                return obj.has('width');
            }
            return 'width' in obj;
        },
        hasHeight = function (obj){
            if (obj instanceof Backbone.Model){
                return obj.has('height');
            }
            return 'height' in obj;
        },
        hasRadius = function (obj){
            if (obj instanceof Backbone.Model){
                return obj.has('radius');
            }
            return 'radius' in obj;
        },
        hasWidthHeight = occamsrazor.chain(hasWidth, hasHeight);
These validators take an object and returns a positive number if the object has a feature.
In this case is convenient validate both a simple object and a Backbone model.
You can find further explanations about validators in the occamsrazor.js documentation.
    shapesCollection.model.addConstructor(hasWidth, Backbone.Model.extend({
        getArea: function (){
            var w = this.get('width');
            return w*w;
        }
    }));

    shapesCollection.model.addConstructor(hasWidthHeight, Backbone.Model.extend({
        getArea: function (){
            var w = this.get('width'),
                h = this.get('height');
            return w*h;
        }
    }));

    shapesCollection.model.addConstructor(hasRadius, Backbone.Model.extend({
        getArea: function (){
            var r = this.get('radius');
            return Math.round(Math.PI * r * r);
        }
    }));
From now the collection can work with three kind of models transparently:
    shapesCollection.add([{width: 10, height: 5}, {width: 10}, {radius: 3}]);
    
    console.log(shapesCollection.at(0).getArea()); // 50
    console.log(shapesCollection.at(1).getArea()); // 100
    console.log(shapesCollection.at(2).getArea()); // 28

Views

If you use an heterogeneus collection you will surely need something analogous for the views.
You will probably need a different view for each model. This works almost the same as models. First create a collection view::
    var shapesView = new Backbone.Occamsrazor.CollectionView({collection: shapesCollection, el: $('#myid')});
And then add the views:
    shapesView.itemView.addConstructor([null, hasRadius], Backbone.Occamsrazor.ItemView.extend({
        tagName:  'div',
        render: function (){
            this.$el.html('The area of the circle is ' + this.model.getArea());
            return this;
            
        }
    }));

    shapesView.itemView.addConstructor([null, hasWidth], Backbone.Occamsrazor.ItemView.extend({
        tagName:  'div',
        render: function (){
            this.$el.html('The area of the square is ' + this.model.getArea());
            return this;
        }
    }));

    shapesView.itemView.addConstructor([null, hasWidthHeight], Backbone.Occamsrazor.ItemView.extend({
        tagName:  'div',
        render: function (){
            this.$el.html('The area of the rectangle is ' + this.model.getArea());
            return this;
        }
    }));
You should notice that I used Backbone.Occamsrazor.ItemView as view constructor function. This is nearly identical to Backbone.View: the only difference is in the way the model is passed to the costructor.
This emphasizes the fact that you must pass the model as argument and allows occamsrazor to pick the right view for a that model.

I have prepared a simple demostration of the potential of this library here. This is the classic todomvc application by Addy Osmani. The interesting part is in how it is simple adding plugins to enhance the application without touching the original code.

I hope this makes clear how occamsrazor.js can be helpful !