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

Image Map Editor, Fabric vs. ImageMapster, and Virtual Designers

, 15 May 2013 CPOL
Rate this:
Please Sign up or sign in to vote.
How to Use the Fabric JQuery Library to Create Image Maps

ImageMapEditor

Summary

This article includes the full source code for the HTML5 ImageMap Editor I created that allows you to create an image map from an existing image that can easily be used with the JQuery plugin ImageMapster. In addition, you can also create a Fabric canvas that functions exactly like an image map but with far more features than any image map. I will be updating the source code from time to time with new web tools and features and you can download the updated source code (free) from my website.

Introduction

I recently had a client who wanted me to create an HTML5 Virtual Home Designer website with images of homes that users could "color in" like in those crayon coloring books where you have an image with outlines of parts of the image and you paint within the outlines.  But in this case of painting parts of a home like the roof or stonefront you would also want to fill in an outlined areas with patterns where ecah pattern can be different colors. The obvious choice initially was to use image maps of a houses where the user could select different colors and patterns for each area of the image map of a home like the roof, gables, siding, etc. And the obvious choice was to use the popular JQuery plugin for image maps, i.e., Imagemapster. See https://github.com/jamietre/imagemapster

But I still needed a way to create the html <map> coordinates for the image maps of the houses where the syntax would work with the ImageMapster plugin. I didn't like any of the image map editors like Adobe Dreamweaver's Hot Spot Drawing Tools or any of the other editors because they didn't really meet my needs either. So I decided to write my own Image Map Editor which is the editor included in the article.

To create my Image Map Editor I decided to use Fabric.js, a powerful, open-source JavaScript library by Juriy Zaytsev, aka "kangax," with many other contributors over the years. It is licensed under the MIT license. The "all.js" file in the sample project is the actual "Fabric.js" library. Fabric seemed like a logical choice to build an Image Map Editor because you can easily create and populate objects on a canvas like simple geometrical shapes — rectangles, circles, ellipses, polygons, or more complex shapes consisting of hundreds or thousands of simple paths. You can then scale, move, and rotate these objects with the mouse; modify their properties — color, transparency, z-index, etc. It also includes a SVG-to-canvas parser.

Background

In addition to Fabric I wanted a simple toolbar for my controls so I included the Bootstrap library for my toolbar, buttons, and dropdowns. Some of the libraries I used include:   

  • Fabric.js Library. This library is found in the "all.js" file in this project. See  Fabric.js
  • Underscore. A library with with about 80-odd utility functions. See underscore
  • Bootstrap. Used to create a nice looking toolbar. See Bootstrap
  • MiniColors. Cooler-looking color picker than the bootsrap color picker. See MiniColors
  • ScrollMenu. To squeeze a lot of pattern images into a dropdown I wrote a plugin, "jquery.scrollmenu.js," that allows you to scroll a dropdown up and down.
  • imagemapster. Although not used in this project this editor generates the html <map> coordinates for the ImageMapster plugin. 

 Image Maps, Area Groupings, and Metadata Options

In HTML and XHTML, an image map is a list of coordinates relating to a specific image, created in order to hyperlink areas of the image to various destinations (as opposed to a normal image link, in which the entire area of the image links to a single destination). For example, a map of the world may have each country hyperlinked to further information about that country. The intention of an image map is to provide an easy way of linking various parts of an image without dividing the image into separate image files. 

For working with the ImageMapster plugin we have the following attributes:

mapKey:  An attribute identifying each imagemap area. This refers to an attribute on the area tags that will be used to group them logically. Any areas containing the same mapKey will be considered part of a group, and rendered together when any of these areas is activated. You can specify more than one value in the mapKey attribute, separated by commas. This will cause an area to be a member of more than one group. The area may have different options in the context of each group. When the area is physically moused over, the first key listed will identify the group that's effective for that action. ImageMapster will work with any attribute you identify as a key. To maintain HTML compliance, I appended "data-" to the front of whatever value you assign to mapKey when I generate the html for the <map> coordinates, i.e., "data-mapkey." Doing this makes names legal as HTML5 document types. For example, you could set mapValue: 'statename' to an image map of the united states, and add an attribute to your areas that provided the full name of each state, e.g. data-statename="Alaska", or in the case of image maps of homes you might have, e.g. data-home1=mapValue, where the mapValue might equal = "roof", or "siding", etc. for a house or "state" for a map.

mapValue:  An area name or id to reference that given area of a map. For example, the following code defines a rectangular area (9,372,66,397) that is part of the "roof" of a house:  

 //mapKey = "home1", mapValue = "roof" and <span class="style2">data-home1="roof"</span>
<img src="someimage.png" alt="image alternative text" usemap="#mapname" />
<map name="mapname">
   <area shape="rect" <span class="style2">data-home1="roof"</span> coords="9,372,66,397" href="#" alt="" title="hover text" />
</map>

 Creating The HTML <map></map> Code

The purpose of this editor is to create the html for an image map when the user selects "Show Image Map Html." To do this I decided to use the underscore library that allowed me to easily create the syntax for the html <map></map>. Keep in mind that our goal here is to create the html code that we can copy and paste into our website that will work with the ImageMapster plugin. First I created a template, i.e., "map_template," for the format of the <map></map> html using underscore as follows: 

<script type="text/underscoreTemplate" id="map_template";>
&lt;map name="mapid" id="mapid"&gt;
<% for(var i=0; i<areas.length; i++) { var a=areas[i]; %>&lt;area shape="<%= a.shape %>" 
<%= "data-"+mapKey %>="<%= a.mapValue %;>" coords="<%= a.coords %>" href="<%= a.link %>" alt="<%= a.alt %>" /&gt;
<% } %>&lt;/map&gt;
</script>

Serializing the Fabric Canvas

I added the following methods to this editor but the ONLY method you need to create an image map is "Show Image Map Html": 

1) Show Image Map Html  (Uses underscore template "map_template")
2) Show Objects Custom Data  (Uses underscore template "map_data")
3) Show Objects JSON Data (Uses JSON.stringify(canvas) ... saved with no background)
4) Save JSON Local Storage (Uses JSON.stringify(canvas) ... saved local storage with no background)
5) Load JSON Local Storage (Uses loadCanvasFromJSONString(s) ... load from local storage)

Let's look at two ways of serializing the fabric canvas. The first is to use underscore and to script a custom data template that you load with the properties of teh canvas elements. The second way is to use JSON.stringify(camvas). Let's first look at how we would use underscore. Below is an example of a template to store the properties using underscore. 

<script type="text/underscoreTemplate" id="map_data">
[<% for(var i=0; i<areas.length; i++) { var a=areas[i]; %>
{
mapKey: "<%= mapKey %>",
mapValue: "<%= a.mapValue %>",
type: "<%= a.shape %>",
link: "<%= a.link %>",
alt: "<%= a.alt %>",
perPixelTargetFind: <%= a.perPixelTargetFind %>,
selectable: <%= a.selectable %>,
hasControls: <%= a.hasControls %>,
lockMovementX: <%= a.lockMovementX %>,
lockMovementY: <%= a.lockMovementY %>,
lockScaling: <%= a.lockScaling %>,
lockRotation: <%= a.lockRotation %>,
hasRotatingPoint: <%= a.hasRotatingPoint %>,
hasBorders: <%= a.hasBorders %>,
overlayFill: null,
stroke: "<#000000>",
strokeWidth: 1,
transparentCorners: true,
borderColor: "<black>",
cornerColor: "<black>",
cornerSize: 12,
transparentCorners: true,
pattern: "<%= a.pattern %>",
<% if ( (a.pattern) != "" ) { %>fill: "#00ff00",<% } else { 
%>fill: "<%= a.fill %>",<% } %> opacity: <%= a.opacity %>,
top: <%= a.top %>, left: <%= a.left %>, scaleX: <%= a.scaleX %>,
scaleY: <%= a.scaleY %>,
<% if ( (a.shape) == "circle" ) { %>radius: <%= a.radius %>,<% } 
%><% if ( (a.shape) == "ellipse" ) { %>width: <%= a.width %>,
height: <%= a.height %>,<% } 
%><% if ( (a.shape) == "rect" ) { %>width: <%= a.width %>,,
height: <%= a.height %>,<% } 
%><% if ( (a.shape) == "polygon" ) { %>points: [<% for(var j=0; j<a.coords.length-1; j = j+2) {  
var checker = j % 6; %> <% if ( (checker) == 0 ) { 
%>{x: <%= (a.coords[j] - a.left)/a.scaleX %>, y: <%= (a.coords[j+1] - a.top)/a.scaleY %>}, <% } 
else { %>{x: <%= (a.coords[j] - a.left)/a.scaleX %>, y: <%= (a.coords[j+1] - a.top)/a.scaleY %>}, <% }
 } %>]<% } %>},<% } %>
]
</script>

In order to load the underscore Template above we create an array using the corresponding values from the fabric elements as shown below. Please keep in mind that I hard-coded some properties to suit my own needs for the website I was building and you can modify this to suit your own needs. 

    function createObjectsArray(t) {
        fabric.Object.NUM_FRACTION_DIGITS = 10;
        mapKey = $('#txtMapKey').val();
        if ($.isEmptyObject(mapKey)) {
            mapKey = "home1";
            $('#txtMapKey').val(mapKey);
        }

        // loop through all objects & assign ONE value to mapKey
        var objects = canvas.getObjects();
        canvas.forEachObject(function(object){
            object.mapKey = mapKey;
        });
        canvas.renderAll();
        canvas.calcOffset()
        clearNodes();

        var areas = []; //note the "s" on areas!
        _.each(objects, function (a) {
            var area = {}; //note that there is NO "s" on "area"!
            area.mapKey = a.mapKey;
            area.link = a.link;
            area.alt = a.alt;
            area.perPixelTargetFind = a.perPixelTargetFind;
            area.selectable = a.selectable;
            area.hasControls = a.hasControls;
            area.lockMovementX = a.lockMovementX;
            area.lockMovementY = a.lockMovementY;
            area.lockScaling = a.lockScaling;
            area.lockRotation = a.lockRotation;
            area.hasRotatingPoint = a.hasRotatingPoint;
            area.hasBorders = a.hasBorders;
            area.overlayFill = null;
            area.stroke = '#000000';
            area.strokeWidth = 1;
            area.transparentCorners = true;
            area.borderColor = "black";
            area.cornerColor = "black";
            area.cornerSize = 12;
            area.transparentCorners = true;
            area.mapValue = a.mapValue;
            area.pattern = a.pattern;
            area.opacity = a.opacity;
            area.fill = a.fill;
            area.left = a.left;
            area.top = a.top;
            area.scaleX = a.scaleX;
            area.scaleY = a.scaleY;
            area.radius = a.radius;
            area.width = a.width;
            area.height = a.height;
            area.rx = a.rx;
            area.ry = a.ry;
            switch (a.type) {
                case "circle":
                    area.shape = a.type;
                    area.coords = [a.left, a.top, a.radius * a.scaleX];
                    break;
                case "ellipse":
                    area.shape = a.type;
                    var thisWidth = a.width * a.scaleX;
                    var thisHeight = a.height * a.scaleY;
                    area.coords = [a.left - (thisWidth / 2), a.top - (thisHeight / 2), a.left + (thisWidth / 2), a.top + (thisHeight / 2)];
                    break;
                case "rect":
                    area.shape = a.type;
                    var thisWidth = a.width * a.scaleX;
                    var thisHeight = a.height * a.scaleY;
                    area.coords = [a.left - (thisWidth / 2), a.top - (thisHeight / 2), a.left + (thisWidth / 2), a.top + (thisHeight / 2)];
                    break;
                case "polygon":
                    area.shape = a.type;
                    var coords = [];
                    _.each(a.points, function (p) {
                        newX = (p.x * a.scaleX) + a.left;
                        newY = (p.y * a.scaleY) + a.top;
                        coords.push(newX);
                        coords.push(newY);
                    });
                    area.coords = coords;
                    break;
            }
            areas.push(area);
        });

        if(t == "map_template") {
            $('#myModalLabel').html('Image Map HTML');
            $('#textareaID').html(_.template($('#map_template').html(), { areas: areas }));
            $('#myModal').on('shown', function () {
                $('#textareaID').focus();  
            });
            $("#myModal").modal({
                show: true,
                backdrop: true,
                keyboard: true
            }).css({
                "width": function () { 
                return ($(document).width() * .6) + "px";  
                },
                "margin-left": function () { 
                return -($(this).width() / 2); 
                }
            });         
        }
        if(t == "map_data") {
            $('#myModalLabel').html('Custom JSON Objects Data');
            $('#textareaID').html(_.template($('#map_data').html(), { areas: areas }));
            $('#myModal').on('shown', function () {
                $('#textareaID').focus();  
            });
            $("#myModal").modal({
                show: true,
                backdrop: true,
                keyboard: true
            }).css({
                "width": function () { 
                return ($(document).width() * .6) + "px";  
                },
                "margin-left": function () { 
                return -($(this).width() / 2); 
                }
            });  
        }
        return false;
    };

Serializing Fabric with Custom Properties 

If you want to use JSON.stringify(canvas) then you need to do some extra work. The most important thing to understand about building image maps is that you need accuracy up to 10 decimal places or your image maps will not align properly, especially in the case of polygons. When you use underscore this issue doesn't come because you read the position and point properties accuratly to the required number of decimal places. But JSON.stringify(canvas) rounds of this data to 2 decimal places which results in dramatic misalignment in image maps. I realized this problem early on which is why I initially used the template approach for accuracy. Then in a post, Stefan Kienzle, was kind enough to point out that Fabric has a solution for this issue in that you can set the number of decimal places in a fabric canvas as follows:
    fabric.Object.NUM_FRACTION_DIGITS = 10;

This solved one of the problems with using JSON.stringify(canvas).  Another issue is that you need to include a few custom properties for image maps and other properties not normally serialized by "stringfy."  For example, you will need to add a few extra properties to all fabric object types that we use in image maps and add code that will include the serialization of these custom properties. In fabric to add properties we can either subclass an existing element type or we can extend a generic fabric element's "toObject" method. It would be crazy to subclass just one type of element since our custom properties need to apply to any type of element. Instead, we can just extended a generic fabric element's "toObject" method for the additional properties like: mapKey, link, alt, mapValue, and pattern for our image maps and fabric properties lockMovementX, lockMovementY, lockScaling, and lockRotation as shown below. 

    canvas.forEachObject(function(object){
        // Bill SerGio - We add custom properties we need for image maps here to fabric 
        // Below we extend a fabric element's toObject method with additional properties
        // In addition, JSON doesn't store several of the Fabric properties !!!
        object.toObject = (function(toObject) {
            return function() {
            return fabric.util.object.extend(toObject.call(this), {
                mapKey: this.mapKey,
                link: this.link,
                alt: this.alt,
                mapValue: this.mapValue,
                pattern: this.pattern,
                lockMovementX: this.lockMovementX,
                lockMovementY: this.lockMovementY,
                lockScaling: this.lockScaling,
                lockRotation: this.lockRotation
            });
            };
        })(object.toObject);
        ...
    });
    canvas.renderAll();
    canvas.calcOffset();

Fabric Background Image

We create the image map by drawing our sections on top of an existing image, a "background" image that we make the background of our canvas. For my own purposes in the editor I do not serialize this background image. In fact, for my own purposes I remove the background image prior to serialization and add it back after serialization so that it is not part of the serialed data. You can change this to suit your own preferences. One of the reasons I did this is because the fullpath of the background image is serialized and unless you are restoring with the same path you will have an issue. I need a relative path for my own purposes. The "background" image is added as follows.
    canvas.setBackgroundImage(backgroundImage, canvas.renderAll.bind(canvas));

Bootstrap's Navbar 

I wanted to place all of the controls in a single row to allow as much space as possible for editing. I decided to use the bootstrap library and the bootstratp "navbar" for controls for a clean look as shown below.



NavBar Features from left to right:

  • Save. This dropdown includes several Save & Restore options.
  • Circle. Adds a fabric circle element to the canvas.
  • Elipsis. Adds a fabric elipse element to the canvas.
  • Rectangle. Adds a fabric rectangle element to the canvas with EQUAL sides (i.e., a square).
  • Polygon. The "polygon" icon when clicked adds a fabric "open" ploygon element to the canvas.
    Each click on the canvas adds a new node to the open polygon. To close the polygon simply click
    the "Close Polygon" symbol (not shown here) that will appear only while the polygon is open.
  • Text. The "letter" icon when clicked adds a fabric text element to the canvas.
  • Tools. The "tools" icon when clicked displays dropdown of utilities like Copy, Paste, Delete, Erase, Z-Order, Select All Objects, Lock All Objects, etc.
  • Properties. The "properties" icon when clicked displays list of properties of selected fabric element.
  • Animate. The "target" icon illustrates some typical fabric animations.
  • Opacity. The "checked" icon when clicked changes the opacity of the selected fabric element.
  • Color Selector. Allows you to change color of selected fabric element. I am NOT using bootstrap's color selector!
  • Zoom. The "magnify" icon when clicked displays the controls for zooming in and out.
  • Areas. Displays list of map areas, i.e., fabric element mapValues which are the areas for ImageMapster.
  • MapValues DropDown. This dropdown displays a list of the current mapValues of all of the elements in the canvas.
    Remember that the DataKey in ImageMapster is the mapValue with "data-" in front of it for HTML5 compliance.
  • Refresh. This icon when clicked builds the MapValues DropDown by reading the values for mapValue property of the elements in the canvas.
    This is the value of the MapKey Id used in ImageMapster's plugin for image maps.
  • Patterns. The "patterns" dropdown displays a scrolling list of image patterns. I wrote a plugin, i.e. jquery.scrollmenu.js, to make it easy to display
    a long list of items in a menu by scrolling them. Patterns are applied to ALL element mapValues that match the selected mapValue in the MapValues DropDown.

When I later added zoom I also changed the NavBar controls to stay at the top of the page so that I could still click on an item in the NavBar when I was adding the nodes for a polygon and the page was scrooled down. To accomplish I used Bootstrap’s class ‘navbar-fixed-top’ as follows: 

<nav class="navbar navbar-fixed-top">
   <div class="navbar-inner">
   ... etc.

Manipulating Fabric Canvas Elements for Image Maps

Please keep in mind that this editor is not meant to be a general purpose editor or a drawing program. I created it to do one thing which is to create the html for an image map. The toolbar includes all the basic geometric shapes in a standard image map including circle, ellipse, rectangle, and polygon. I added text just as a demo but text is not part of a standard image map. The reader is free to add other Fabric shapes and options. 

We listen for the mousedown event on the Fabric canvas as follows: 

     var activeFigure;
    var activeNodes;
    canvas.observe('mouse:down', function (e) {
        if (!e.target) {
            add(e.e.layerX, e.e.layerY);
        } else {
            if (_.detect(shapes, function (a) { return _.isEqual(a, e.target) })) {
                if (!_.isEqual(activeFigure, e.target)) {
                    clearNodes();
                }
                activeFigure = e.target;
                if (activeFigure.type == "polygon") {
                    addNodes();
                }
                $('#hrefBox').val(activeFigure.link);
                $('#titleBox').val(activeFigure.title);
                $('#groupsBox').val(activeFigure.groups);
            }
        }
    });
When a user clicks on the circle on the toolbar it sets the activeFigure equal to the figure type of the object to be added such as "circle" or "polygon." Where we position these objects initially on our canvas isn't important because we will be moving and re-shaping them to exactly match the areas of our image map. Then when the user clicks on the canvas, the selected figure type of object is added to the canvas using the following. Please keep in mind that I created this editor to meet my own immediate needs in creating image maps. You can easily customize the features of this editor to meet meet your own needs or preferences.
    function add(left, top) {
        if (currentColor.length < 2)
        {
            currentColor = '#fff';
        }

        if ((window.figureType === undefined) || (window.figureType == "text"))
            return false;

        var x = (window.pageXOffset !== undefined) ? window.pageXOffset : (document.documentElement || document.body.parentNode || document.body).scrollLeft;
        var y = (window.pageYOffset !== undefined) ? window.pageYOffset : (document.documentElement || document.body.parentNode || document.body).scrollTop;

        //stroke: String, when 'true', an object is rendered via stroke and this property specifies its color
        //strokeWidth: Number, width of a stroke used to render this object

        if (figureType.length > 0) {
            var obj = {
                left: left,
                top: top,
                fill: ' ' + currentColor,
                opacity: 1.0,
                fontFamily: 'Impact', 
                stroke: '#000000', 
                strokeWidth: 1,
                textAlign: 'right'
            };

            var objText = {
                left: left,
                top: top,
                fontFamily: 'Impact', 
                strokeStyle: '#c3bfbf', 
                strokeWidth: 3,
                textAlign: 'right'  
            };

            var shape;
            switch (figureType) {
                case "text":
                    //var text = document.getElementById("txtAddText").value;
                    var text = gText;
                    shape = new fabric.Text ( text ,  obj);
                    shape.scaleX = shape.scaleY = canvasScale;
                    shape.lockUniScaling = true;
                    shape.hasRotatingPoint = true;
                    break;
                case "square":
                    obj.width = 50;
                    obj.height = 50;
                    shape = new fabric.Rect(obj);
                    shape.scaleX = shape.scaleY = canvasScale;
                    shape.lockUniScaling = false;
                    break;
                case "circle":
                    obj.radius = 50;
                    shape = new fabric.Circle(obj);
                    shape.scaleX = shape.scaleY = canvasScale;
                    shape.lockUniScaling = true;
                    break;
                case "ellipse":
                    obj.width = 100;
                    obj.height = 50;
                    obj.rx = 100;
                    obj.ry = 50;
                    shape = new fabric.Ellipse(obj);
                    shape.scaleX = shape.scaleY = canvasScale;
                    shape.lockUniScaling = false;
                    break;
                case "polygon":
                    //$('#btnPolygonClose').show();
                    $('#closepolygon').show();

                    obj.selectable = false;
                    if (!currentPoly) {
                        shape = new fabric.Polygon([{ x: 0, y: 0}], obj);
                        shape.scaleX = shape.scaleY = canvasScale;
                        lastPoints = [{ x: 0, y: 0}];
                        lastPos = { left: left, top: top };
                    } else {
                        obj.left = lastPos.left;
                        obj.top = lastPos.top;
                        obj.fill = currentPoly.fill;
                        // while we are still adding nodes let's make the element 
                        // semi-transparent so we can see the canvas background
                        // we will reset opacity when we close the nodes
                        obj.opacity = .4;
                        currentPoly.points.push({x: left-lastPos.left, y: top-lastPos.top });
                        shapes = _.without(shapes, currentPoly);
                        lastPoints.push({ x: left - lastPos.left, y: top-lastPos.top })
                        shape = repositionPointsPolygon(lastPoints, obj);
                        canvas.remove(currentPoly);
                    }
                    currentPoly = shape;
                    break;
            }

            shape.link = $('#hrefBox').val();
            shape.alt = $('#txtAltValue').val();
            mapKey = $('#txtMapKey').val();
            shape.mapValue = $('#txtMapValue').val();

            // Bill SerGio - We add custom properties we need for image maps here to fabric 
            // Below we extend a fabric element's toObject method with additional properties
            // In addition, JSON doesn't store several of the Fabric properties !!!
            shape.toObject = (function(toObject) {
                return function() {
                return fabric.util.object.extend(toObject.call(this), {
                    mapKey: this.mapKey,
                    link: this.link,
                    alt: this.alt,
                    mapValue: this.mapValue,
                    pattern: this.pattern,
                    lockMovementX: this.lockMovementX,
                    lockMovementY: this.lockMovementY,
                    lockScaling: this.lockScaling,
                    lockRotation: this.lockRotation
                });
                };
            })(shape.toObject);
            shape.mapKey = mapKey;
            shape.link = '#';
            shape.alt = '';
            shape.mapValue = '';
            shape.pattern = '';
            lockMovementX = false;
            lockMovementY = false;
            lockScaling = false;
            lockRotation = false;
            canvas.add(shape);
            shapes.push(shape);
            if (figureType != "polygon") {
                figureType = "";
            }
        } else {
            deselect();
        }
    }

Applying Patterns to Canvas Elements

Many virtual design websites need to apply not just a color to a map area but need to also apply a pattern and color. Below are the two methods I created to apply patterns to fabric elements in my fabric canvas. 

     // "title" is the mapValue & "img" is the short path for the pattern image
    function SetMapSectionPattern(title, img) {
        canvas.forEachObject(function(object){
            if(object.mapValue == title){
                loadPattern(object, img);
            }
        });
        canvas.renderAll();
        canvas.calcOffset()
        clearNodes();
    }

    function loadPattern(obj, url) {
        obj.pattern = url;
        var tempX = obj.scaleX;
        var tempY = obj.scaleY;
        var zfactor = (100 / obj.scaleX) * canvasScale;

        fabric.Image.fromURL(url, function(img) { 
            img.scaleToWidth(zfactor).set({
                originX: 'left',
                originY: 'top'
            });

            // You can apply regualr or custom image filters at this point 
            //img.filters.push(new fabric.Image.filters.Sepia(), 
            //new fabric.Image.filters.Brightness({ brightness: 100 }));
            //img.applyFilters(canvas.renderAll.bind(canvas));
            //img.filters.push(new fabric.Image.filters.Redify(), 
            //new fabric.Image.filters.Brightness({ brightness: 100 }));
            //img.applyFilters(canvas.renderAll.bind(canvas));

            var patternSourceCanvas = new fabric.StaticCanvas();
            patternSourceCanvas.add(img);

            var pattern = new fabric.Pattern({
            source: function() {
                patternSourceCanvas.setDimensions({
                    width: img.getWidth(),
                    height: img.getHeight()
                });
                return patternSourceCanvas.getElement();
                },
                repeat: 'repeat'
            });
            fabric.util.loadImage(url, function(img) {
                // you can customize what properties get applied at this point
                obj.fill = pattern;
                canvas.renderAll();
            });
        });
    }

Sliding Patterns Drop Down

Since there are in any virtual designer a lot of possible pattern images I added a slider to the drop down for the patterns in the editor. In order to apply a pattern to a  section, i.e., "mapValue," of the objects in the canvas you first need to click the refresh symbol on the toolbar that will load the existing mapValues in the canvas into the drop down on the left of the refresh symbol as  show below. Then select a mapVlue from the mapValues drop down. Next you can select a pattern from the patterns drop down and it will  be applied to all the objects with the mapValue you selected. I created a short video to illustrate this on YouTube.

         

Adding Zoom Was A Must But It Created Some New Issues!

As soon as I began using my image map editor I quickly realized that I would have to add zoom. My image map had some really tiny areas where I need to create polygons so I added the ability to zoom in on the map as follows. 

     // Zoom In
    function zoomIn() {
        // limiting the canvas zoom scale 
        if (canvasScale < 4.9) {
            canvasScale = canvasScale * SCALE_FACTOR;

            canvas.setHeight(canvas.getHeight() * SCALE_FACTOR);
            canvas.setWidth(canvas.getWidth() * SCALE_FACTOR);

            var objects = canvas.getObjects();
            for (var i in objects) {
                var scaleX = objects[i].scaleX;
                var scaleY = objects[i].scaleY;
                var left = objects[i].left;
                var top = objects[i].top;

                var tempScaleX = scaleX * SCALE_FACTOR;
                var tempScaleY = scaleY * SCALE_FACTOR;
                var tempLeft = left * SCALE_FACTOR;
                var tempTop = top * SCALE_FACTOR;

                objects[i].scaleX = tempScaleX;
                objects[i].scaleY = tempScaleY;
                objects[i].left = tempLeft;
                objects[i].top = tempTop;

                objects[i].setCoords();
            }
            canvas.renderAll();
            canvas.calcOffset();
        }
    }

I quickly noticed that when I was zoomed in on the canvas and the page was scrolled down and I clicked on a nav button that the window would scroll up to the top and I would have to manually scroll down again to the area I was working on. There are several ways to fix this but I decided to use on the button links in the toolbar the following simple solution that prevents a click on the link from scrolling the browser window up to the navbar.

href="javascript:void(0)" 

The next issue I ran into was that of the zoom factor or scaleX and scaleY of the fabric objects created. If all the fabric objects added to the canvas have scaleX = 1.0 and scaleY = 1.0 then work nicely. But if you are zoomed in and add an object then these scale values aren't 1 and things get a bit trixky when saving and restoring the map. I finally figured out that the best thing was to make sure that the whole canvas is zoomed down to it's normal setting of 1:1. Why? Because when we restore a svaed map we are always restoring the saved objects to a canvas scaled at 1:1. 

I Have An Epiphany - Fabric is Better Than An Image Map!

When I started to wrote this image map editor I had only used Fabric to create the editor so I could create an image map for ImageMapster plugin. Then, somewhere during the process of writing this editor I had an epiphany! It dawned on me that using the Fabric canvas as an "image map" was far superior to using a standard image map! In other words, I could take an image and divide it up into sections, i.e., "mapValues", and color those sections, add patterns to those sections or animate those sections to create a kind of super image map. So feel free to use and customize this editor to create standard image maps or to create fabric "image maps" that have a lot more features than the standard image map.

Using the Code 

There are two ways to use this editor, namely, to create the <map> html for use with ImageMapster, or to create a fabric canvas that works exactly like an image map but has many more features. One of the things to be aware of if you use ImageMapster is that ImageMapster's "p.addAltImage = function (context, image, mapArea, options) {" function was not really written to work with the idea of using small images to fill large areas by applying a "pattern" to a section of an image map. So, as a heads up, I want to point out that you will need to either modify Imagemapster's "p.addAltImage" or add a new function to ImageMapster's plugin similar to the following in order to accomplish this as follows: 

    // Add a function like this to the ImageMapster plugin to apply "patterns" to map sections
    p.addPatternImage = function (context, image, mapArea, options) {
        context.beginPath();
        this.renderShape(context, mapArea);
        context.closePath();
        context.save();
        context.clip();
        context.globalAlpha = options.altImageOpacity || options.fillOpacity;
        //you can replace the line below with one that positions a smaller pattern reactangular exactly over map area to save memory
        context.clearRect(0, 0, mapArea.owner.scaleInfo.width, mapArea.owner.scaleInfo.height);    // Clear the last image if it exists.
        var pattern = context.createPattern(image, 'repeat'); // Get the direction from the button.    
        context.fillStyle = pattern;                          // Assign pattern as a fill style.      
        context.fillRect(0, 0, mapArea.owner.scaleInfo.width, mapArea.owner.scaleInfo.height);     // Fill the canvas.   
    };

Map2JSON

I also added a file, i.e., map2json.htm, to the project with a sample image map and the code to convert an existing image map into a fabric canvas with corresponding image map elements for editing. You will have to tweek the code to change the variable names to match your own though.

Points of Interest

As I said earlier, I had an epiphany when I realized that I can use a Fabric canvas to replace the old image map but this editor will do the job for either.  In addition, as mentioned above, using "javascript:void(0)" instead of "#" prevents scrolling which clicking on the navbar was a really useful tip I found on the web.
I used VisualStudio as my web editor but the editor itself is just an ordinary "html" file, i.e., "ImageMapEditor.htm," that you can just double click on and run in any web browser to use it.

Chrome Frame Plugin. I recommend installing the Chrome Frame Plugin:  The advantage of using the Chrome Frame plugin is that, once it's installed, Internet Explorer will have the support for the latest HTML, JavaScript and CSS standard features that older versions of IE don't support. This plugin has an added benefit for web developers, which is that it allows them to code applications with modern web features without leaving the IE users behind. Just think the amount of time that a web developer saves without having to code hacks and workarounds for IE.

Conclusion 

You can decide for yourself  which is better, Imagemapster and a standard image map, OR using a fabric canvas with fabric objects that adds many more cool features. Of course, it depends on your needs and what the clients wants! At least this editor will allow you to create both of these and test them against each other. Enjoy!

License

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

Share

About the Author

William SerGio
Software Developer (Senior) http://www.SerGioApps.com
United States United States
I love coding and develop desktop apps (C++, C#, Java), websites, and mobile apps (iPhone, Android, Blackberry, iPad, PhoneGap/Cordova).
 
I am launching a new national TV series that features the best mobile apps. Show me your apps!
 
I have written software for Microsoft, MySpace.com, Quicken (Intuit), Mellon Bank, U.S. Army, U.S. Navy, Franklin Templeton, Pepsi, Universal Studios, Ryder Systems, AVID, Media 100, etc.
 
Bill SerGio
http://www.SerGioApps.com
http://www.Software-rus.com

Comments and Discussions

 
QuestionWhen canvas height is larger than the window, editing (mouse down) causes page to scroll up to canvas top PinmemberBlogo Blog14-Nov-14 5:37 
AnswerRe: When canvas height is larger than the window, editing (mouse down) causes page to scroll up to canvas top PinmemberWilliam SerGio14-Nov-14 6:09 
GeneralRe: When canvas height is larger than the window, editing (mouse down) causes page to scroll up to canvas top PinmemberBlogo Blog14-Nov-14 17:57 
QuestionMVC Pinmembererrrerwwed20-Feb-14 4:51 
AnswerRe: MVC PinmemberWilliam SerGio20-Feb-14 4:59 
AnswerRe: MVC PinmemberWilliam SerGio14-Nov-14 6:11 
GeneralMy vote of 5 Pinmemberasugix4-Jun-13 14:13 
GeneralMy vote of 5 Pinmemberibrahim_ragab15-May-13 13:12 
GeneralMy vote of 5 PinprofessionalPrasad Khandekar15-May-13 1:55 

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 | Terms of Use | Mobile
Web04 | 2.8.141220.1 | Last Updated 15 May 2013
Article Copyright 2013 by William SerGio
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid