Preface
Thanks to those of you who take the time to read through the article. If you feel like dropping off a vote (and particularly if it's a low one), please include a comment that mentions what the problem was. Feedback is what drives improvement.
Introduction
The HTML map element defines a client-side image map: a set of regions that are bound to an img
element. From a web designer's point of view, it would be nice to be able to highlight or replace the currently active region. Pure HTML doesn't provide a way to alter the contents of an image map. area
tags -- the immediate children of the map
-- are nothing more than hyperlink hot spots. They do not allow any styling, such as changing a background image or border color, for instance. The easiest way to indicate a current map region via JavaScript is to change the entire image on the mouseover
event and then restore the original on mouseout
. Here is an example. This method can be very bandwidth-consuming if the target image is big. Another way, which involves CSS and overlay images, is much better in terms of bandwidth, but is very complex to install.
The most popular (and the most practical) method of creating an interactive image-map is the use of overlay images. This was described in detail in my previous article. However, it has drawbacks. The biggest one is that you have to create a distinct image for every single area. In the case of geographical maps, this task can consume huge amounts of time.
This article introduces an alternative approach, which makes use of embedded vector graphics -- namely, SVG and VML -- that are supported by modern browsers. This script requires nothing but the original image and the image map derived from it. Thus, it eliminates the most time-consuming step of the previous approach, but still provides fast and juicy-looking operation. Installation of the script requires a casual knowledge of HTML and JavaScript. The script is perfectly compatible with all mainstream browsers: Mozilla/Firefox 2, Opera 9 and MS IE 6/7. Lightmapper/v script is distributed under the zlib/libpng license.
This article assumes that you know what SVG and VML are. If you don't, I advise you to read this article.
Setup
- Let's assume that you have the image. In my case, it was the map of Europe. You need to create an image-map. If your image is simple, you can use Imagemapic. For more complex images, you can use Image Mapper (free and I've used it), MapEdit (free trial version is available) or the GIMP image-mapping plug-in. Of course, you can also use your favorite Adobe/Macromedia ubertool if you can afford it.
- Create a new HTML page; embed an image and the image-map you created to this page.
- Include the references to lightmapperv.js and svg+vml.js in your HTML file:
<html>
<head>
<script type="text/javascript" src="svg+vml.js"></script>
<script type="text/javascript" src="lightmapperv.js"></script>
- Create an empty function -- I've called it
prepare
-- in the HEAD
of your page and bind it to the BODY
's onload
event:
<html>
<head>
...
<script type="text/javascript">
function prepare()
{
}
</script>
</head>
<body onload="prepare()">
- In that function, create the
LightmapperV
object:
function prepare()
{
new LightmapperV("image_of_europe", , ,
[ ["Albania", "#85D185", -1, -2],
["Austria", "#A0A0D1", -1, -1],
["Belarus", "#7FD1D1", -1, -1],
["Belgium", "#D1D172", -1, -1],
["BosniaAndHerzegovina", "#A0A0D1", -1, -1],
...
["Sweden", "#7FD1D1", -1, -1, , , "Sweden_Gotland"],
["Sweden_Gotland", "#7FD1D1", -1, -1, , , "Sweden"],
["Switzerland", "#D1D172", -1, -1],
["Ukraine", "#D19696", -2, -1] ]);
}
VoilĂ ! The interactive image-map works.
The constructor of the LightmapperV
object may seem quite complex. It isn't; here is the explanation:
function Lightmapper(imgId,
mouseOver,
mouseOut,
bindings)
Note: The script makes use of displacement values, specified in pixels, that are used to align the created vector polygons with the original image. These values must be found experimentally by your artist/programmer during the art setup.
Inner Workings
Most of the JavaScript voodoo that makes LightmapperV possible is explained in my article "Bridge design pattern with JavaScript." The svg+vml.js file was taken directly from that article's source archive. lightmapperv.js itself does little:
- It creates the drawing "canvas:"
var ct = document.getElementById(this.imgId), _ct_ = ct.parentNode;
var div = this.canvas = document.createElement("DIV");
this._reposition(this);
_ct_.appendChild(div);
var jvg = new VectorGraphics(div);
jvg.SetOpacity(0);
jvg.SetStrokeWidth(0);
- It creates vector polygons:
for(var i = 0, l = this.binds.length; i < l; i++)
{
var elem = this.binds[i],
area = document.getElementById(elem["area"]);
var coords = area.coords.split(","), coords2 = [];
var m = coords.length;
while(m)
{
coords2[m / 2 - 1] = [++coords[--m - 1] + elem["xdsp"],
+ coords[--m + 1] + elem["ydsp"]];
}
jvg.SetFillColor(elem["colr"]);
jvg.SetStrokeColor(elem["colr"]);
var polygon = this.binds[i].overlay = jvg.Polygon(coords2);
}
- It sets up mouse events:
var mi = elem["musi"] || this.mouseOver;
var mo = elem["muso"] || this.mouseOut;
this._setup_event(polygon,
"mouseover", this._callLater(this._fade, polygon, 1, 0.5, 0.1, mi));
this._setup_event(polygon,
"mouseout", this._callLater(this._fade, polygon, 0, 0.5, 0.1, mo));
if(area.href)
{
function go(href)
{
return function() { window.location.href = href; }
}
this._setup_event(polygon, "mousedown", go(area.href));
}
- It finds and binds the linked areas:
var bl = this.binds.length;
for(var i = 0; i < l; i++)
{
var obj = this.binds[i].overlay;
var links = this.binds[i]["link"], ll = links.length;
for(var k = 0; k < ll; k++)
{
for(var j = 0; j < bl; j++)
{
var elem = this.binds[j].overlay;
if(links[k] == elem["area"])
{
obj.links[obj.links.length] = elem;
break;
}
}
}
}
- It can fade-in/fade-out polygons on mouseover/mouseout respectively:
_fade: function(obj, destOp, rate, delta, callback)
{
if(obj.timer) clearTimeout(obj.timer);
var proto = LightmapperV.prototype;
var curOp = parseFloat(proto._getSetOpacity(obj));
var direction = (curOp <= destOp) ? 1 : -1;
var links = obj.links, bindings = obj.parent.binds;
var bl = bindings.length, ll = links.length;
if((destOp > curOp) && (curOp == 0))
{ { for(var i = 0; i < bl; i++)
{
var elem = bindings[i].overlay;
if(elem != obj)
{
for(var j = 0; j < ll; j++)
if(links[j] == elem)
break;
if(j == ll) proto._fade(elem, 0, 0.5, 0.1);
}
}
} }
delta = Math.min(direction * (destOp - curOp), delta);
curOp += direction * delta;
curOp = Math.round(curOp * 10) / 10;
proto._getSetOpacity(obj, curOp);
for(var j = 0; j < ll; j++)
proto._getSetOpacity(links[j], curOp);
if(curOp != destOp)
obj.timer = setTimeout(function()
{
proto._fade(obj, destOp, rate, delta, callback);
}, rate);
else
{
if(callback)
callback(obj.area); }
}
- While fading, is gets or sets the polygon's opacity:
_getSetOpacity: function(polygon, opacity)
{
if(polygon.hasAttributes && polygon.hasAttributes("fill-opacity"))
{
if(opacity != undefined)
{
polygon.setAttribute("fill-opacity", opacity);
polygon.setAttribute("stroke-opacity", opacity);
return opacity;
}
else
return polygon.getAttribute("fill-opacity");
}
else if(polygon.tagName.toLowerCase() == "shape")
{ var c = polygon.childNodes.length;
for(var i = 0; i < c; i++)
{
var child = polygon.childNodes[i],
tag = child.tagName.toLowerCase();
if(opacity != undefined)
{
var f = false, s = false;
if(tag == "fill")
{
child.opacity = opacity;
f = true;
}
if(tag == "stroke")
{
child.opacity = opacity;
s = true;
}
if(f && s) return opacity;
}
else
if((tag == "fill") || (tag == "stroke"))
return child.opacity;
}
}
return 1;
}
Cons
This approach has drawbacks, of course (which one doesn't?). At first, a source image-map must be of a quite high resolution. You have to craft your image-map carefully or get crappy-looking pop-ups (by the way, it is a nice time to hire a man to do it for you). Secondly, not all browsers currently are SVG-friendly. The Apple Safari browser still has problems with inline SVG as of July 2007.
Coda
That is all. I hope this article and this code will help you create more accessible and/or more interactive pages. Feel free to e-mail me if you have any problems. Have fun!
History
- July 31th, 2007 - Initial release