2008-11-09

Creating dynamic client-side image maps with javascript/jQuery

I recently ran into an odd problem - I needed a dynamic client-side image map. Dynamic as in "the areas will be in the right spot regardless of how large the image is client-side".

I've been working on a site for the house I live in, which creates a lot of various forms of media. We decided to offload video hosting for videos we make to youtube, since we can't afford hosting costs for streaming video at this point in time. Youtube offers a pretty neat chromeless player that you can control through javascript, and the guy who does most of our design work whipped up a pretty nice-looking television with buttons image to use as the player. Given that he's not exactly very tech-savvy, it seemed like using an image map (which I haven't really heard much about for years) would be an appropriate way to make the player

Now, the problem with image maps is that since the coords attribute of the area tags is based off of pixels, you can't easily use an image that you don't know what dimensions it will end up being on the clients browser.

This is one of the situations I've run into where the solution probably would have been obvious if I had gone to college, but I enjoy solving problems anyhow - I got out the graph paper and pencil, drew a few boxes with points in them, and it became apparent that if I represented the coords for the areas as ((x/image_width)/(y/image_height)) instead of just (x/y), that it would be pretty easy to modify for any image size.

Javascript to the rescue:


//the height and width of the image on the server
CircleButton.prototype.base_width = 800;
CircleButton.prototype.base_height = 600;

//stores information about where buttons are as the ratio of
//(x/base_width)/(y/base_height)
function CircleButton(x, y, diameter, func) {
//express x,y,diameter as ratios instead of pixels.
this.x = x / this.base_width;
this.y = y / this.base_height;
this.diameter = diameter * ((this.base_width / this.base_height)
/ (this.x / this.y));
this.func = func;

this.adjust_coords = function(width, height) {
//coordinates for an image of (width, height) on the client
return [parseInt(width * this.x),
parseInt(height * this.y),
parseInt(this.diameter)];

}

this.tag_string = function(width, height) {
var coords = this.adjust_coords(width, height);
return '<area shape="circle" coords="' + coords.join() + '" href="javascript:' + this.func + '"/>';
}
}


This only works for circular buttons, but would be easy to modify for any of the other shape options (I only need circular buttons at the moment).

To use this on a page, you need to

  1. Initialize some buttons with the coords that they would have in the image on the server
  2. Find the image's width and height on the client
  3. Generate the area tags with the previously found width and height
Like so:

function setup_map() {
var base_buttons = [new CircleButton(525, 127, 15, 'ytplayer.playVideo();'),
new CircleButton(584, 153, 15, 'ytplayer.pauseVideo();')];

//size of the map image on the client
var map_width = $('#my-image').width();
var map_height = $('#my-image').height();

$('#my-map').empty();
$('#my-image').removeAttr('usemap');

var the_map = document.getElementById('my-map');

for (var i = 0; i < base_buttons.length; i++) {
(function(b) {
s = base_buttons[b].tag_string(map_width, map_height);
//we have to append to the innerHTML like this because of the way
//jQuery appends stuff
the_map.innerHTML += s;
})(i);
}

$('#my-image').attr('usemap','#my-map');
}


Assuming markup like this:


<div id="video" class="content">

<map id="my-map" name="my-map">
</map>

<img id="my-image" src="images/my-image.png"/>
<object width="48%" height="52%" id="video-player"
allowscriptaccess="always"
data="http://youtube.com/apiplayer?enablejsapi=1&playerapiid=ytplayer">
</object>
</div>


The calls to empty and removeAttr can probably be removed if, unlike me, your image and map are not residing in a jQuery tab. We have to append to the innerHTML of the map because of a quirk with how jQuery does appends - as far as I can tell, it appends to an empty div - and area tags aren't valid inside a div, so the browser ends up not displaying them. That was a bit frustrating to figure out.

Also, if you're like me, and the image and map are residing in a jQuery tab, you'll want to bind setup_map to the click attribute of the tab with a timeout like so:


$("a[href='#my-tab']").click(function() {
//we have to wait a little bit for the player
//image to load, otherwise it reports a width of 0
setTimeout('setup_map()', 100);
});


Just about any amount of delay should work, 100 milliseconds is unnoticeable, 500 would work if you wanted to be really safe

Image maps don't get much action these days, probably because when they were made, there wasn't much of a use for them - and now, there are other ways to accomplish what they were generally used for. I do feel that they were quite appropriate for making a custom player with youtubes chromeless player, and it was fairly straightforward to allow for differing image sizes on the client side. Hooray!

1 comment: