Tag Archives: html 5

Creating Image Maps From Canvas-Derived Coordinates

10 Jun 2015

Here’s a cool thing – I came across a situation where I was stacking identically-sized transparent PNG’s on top of each other but needed to be able to select their visible areas. The layered nature of z-ordering the images prevented us from getting beyond the top-most layer (actually, not image tags but z-ordered divs with background-images, but for all intents and purposes its the same issue).

The first thought was to use HTML5 Canvas and then track the coordinate of the users click/touch event to figure out what they were clicking on. A nice start but our browser requirements included old IE which prevented us from using Canvas so we were stuck with the PNG stack. In addition to dealing with old IE the requirements of the project meant also that we couldn’t simply merge all the PNG’s and then simply create an image map because:

  • Our client would be uploading the UI-related PNG’s into the system – both on and off states
  • We couldn’t rely on the client to be smart enough to draw image maps within the admin console each time an image was uploaded, especially since some of the art bumped or “went under” some of the other art within the PNG stack.

I thought we could still use Canvas if we applied it to the process of uploading the images – knowing that we essentially had small bitmaps “floating” within a larger transparent PNG I realized that we might be able to get some useful data from a Canvas, maybe enough to determine what should be clickable and what shouldn’t.

A quick Google search revealed this post at stackoverflow. It describes a “Marching Squares Edge Detection” algorithm that when applied to my needs would give me an array of coordinates that could be easily converted into an image map.

Detecting Multiple Edges

Looking at the edge detection algorithm showed that it detects the edge for the first shape that it encounters, ignoring anything else in the Canvas even though other shapes may exist. The fix here was to remove the shape that it finds and then run the algorithm again, repeating the find/remove process until nothing else is found. This is done while saving the boundary of each shape so that when the entire process was done we could use that data to create the image maps.

As for removing the first shape that was encountered I wrote the following – it draws a shape on the canvas exactly where the found shape is using that shape’s boundary and then overwrites it with a fill. Since I set the Canvas globalCompositeOperation to destination-out the end result is that the shape is removed from the canvas thus allowing me to find the next shape’s boundary as the previous shape no longer exists.

    function _removeBitmap(){
        var i, len;
	// draw outline path
	_ctx.globalCompositeOperation = 'destination-out';
	_ctx.beginPath();
	_ctx.moveTo(_points[0][0],_points[0][1]);
	for (i=1,len=_points.length;i<len;i++){
    	    var point = _points[i];
	    _ctx.lineTo(point[0],point[1]);
	}
	_ctx.closePath();
	_ctx.fill();
	_ctx.globalCompositeOperation = 'source-over';
    }

Detecting if the Canvas is Empty

Next, before I call the edge detection function again I first need to know if the Canvas is empty – so here’as another function that looks to see if the alpha of any pixel is set below a certain threshold. Why a threshold? Well, it seems that even though I can’t see the bitmaps that I removed with the destination-out composite operation that there may still be pixels here and there that do exist though are effectively invisible. A threshold settles that particular issue, you may need to play with it on your own if you use this code.

    ns.isEmpty = function(){
        var data = _ctx.getImageData(0,0,_canvas.width,_canvas.height).data;
        var emptyThreshold = 20; // maximum allowed alpha before a pixel is considered "empty"
        var i, l;
        var retVal = true;
        // maxAlpha: what is the highest alpha? most times its not zero.
        // log this to the console to see what the max alpha is, 
        // then set "emptyThreshold" accordingly.
        var maxAlpha = 0;  
	for (i=0,l=data.length; i < l; i += 4){
            // for debugging purposes
            maxAlpha = data[i + 3] > maxAlpha ? data[i + 3] : maxAlpha; 
		if(data[i + 3] > emptyThreshold){
                retVal = false;
	    }
	}
        return retVal;
    }

Yes, I know about loading up a blank canvas, getting its base64 via toDataURL and then trying to compare against it to see if a Canvas is empty – but note again how the composite operation left some pixels behind which means that comparing against a truly blank canvas wouldn’t work.

Working Example

Here’s a working example – inspect the iFramed page and note the absence of any image maps, then click “Start”. What you will see is:

  • Each image is loaded into the Canvas
  • The edge detection script finds the first “floating” shape, I remove it and then run the edge detection again, repeating until i determine that the image is now empty of any “solid” shapes
  • The next image is loaded and the process repeats
  • Once all edges have been found the image map is added to the DOM

NOTE: click start and let the sample run through all of the edge detection for everything (slower on mobile as all of the images load synchronously). It will be done when all the pieces display. From there you can click the other buttons.

You will note that the Canvas is still in this proof-of-concept after all of the edge detection is completed and the image maps are added – the final implementation of this stacks all images via absolute positioning without any Canvas elements. The top-most image of the stack has the image map applied to it. In this way we are able to automate the creation of the image maps within the browser when each image is uploaded into the system via the purpose-built CMS and not need to worry about the client using some sort of drawing tool to create the image maps themselves.

Finished Code

Here’s the result – separated from the edge detection stuff which I broke out into its own file that you can download from here while viewing the source of the example to learn how the parts were assembled.

;(function(ns,$){

    var _canvas, _ctx, _cw, _points, _imgData, _data;
    var _allPaths = [];
    var _img = new Image();
	_img.crossOrigin = 'anonymous';
	_img.onload = _drawImgToCanvas;

    function _drawImgToCanvas(){
	_ctx.drawImage(_img,_canvas.width/2-_img.width/2,_canvas.height/2-_img.height/2);
        _findArea();
    }

    function _findArea(){
        _imgData = _ctx.getImageData(0,0,_canvas.width,_canvas.height);
        _data = _imgData.data;
        _points = marchingSquares.contour();// call the marching ants algorithm
	_allPaths[_allPaths.length] = _points;// store the area in the _allPaths collection
	_removeBitmap();// remove the shape so we can move on to the next one
    }

    function _removeBitmap(){
        var i, len;
	// draw outline path
	_ctx.globalCompositeOperation = 'destination-out';
	_ctx.beginPath();
	_ctx.moveTo(_points[0][0],_points[0][1]);
	for (i=1,len=_points.length;i<len;i++){
	    var point = _points[i];
	    _ctx.lineTo(point[0],point[1]);
	}
	_ctx.closePath();
	_ctx.fill();
	_ctx.globalCompositeOperation = 'source-over';
        
        if (ns.isEmpty()){
            _createMap();
        } else {
            _findArea();
        }
    }
	
    ns.isEmpty = function(){
	var data = _ctx.getImageData(0,0,_canvas.width,_canvas.height).data;
        var emptyThreshold = 20; // maximum allowed alpha before a pixel is considered "empty"
        var i, l;
        var retVal = true;
        var maxAlpha = 0; // what is the highest alpha? most times its not zero. log this to console to see what the max alpha is, set "emptyThreshold" accordingly.
	for (i=0,l=data.length; i < l; i += 4){
            maxAlpha = data[i + 3] > maxAlpha ? data[i + 3] : maxAlpha; // for debugging purposes
	    if(data[i + 3] > emptyThreshold){
                retVal = false;
	    }
	}
        return retVal;
    }
	
    function _createMap(){
	var mapTPL = '%areas%';
	var areasTpl = '';
	var areas = '';
	var map = '';
	var coordsList = '';
	for (var h=0,len=_allPaths.length;h<len;h++){
 	    coordsList = '';
	    for (var i=0,len2=_allPaths[h].length;i<len2;i++){
		coordsList += _allPaths[h][i].join(',');
		coordsList += i != _allPaths[h].length -1 ? ',' : '';
	    }
	    areas += areasTpl.replace('%coords%',coordsList);
	}
	map = mapTPL.replace('%areas%',areas);
	$('#mapWrapper').html(map);
    }
    
    ns.returnData = function(){
	return _data;
    }
	
    ns.returnCW = function(){
	return _cw;
    }
	
    ns.init = function(canvasID,imgSrc){
	_canvas = document.getElementById(canvasID);
	_ctx = _canvas.getContext('2d');
	_cw = _canvas.width;
	_img.src = imgSrc;
    }
	
})(this.mapFromCanvas = this.mapFromCanvas || {}, jQuery);

JQuery Mobile Checkboxes

31 Oct 2012

JQuery Mobile has some nice form element replacements that are more mobile device/touch-friendly than the way browsers currently present form elements. One of them is the visual replacement for a checkbox. To be clear the ugly checkbox is still there, its now layered underneath the JQM checkbox art.

The end results looks like this:

Click here to see a working example.

The docs IMO don’t quite tell you one critical thing – that certain tag attributes need to be identical in order for the framework to enhance the checkbox. If one of these is out of place you won’t get what you’re looking for.

Here’s how I implemented a checkbox today after trial and error:

...
  
...
  
...

According to the docs the only thing to pay attention to is to ensure that the LABEL tag’s for attribute value is the same as the INPUT tag’s name value so that they are semantically linked together and displayed appropriately by the framework. There is one critical piece of information and that’s the INPUT tag’s id – it too must be the same value as the previous two attribute values. Omit that last bit and you don’t get any visual enhancement of the CHECKBOX element.

As far as getting the label removed all that is needed (as can be seen in the code sample above) is to add data-iconpos=”notext” to the input tag.

Lastly, I wanted to set the width so I added a class to the FIELDSET element and that was it, my check box was created and ready to go.

A handy tip is the use of the jQuery trigger method when inserting checkboxes into your document after it has loaded. Doing so ensures that you get the visual enhancement.

For example:

...
var newcbox = '';
    newcbox = '';

// add html to DOM and "trigger" jQuery visual enhancement
$(newcbox).appendTo('#contentWrapper').trigger('create');
...