Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / Javascript

Lightmapper/v

4.99/5 (27 votes)
9 Aug 2007CPOL4 min read 2   382  
Creating interactive image-maps with in-browser vector graphics
Screenshot - lightmapperv.jpg

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

  1. 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.
  2. Create a new HTML page; embed an image and the image-map you created to this page.
  3. Include the references to lightmapperv.js and svg+vml.js in your HTML file:
    HTML
    <html>
     <head>
      <script type="text/javascript" src="svg+vml.js"></script>
      <script type="text/javascript" src="lightmapperv.js"></script>
  4. 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
    <html>
     <head>
    
      ...
    
      <script type="text/javascript">
    
       function prepare()
       {
       }
    
       </script>
     </head>
    
    <body onload="prepare()">
  5. In that function, create the LightmapperV object:
    JavaScript
    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:

JavaScript
function Lightmapper(imgId,     // id of the IMAGE element the map's bound to,
                     mouseOver, // global onMouseOver handler,
                     mouseOut,  // global onMouseOut handler
                     bindings)  // array of arrays of 
                                // area/image/position/handler bindings:
                                //
                                // [area_id, color, x_displacement, 
                                // y_displacement, on_mouse_over, 
                                // on_mouse_out, 
                                // linked_areas_ids...]
                                // where:
                                //
                                // area_id - id of the area,
                                //
                                // color - colors (in #xxx or #xxxxxx format)
                                //         that will be used to highlight the
                                //         area,
                                //
                                // x_displacement - horiz. displacement 
                                //                  required for correct 
                                //                  alignment of mouse-over 
                                //                  image and the original map
                                //                  (see note below),
                                //
                                // y_displacement - vertical displacement,
                                //
                                // on_mouse_over - onMouseOver handler for 
                                //                 current (area_id) area
                                //                (replaces the 
                                //                 global handler),
                                //
                                // on_mouse_out - onMouseOut handler for 
                                //                current (area_id) area
                                //               (replaces the global 
                                //                handler),
                                //
                                // linked_area_ids - ids, comma separated, 
                                //                   of the linked
                                //                   areas (=pop up 
                                //                   with the current area).

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:"
    JavaScript
    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:
    JavaScript
    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:
    JavaScript
    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:
    JavaScript
    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:
    JavaScript
    _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:
    JavaScript
    _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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)