Summary
This article includes the full source code for both an Angular 8 Fabric.js Image Editor and a plain JavaScript HTML5 ImageMap Editor for laptops. I created these apps that allow 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.
The Angular 8 version is designed for use on mobile phones where you might want to layout objects inside a room.
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.
I recently added a separate Angular 8 Mobile version of this HTML5 ImageMap Editor and the source code for that is available for download above. To instal the Angular version just download and unzip the file and open the code in VS Code and run in terminal:
npm install
The most important thing about the Angular 8 version is how we add Fabric.js to our Angular Mobile App as follows:
npm i fabric
npm i @types/fabric
Then in our component that will display our Fabric canvas we add:
import { fabric } from 'fabric';
declare var canvas: any;
this.canvas = new fabric.Canvas('swipeCanvas', {
hoverCursor: 'hand',
selection: true,
backgroundColor: '#F0F8FF',
selectionBorderColor: 'blue',
defaultCursor: 'hand'
});
In Angular 8 there is a major change in how we get a reference of application view. Since Angular 8 ViewChild and ContentChild decorators must now have a new option called static that is applied as follows:.
If you add static: true on a dynamic element (wrapped in a condition or a loop), then it will not be accessible in ngOnInit nor in ngAfterViewInit.
@ViewChild('swipeCanvas', {static: true}) swipeCanvas: ElementRef;
OR, setting it to static: false should behave as you're expecting it to and query results available in ngAfterViewInit.
@ViewChild('swipeCanvas', {static: false}) swipeCanvas: ElementRef;
And it is thats imple to add a Fabric canvas in Angular 8.
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";>
<map name="mapid" id="mapid">
<% for(var i=0; i<areas.length; i++) { var a=areas[i]; %><area shape="<%= a.shape %>"
<%= "data-"+mapKey %>="<%= a.mapValue %;>" coords="<%= a.coords %>" href="<%= a.link %>" alt="<%= a.alt %>" />
<% } %></map>
</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":
- Show Image Map Html (Uses underscore template "map_template")
- Show Objects Custom Data (Uses underscore template "map_data")
- Show Objects JSON Data (Uses JSON.stringify(canvas) ... saved with no background)
- Save JSON Local Storage (Uses JSON.stringify(canvas) ... saved local storage with no background)
- 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);
}
var objects = canvas.getObjects();
canvas.forEachObject(function(object){
object.mapKey = mapKey;
});
canvas.renderAll();
canvas.calcOffset()
clearNodes();
var areas = [];
_.each(objects, function (a) {
var 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){
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;
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 = 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":
$('#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;
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();
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.
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'
});
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) {
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.
function zoomIn() {
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:
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;
context.clearRect(0, 0, mapArea.owner.scaleInfo.width, mapArea.owner.scaleInfo.height);
var pattern = context.createPattern(image, 'repeat');
context.fillStyle = pattern;
context.fillRect(0, 0, mapArea.owner.scaleInfo.width, mapArea.owner.scaleInfo.height);
};
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!