Click here to Skip to main content
Click here to Skip to main content

Lightmapper/v

, 9 Aug 2007
Rate this:
Please Sign up or sign in to vote.
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>
     <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>
     <head>
    
      ...
    
      <script type="text/javascript">
    
       function prepare()
       {
       }
    
       </script>
     </head>
    
    <body onload="prepare()">
  5. 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,     // 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:"
    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

License

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

About the Author

Dmitry Khudorozhkov
Software Developer Freelance software engineer
Russian Federation Russian Federation
Dmitry Khudorozhkov began programming (and gaming) with his ZX Spectrum in 1989. Having seen and used all IBM PCs from early XT to the latest x64 machines, now Dmitry is a freelance programmer, living in Moscow, Russia. He is a graduate of the Moscow State Institute of Electronics and Mathematics (Applied Mathematics).
 
He is proficient in:
 
- C/C++ - more that 9 years of experience. Pure Win32 API/MFC desktop programming, networking (BSD/Win sockets), databases (primarily SQLite), OpenGL;
 
- JavaScript - more that 6 years of experience. Client-side components, AJAX, jQuery installation and customization;
 
- Firefox extensions (immediatelly ready for addons.mozilla.org reviewing) and Greasemonkey scripts. As an example of extensions Dmitry made you can search for FoxyPrices or WhatBird Winged Toolbar;
 
- XML and it's applications (last 2 years): XSLT (+ XPath), XSD, SVG, VML;
 
- ASP.NET/C# (webservices mostly);
 
Also familiar with (= entry level):
 
- PHP;
 
- HTML/CSS slicing.
 
Trying to learn:
 
- Ruby/Ruby-on-Rails;
 
- Czech language.
 
If you wish to express your opinion, ask a question or report a bug, feel free to e-mail:dmitrykhudorozhkov@yahoo.com. Job offers are warmly welcome.
 
If you wish to donate - and, by doing so, support further development - you can send Dmitry a bonus through the Rentacoder.com service (registration is free, Paypal is supported). Russian users can donate to the Yandex.Money account 41001132298694.
 
-

Comments and Discussions

 
QuestionHi ! help plz Pinmembersamih010122-Oct-09 7:39 
AnswerRe: Hi ! help plz Pinmemberjulianincorvaia24-Jun-10 9:27 
GeneralAdept code for variable value instead of mouseover Pinmemberroylamers7-Dec-07 5:52 
General Vote for this article in a CodeProject Monthly Survey, please! PinmemberDmitry Khudorozhkov14-Sep-07 17:29 
GeneralJavascript Map PinmemberAlex.Bykovsky15-Aug-07 11:52 
AnswerRe: Javascript Map PinmemberMartin Nemitz15-Aug-07 21:27 
AnswerRe: Javascript Map [modified] PinmemberDmitry Khudorozhkov15-Aug-07 22:07 
GeneralRe: Javascript Map PinmemberDmitry Khudorozhkov15-Aug-07 22:06 
GeneralExcellent PinmemberSacha Barber9-Aug-07 20:45 
GeneralRe: Excellent PinmemberDmitry Khudorozhkov9-Aug-07 22:03 
GeneralRe: Excellent PinmemberSacha Barber9-Aug-07 22:21 
GeneralMore. PinmemberDeepak.Kumar9-Aug-07 17:32 
GeneralRe: More. PinmemberDmitry Khudorozhkov9-Aug-07 17:55 
GeneralGreat work... PinmemberPaul Selormey9-Aug-07 15:09 
GeneralRe: Great work... [modified] PinmemberDmitry Khudorozhkov9-Aug-07 16:55 
GeneralRe: Great work... PinmemberPaul Selormey9-Aug-07 18:07 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web03 | 2.8.140709.1 | Last Updated 9 Aug 2007
Article Copyright 2007 by Dmitry Khudorozhkov
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid