Sapan Diwakar

Software developer

Follow me on Twitter Check out my code on GitHub View some of my designs on Dribbble Take a look at my Linked In profile

Using HTML5 Canvas Element to create cool tagging interface

There have been several new additions to the world of web with the introduction of HTML5. I have been playing with canvas element for quite some time now and in my opinion, it is one of the best features introduced in HTML5. I am more interested in canvas element because I have been involved with image processing in the past and it allows us to play with images in a whole new dimension. In this tutorial, I would be explaining about the use of the canvas element to create a cool tagging interface. Tagging will allow users to add annotations to selected portions of the image so that when they hover the mouse over that particular portion of the image, the tag comes up. The tags can contain complete HTML text. That means that the user can also insert links, images etc. in the tags.

Lets create a new canvas element to begin with.

<div id="image-space">  
    <canvas id="image-canvas" width=750px height=750px></canvas>
</div>  

Now we can play with this canvas element using javascript and jquery. Obtain the canvas element using getElementByID, obtain canvas context and load an image into the canvas element.

var image = new Image();  
var canvas = document.getElementById('image-canvas');  
var context = imageCanvas.getContext('2d');

image.onload = function() {  
     context.drawImage(image, imageX, imageY); // Draw image only after                                                    // loading is finished
}

image.src = imageURL;  

Lets create a datastructure Box to hold all the tags that the user creates.

function Box() {  
    this.x = 0;
    this.y = 0;
    this.w = 1; // default width and height?
    this.h = 1;
    this.fill = '#444444';
    this.tag = 'default tag';
}

// Methods on the Box class
Box.prototype = {  
    draw: function(context, isFilled) {
        context.fillStyle = this.fill;
        context.strokeStyle = strokeStyle;
        // We can skip the drawing of elements that have moved off the screen:
        if (this.x &amp;amp;gt; WIDTH || this.y &amp;amp;gt; HEIGHT)
            return;
        if (this.x + this.w &amp;amp;lt; 0 || this.y + this.h &amp;amp;lt; 0)
            return;

        if (isFilled) {
            context.fillRect(this.x, this.y, this.w, this.h);
        } else {
            context.strokeRect(this.x,this.y,this.w,this.h);
        }

    } // end draw
}

//Initialize a new Box and add it
function addRect(x, y, w, h, fill, tag) {  
    var rect = new Box;
    rect.x = x;
    rect.y = y;
    rect.w = w
    rect.h = h;
    rect.fill = fill;
    rect.tag = tag;
    boxes.push(rect);   // boxes is an array that holds all our current tags
}

The next step is to bind mouse events to functions that will perform the tagging.

canvas.onmousedown = taggerMouseDown;  
canvas.onmouseup = taggerMouseUp;  
canvas.onmousemove = taggerMouseMove;

/**
 * Sets mx,my to the mouse position relative to the canvas
 */
function getMouse(e){  
    var element = canvas;
    offsetX = 0;
    offsetY = 0;

    if (element.offsetParent){
        do{
            offsetX += element.offsetLeft;
            offsetY += element.offsetTop;
        }
        while ((element = element.offsetParent));
    }

    // Add padding and border style widths to offset
    offsetX += stylePaddingLeft;
    offsetY += stylePaddingTop;

    offsetX += styleBorderLeft;
    offsetY += styleBorderTop;

    mx = e.pageX - offsetX;
    my = e.pageY - offsetY
}

// User started tagging
taggerMouseDown = function(e){  
    getMouse(e);

    taggerStarted = true;
    rectX = mx;
    rectY = my;
};

// Either user is moving mouse or drawing a box for tagging
taggerMouseMove = function(e){

    if (!taggerStarted){
        return;
    }

    getMouse(e);

    var x = Math.min(mx, rectX),
        y = Math.min(my, rectY),
        w = Math.abs(mx - rectX),
        h = Math.abs(my - rectY);

    mainDraw(x, y, w, h);  // This function draws the box at intermediate steps
}

// Tagging is completed
taggerMouseUp = function(e){  
    getMouse(e);
    if (taggerStarted){
        var tag = prompt("Enter any tag");
        if (tag != null && tag != "") {

            var rectH = my - rectY;
            var rectW = mx - rectX;

            if ( rectH &amp;amp;lt; 0) {
                rectY = my;
                rectH = -rectH;
            }
            if (rectW &amp;amp;lt; 0) {
                rectX = mx;
                rectW = -rectW;
            }

            if (rectW == 0 || rectH == 0) {
                alert("Error creating tag! Please specify non-zero height and width");
            } else {
                addRect (rectX, rectY, rectW, rectH, blurStyle, tag);
            }

            // Clear the canvas and draw image on canvas
            context.clearRect(0, 0, canvas.width, canvas.height);
            context.drawImage(image, imageX, imageY);
        }

        taggerStarted = false;
        taggerMouseMove(e);
    }
}

function mainDraw(x, y, w, h) {  
    context.clearRect(0, 0, canvas.width, canvas.height);
    context.drawImage(image, imageX, imageY);
    // Draw background stuff here

    if (!w || !h){
        return;
    }
    context.strokeRect(x, y, w, h);
}

Using these mouse actions, the user can now assign tags to certain regions in the image.

Once the user is done with choosing all the tags that he wants, we can then change the mouse actions to show the annotations on the image on mouse over. This time we will have to handle mousemove event. No action would be performed on other two events.

var annotation = false;  
var annotationRemove = false;

canvas.onmousedown = null;  
canvas.onmouseup = null;  
canvas.onmousemove = annotatedMouseMove;

annotatedMouseMove = function(e){  
    getMouse(e);

    var backgroundDrew = false;

    var l = boxes.length;
    var i = 0;
    for (i = 0; i &amp;amp;lt; l; i++) {
        if (mx > boxes[i].x && mx < boxes[i].x+boxes[i].w && my > boxes[i].y && my < boxes[i].y+boxes[i].h) {
            if (i != prevRectIndex) {
                prevRectIndex = i;
                context.clearRect(0, 0, canvas.width, canvas.height);
                drawImage (context, image, resizeFactor);
                boxes[i].draw(context, true);
                drawAllBoxes(false);

                // Remove previously shown annotation (important when two annotations overlap)
                if (annotation) {
                    m_container.get()[0].removeChild(annotation);
                    annotation = false;
                }
                if(annotationRemove) {
                    m_container.get()[0].removeChild(annotationRemove);
                    annotationRemove = false;
                }

                // Show annotation on mouse over
                annotation = document.createElement('div');
                annotation.style.position = 'absolute';
                annotation.style.top = (offsetY + boxes[i].y) + 'px';
                annotation.style.left = offsetX + boxes[i].x + 'px';
                annotation.style.width = boxes[i].w + 'px';
                annotation.style.lineHeight = boxes[i].h + 'px';
                annotation.className += ' tagger-annotation';

                annotation.innerHTML = boxes[i].tag;

                annotationRemove = document.createElement(&amp;amp;quot;button&amp;amp;quot;);
                annotationRemove.style.position = 'absolute';
                annotationRemove.style.top = offsetY + boxes[i].y + 'px';
                annotationRemove.style.left = offsetX + boxes[i].x + boxes[i].w - 20 + 'px';
                annotationRemove.className += ' annotation';
                annotationRemove.className += ' tagger-annotation-action-remove';

                annotationRemove.onclick = function() {
                    removeAnnotation(i);
                };

                document.getElementById('image-space').appendChild(annotation);
                document.getElementById('image-space').appendChild(annotationRemove);
            }
            break;
        }
    }

    if (i == l &&; prevRectIndex != -1) {
        context.clearRect(0, 0, canvas.width, canvas.height);
        drawImage (context, image, resizeFactor);
        drawAllBoxes(false);
        prevRectIndex = -1;
        if(annotation) {
            document.getElementById('image-space').removeChild(annotation);
            annotation = false;
        }
        if(annotationRemove) {
            document.getElementById('image-space').removeChild(annotationRemove);
            annotationRemove = false;
        }
    }
}

var removeAnnotation = function(i) {  
    boxes.splice(i,1);
    canvasElem.mousemove();
}

And the associated css

.tagger-annotation {
    padding : 0 5px;
    vertical-align : middle;
    text-align : center;
    white-space : nowrap;
}

/* remove button */
.tagger-annotation-action-remove {
    background-image: url(../images/remove.png);
    cursor: pointer;
}

.tagger-annotation-action-remove:hover, tr:focus {
    border: 1px solid #CCC;
    background-color: #FFF;
}


.tagger-annotation-action-remove{
    border: 1px solid transparent;
    height: 20px;
    width: 20px;
    overflow: hidden;
    background-color: transparent;
    background-attachment: scroll;
    background-repeat: no-repeat;
    background-position: 1px 1px;
    padding: 0;
    margin: 0;
}

This is it! I have broken the tutorial into steps. However, there are a few gaps which can be easily filled in. Following these steps, you can create wonderful tagging interfaces easily. Here's a image of how it looks like.