Friday, April 26, 2013

Objects in HTML5 canvas (part 3: interacting with images)

This article is the third of a series of simple recipes that explains how to interact with objects in HTML5 canvas.
  • part 1 (Drawing shapes)
  • part 2 (Interacing with objects)
In this part we will solve the last problem: how to interact with objects with irregular shapes.

The simple answer is that you don't have to. You can wrap any image inside a rectangular box and detect where the image is transparent.

Introducing offscreen canvas

An offscreen canvas is a canvas outside the DOM. It's very useful for 2 reasons:
  • you can do complex drawing once and reuse the result with just an instruction
  • you can use it to check where an image is transparent

Let's draw

First of all I start with an images array (I used inline images, but you can use images urls):

var objs = [
    {name:'firefox',
     src:'data:image/png;base64,iVBORw0KGgoAAAA.....',
     x:30,
     y:50,
     angle:Math.PI/2,
     },
    {name:'opera',
     src:'data:image/png;base64,iVBORw0KGgoAAAA...',
     x:40,
     y:30,
     angle:Math.PI/4,
     },
    {name:'chrome',
     src:'data:image/png;base64,iVBORw0...',
     x:60,
     y:100,
     angle:-Math.PI/4,
     }
];

Then I draw each image inside an offscreen canvas and, then the offscreen canvas in the main canvas:

var i, obj;
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (i = 0;i < objs.length;i++){
    obj = objs[i];
    (function (obj){
        var img = new Image();   // Creating a new img element
        img.onload = function(){
            // It's very important to wait until the image loads!!!
            // otherwise I have no clue of the real size of the image 
            ctx.save();

            ctx.translate(obj.x, obj.y);
            ctx.rotate(obj.angle);
            obj.width = img.width;
            obj.height = img.height;
            //I attach the offscreen canvas to the object
            obj.canvas = document.createElement("canvas");
            obj.canvas.width = img.width;
            obj.canvas.height = img.height;
            obj.ctx = obj.canvas.getContext('2d');
            // I draw the image on a off-screen canvas
            obj.ctx.drawImage(img, 0 , 0);
            // I draw the off-screen canvas on the main canvas
            ctx.drawImage(obj.canvas, -img.width/2 , -img.height/2);
            
            ctx.restore();
        };
        img.src = obj.src; // Set source path
    }(objs[i]));
}

Pay attention! I have applied transformations on the main canvas and not on each  off-screen canvases.

Detecting mouse clicks

Let's finish detecting on which shape a user is pointing. As usual I check each object in reverse order and transform pointer coordinates. Then I must retrieve the image from the offscreen canvas and test the color on the resulting coordinates. If the alpha value is opaque the pointer is on the image.

    // I get a single pixel
    imageData = obj.ctx.getImageData(p.x+(obj.width/2), p.y+(obj.height/2), 1, 1);
    // imageData contains r,g,b,a
    if(imageData.data[3] > 50){ // 50 is the threeshold
        log.innerHTML = 'clicked on ' + obj.name;
        return;
    }            

I have taken a pixel from the canvas using getImageData. This function returns  an array that include 4 values for each pixel (just one in my case).
The values are Red, Green, Blue and Alpha (transparency) and they have values between 0 and 255.
I set a threeshold of 50 on alpha.
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 = objs.length,
        obj,p;
        for (; i > 0; i--){
            obj = objs[i-1];
            // translate coordinates
            p = new Point(posx, posy);
            p.translate(-obj.x, -obj.y);
            p.rotate(-obj.angle);

            imageData = obj.ctx.getImageData(p.x+(obj.width/2), p.y+(obj.height/2), 1, 1);

            if(imageData.data[3] > 50){
                log.innerHTML = 'clicked on ' + obj.name;
                return;
            }            
            
        }
        log.innerHTML = '';

}, false);


And this is the results:
  Your browser doesn't support canvas!


I hope this is helpful!