65.9K
CodeProject 正在变化。 阅读更多。
Home

Angular 图像地图 & 动画编辑器 & 虚拟设计器

2013年5月14日

CPOL

18分钟阅读

viewsIcon

72659

downloadIcon

2765

如何使用 Fabric jQuery 库创建图像地图。

ImageMapEditor

摘要

本文档包含 Angular 8 Fabric.js 图像编辑器和用于笔记本电脑的纯 JavaScript HTML5 ImageMap 编辑器的完整源代码。我创建了这些应用程序,允许您从现有图像创建图像映射,这些图像映射可以轻松地与 JQuery 插件 ImageMapster 一起使用。此外,您还可以创建一个 Fabric 画布,其功能与图像映射完全相同,但功能远超任何图像映射。我会不时地更新源代码,添加新的 Web 工具和功能。

Angular 8 版本专为在手机上使用而设计,您可能希望在房间内布局对象。

引言

我最近有一个客户要求我创建一个 HTML5 虚拟家居设计网站,其中包含房屋图像,用户可以像蜡笔着色书一样“涂色”,图像上有图像部分的轮廓,您在轮廓内进行绘画。 但在这个例子中,像屋顶或石砌外墙一样涂色房屋的各个部分,您还希望用图案填充轮廓区域,每个图案可以有不同的颜色。最初,显而易见的解决方案是使用房屋的图像映射,用户可以为房屋图像映射的每个区域(如屋顶、屋顶山墙、墙板等)选择不同的颜色和图案。而显而易见的解决方案是使用流行的图像映射 JQuery 插件,即 Imagemapster。请参阅 https://github.com/jamietre/imagemapster

但我仍然需要一种方法来为房屋的图像映射创建 html <map> 坐标,其语法可以与 ImageMapster 插件一起使用。我不太喜欢 Adobe Dreamweaver 的热点绘图工具或其他任何图像映射编辑器,因为它们都不能真正满足我的需求。所以我决定编写我自己的图像映射编辑器,这就是本文档中包含的编辑器。

为了创建我的图像映射编辑器,我决定使用 Fabric.js,这是一个由 Juriy Zaytsev(又名“kangax”)创建的强大、开源的 JavaScript 库,多年来还有许多其他贡献者。它已获得 MIT 许可证。示例项目中的“all.js”文件是实际的“Fabric.js”库。Fabric 似乎是构建图像映射编辑器的合乎逻辑的选择,因为您可以轻松地在画布上创建和填充对象,例如简单的几何形状 — 矩形、圆形、椭圆形、多边形,或由数百或数千个简单路径组成的更复杂的形状。然后,您可以使用鼠标缩放、移动和旋转这些对象;修改它们的属性 — 颜色、透明度、z-index 等。它还包括一个 SVG 到画布的解析器。

我最近为此添加了一个独立的Angular 8 移动版HTML5 ImageMap 编辑器,其源代码可在上方下载。要安装 Angular 版本,只需下载并解压文件,然后在VS Code中打开代码并在终端中运行

npm install

Angular 8 版本最重要的部分是如何将 Fabric.js 添加到我们的 Angular 移动应用中,如下所示

npm i fabric
npm i @types/fabric

然后,在我们显示 Fabric 画布的组件中,我们添加

import { fabric } from 'fabric';

declare var canvas: any;

this.canvas = new fabric.Canvas('swipeCanvas', {
   hoverCursor: 'hand',
   selection: true,
   backgroundColor: '#F0F8FF', 
   selectionBorderColor: 'blue',
   defaultCursor: 'hand'
});

在 Angular 8 中,我们获取应用程序视图引用的方式发生了重大变化。自 Angular 8 起,ViewChild 和 ContentChild 装饰器现在必须有一个名为“static”的新选项,其应用方式如下:.

如果在动态元素(封装在条件或循环中)上设置 static: true,则在 ngOnInit 或 ngAfterViewInit 中都无法访问它。

   @ViewChild('swipeCanvas', {static: true}) swipeCanvas: ElementRef;

或者,将其设置为 static: false 应该会如您所预期一样工作,并且查询结果在 ngAfterViewInit 中可用。

   @ViewChild('swipeCanvas', {static: false}) swipeCanvas: ElementRef;

这样,在 Angular 8 中添加 Fabric 画布就变得如此简单。

背景

除了 Fabric,我还想要一个简单的工具栏来放置我的控件,所以我包含了 Bootstrap 库来制作我的工具栏、按钮和下拉菜单。我使用的一些库包括:  

  • Fabric.js 库。该库位于本项目中的“all.js”文件中。请参阅 Fabric.js
  • Underscore。一个包含大约 80 多个实用函数的库。请参阅 underscore
  • Bootstrap。用于创建漂亮的工具栏。请参阅 Bootstrap
  • MiniColors。比 bootstrap 颜色选择器更酷的颜色选择器。请参阅 MiniColors
  • ScrollMenu。为了将大量图案图像塞进下拉菜单,我编写了一个名为“jquery.scrollmenu.js”的插件,它允许您向上和向下滚动下拉菜单。
  • imagemapster。尽管未在此项目中使用的此编辑器会为 ImageMapster 插件生成 html <map> 坐标。 

 图像映射、区域分组和元数据选项

在 HTML 和 XHTML 中,图像映射是与特定图像相关的坐标列表,创建目的是将图像的各个区域链接到不同的目的地(与普通图像链接相反,普通图像链接将整个图像区域链接到一个目的地)。例如,世界地图可以将每个国家链接到有关该国家的进一步信息。图像映射的目的是提供一种简单的方法来链接图像的各个部分,而无需将图像分成单独的图像文件。 

为了与 ImageMapster 插件配合使用,我们有以下属性:

mapKey: 标识每个图像映射区域的属性。这指的是区域标签上的一个属性,该属性将用于在逻辑上对它们进行分组。包含相同 mapKey 的任何区域都将被视为一组的一部分,并且当其中任何一个区域被激活时将一起呈现。您可以在 mapKey 属性中指定多个值,用逗号分隔。这将导致一个区域成为多个组的成员。区域在每个组的上下文中可能具有不同的选项。当区域被物理鼠标悬停时,列出的第一个键将标识对于该操作有效的组。ImageMapster 将与您标识为键的任何属性一起工作。为了保持 HTML 合规性,在我生成 <map> 坐标的 html 时,我会在您分配给 mapKey 的任何值前面附加“data-”,即“data-mapkey”。这样做使名称成为 HTML5 文档类型的合法名称。例如,您可以将 mapValue 设置为 'statename' 到美国地图,并向您的区域添加一个属性,提供每个州的完整名称,例如 data-statename="Alaska",或者在房屋图像映射的情况下,您可能拥有,例如 data-home1=mapValue,其中 mapValue 可能等于 = "roof"、"siding" 等房屋区域,或地图的 "state"。

mapValue: 区域名称或 id,用于引用地图的给定区域。例如,以下代码定义了一个矩形区域 (9,372,66,397),它是房屋“屋顶”的一部分:  

//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>

 创建 HTML <map></map> 代码

此编辑器的目的是在用户选择“显示图像映射 HTML”时创建图像映射的 html。为此,我决定使用 underscore 库,它允许我轻松地创建 html <map></map> 的语法。请记住,我们的目标是创建我们可以复制并粘贴到我们的网站中,并且可以与 ImageMapster 插件一起使用的 html 代码。首先,我使用 underscore 为 <map></map> html 的格式创建了一个模板,即“map_template”,如下所示: 

<script type="text/underscoreTemplate" id="map_template";>
<map name="mapid" id="mapid">
<% 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;
<% } %></map>
</script>

序列化 Fabric 画布

我在编辑器中添加了以下方法,但您创建图像映射所需的唯一方法是“显示图像映射 HTML”: 

  1. 显示图像映射 HTML  (使用 underscore 模板“map_template”)
  2. 显示对象自定义数据  (使用 underscore 模板“map_data”)
  3. 显示对象 JSON 数据(使用 JSON.stringify(canvas) ... 保存时无背景)
  4. 将 JSON 保存到本地存储(使用 JSON.stringify(canvas) ... 保存到本地存储时无背景)
  5. 从本地存储加载 JSON(使用 loadCanvasFromJSONString(s) ... 从本地存储加载)

让我们来看两种序列化 fabric 画布的方法。第一种是使用 underscore 并编写自定义数据模板,该模板用画布元素的属性加载。 第二种方法是使用 JSON.stringify(camvas)。让我们首先看看如何使用 underscore。下面是一个使用 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>

为了加载上面的 underscore 模板,我们使用 fabric 元素的相应值创建一个数组,如下所示。请记住,我硬编码了一些属性以适应我正在构建的网站的需求,您可以根据自己的需求进行修改。 

    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;
    };

使用自定义属性序列化 Fabric 

如果要使用 JSON.stringify(canvas),则需要做一些额外的工作。理解构建图像映射的最重要的事情是,您需要高达 10 位小数的精度,否则您的图像映射将无法正确对齐,尤其是在多边形的情况下。当您使用 underscore 时,这个问题不会出现,因为您读取的位置和点属性的精度达到了所需的小数位数。但是 JSON.stringify(canvas) 会将此数据四舍五入到 2 位小数,这会导致图像映射出现严重的错位。我早就意识到了这个问题,这就是为什么我最初使用模板方法来确保准确性。然后,在 帖子 中,Stefan Kienzle 好心地指出,Fabric 提供了解决此问题的方法,您可以在 fabric 画布中设置小数位数,如下所示:

    fabric.Object.NUM_FRACTION_DIGITS = 10;

这解决了使用 JSON.stringify(canvas) 的一个问题。 另一个问题是,您需要包含一些用于图像映射的自定义属性以及“stringfy”通常不序列化的其他属性。 例如,您需要为我们在图像映射中使用的所有 fabric 对象类型添加一些额外的属性,并添加代码来包含这些自定义属性的序列化。在 fabric 中添加属性,我们可以子类化现有的元素类型,也可以扩展通用 fabric 元素的“toObject”方法。为了单个元素类型进行子类化就太疯狂了,因为我们的自定义属性需要适用于任何类型的元素。相反,我们可以只扩展通用 fabric 元素的“toObject”方法,以添加额外的属性,如:mapKey、link、alt、mapValue 和 pattern,用于我们的图像映射,以及 fabric 属性 lockMovementX、lockMovementY、lockScaling 和 lockRotation,如下所示。 

    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 背景图像

我们通过在我们已有的图像“背景”图像上绘制我们的各个部分来创建图像映射,我们将该图像作为画布的背景。就我个人而言,在编辑器中,我不会序列化此背景图像。事实上,就我个人而言,我在序列化之前删除背景图像,并在序列化之后将其添加回来,这样它就不会成为序列化数据的一部分。您可以根据自己的喜好更改此设置。我这样做的原因之一是,背景图像的完整路径会被序列化,除非您使用相同的路径进行恢复,否则就会出现问题。我个人需要一个相对路径。背景图像的添加方式如下。 

    canvas.setBackgroundImage(backgroundImage, canvas.renderAll.bind(canvas));

Bootstrap 的导航栏 

我想将所有控件放在一行中,以便尽可能多地用于编辑空间。我决定使用 Bootstrap 库和 Bootstrap 的“navbar”控件,以获得整洁的外观,如下所示:



导航栏从左到右的特征

  • 保存。此下拉菜单包括几个“保存和恢复”选项。
  • 圆形。将 fabric 圆形元素添加到画布。
  • 椭圆。添加 fabric 椭圆元素到画布。
  • 矩形。将 fabric 矩形元素添加到画布,边长相等(即正方形)。
  • 多边形。“多边形”图标单击时会将 fabric“开放”多边形元素添加到画布。
    每次在画布上单击都会为开放多边形添加一个新节点。要关闭多边形,只需单击
    “关闭多边形”符号(此处未显示),该符号仅在多边形打开时出现。
  • 文本。“字母”图标单击时会将 fabric 文本元素添加到画布。
  • 工具。“工具”图标单击时会显示实用程序下拉菜单,如复制、粘贴、删除、擦除、层叠顺序、全选对象、锁定所有对象等。
  • 属性。“属性”图标单击时会显示选定 fabric 元素的属性列表。
  • 动画。“目标”图标演示了一些典型的 fabric 动画。
  • 不透明度。“已选中”图标单击时会更改选定 fabric 元素的不透明度。
  • 颜色选择器。允许您更改选定 fabric 元素颜色。我没有使用 bootstrap 的颜色选择器!
  • 缩放。“放大镜”图标单击时会显示用于放大和缩小的控件。
  • 区域。显示映射区域列表,即 fabric 元素的 mapValues,它们是 ImageMapster 的区域。
  • MapValues 下拉菜单。此下拉菜单显示画布中所有元素的当前 mapValues 列表。
    请记住,ImageMapster 中的 DataKey 是 mapValue,前面带有“data-”前缀,以符合 HTML5 标准。
  • 刷新。单击此图标将通过读取画布中元素的 mapValue 属性值来构建 MapValues 下拉菜单。
    这是 ImageMapster 插件用于图像映射的 MapKey Id 的值。
  • 图案。“图案”下拉菜单显示一个可滚动的图案图像列表。我编写了一个插件,即 jquery.scrollmenu.js,以便轻松显示
    菜单中的长列表(通过滚动)。图案适用于 MapValues 下拉菜单中选定的 mapValue 的所有元素 mapValues。

后来我添加了缩放功能,我还将导航栏控件固定在页面顶部,以便在添加多边形节点时仍然可以单击导航栏中的项目,而页面已向下滚动。为了实现这一点,我使用了 Bootstrap 的“navbar-fixed-top”类,如下所示: 

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

操作 Fabric 画布元素以进行图像映射

请记住,此编辑器并非旨在作为通用编辑器或绘图程序。我创建它是为了做一件事,那就是为图像映射创建 html。工具栏包含标准图像映射中的所有基本几何形状,包括圆形、椭圆形、矩形和多边形。我添加文本只是为了演示,但文本不是标准图像映射的一部分。读者可以自由添加其他 Fabric 形状和选项。 

我们侦听 Fabric 画布上的 mousedown 事件,如下所示: 

     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);
            }
        }
    });

当用户单击工具栏上的圆形时,它会将 activeFigure 设置为要添加对象的对象类型,例如“circle”或“polygon”。这些对象最初放置在画布上的位置并不重要,因为我们将移动和重新塑形它们以精确匹配我们的图像映射的区域。然后,当用户单击画布时,将使用以下方法将选定的对象类型添加到画布。请记住,我创建此编辑器是为了满足我创建图像映射的即时需求。您可以轻松地自定义此编辑器的功能,以满足您自己的需求或偏好。

    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();
        }
    }

将图案应用于画布元素

许多虚拟设计网站不仅需要为映射区域应用颜色,还需要应用图案和颜色。以下是我创建的两种方法,用于在我的 fabric 画布中将图案应用于 fabric 元素。 

     // "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();
            });
        });
    }

滑动图案下拉菜单

由于在任何虚拟设计器中都有许多可能的图案图像,因此我在编辑器中的图案下拉菜单中添加了一个滑块。要将图案应用于画布上对象的某个部分,即“mapValue”,您首先需要单击工具栏上的刷新符号,该符号会将画布中现有的 mapValues 加载到刷新符号左侧的下拉菜单中,如下所示。然后从 mapValues 下拉菜单中选择一个 mapVlue。接下来,您可以从图案下拉菜单中选择一个图案,它将应用于具有您所选 mapValue 的所有对象。我创建了一个短视频来说明这一点,请访问 YouTube

         

添加缩放是必须的,但这带来了一些新问题!

我刚开始使用我的图像映射编辑器时,就很快意识到我必须添加缩放功能。我的图像映射有一些非常小的区域,我需要创建多边形,所以我添加了缩放地图的功能,如下所示。 

     // 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();
        }
    }

我很快注意到,当我放大画布并且页面向下滚动时,如果我点击一个导航按钮,窗口会滚动到顶部,我必须手动再次向下滚动到我正在处理的区域。有几种方法可以解决这个问题,但我决定在工具栏的按钮链接上使用以下简单解决方案,该解决方案可防止单击链接将浏览器窗口滚动到导航栏。

href="javascript:void(0)" 

我遇到的下一个问题是 fabric 对象创建的缩放因子或 scaleX 和 scaleY。如果添加到画布的所有 fabric 对象都具有 scaleX = 1.0 和 scaleY = 1.0,则它们工作得很好。但如果您已放大并添加了一个对象,那么这些比例值不是 1,在保存和恢复映射时事情会变得有点棘手。我终于弄清楚了,最好是确保整个画布被缩小到其正常的 1:1 设置。为什么?因为当我们恢复保存的映射时,我们总是在 1:1 的画布比例下恢复保存的对象。 

我有一个顿悟——Fabric 比图像映射更好!

当我开始编写这个图像映射编辑器时,我只使用了 Fabric 来创建编辑器,以便为 ImageMapster 插件创建图像映射。然后,在编写编辑器的过程中,我突然有了一个顿悟!我意识到使用 Fabric 画布作为“图像映射”远远优于使用标准图像映射!换句话说,我可以获取一张图像并将其分成多个部分,即“mapValues”,然后为这些部分着色,为这些部分添加图案,或为这些部分添加动画,从而创建一种超级图像映射。因此,请随意使用和自定义此编辑器来创建标准图像映射,或创建具有比标准图像映射更多功能的 fabric“图像映射”。

使用代码 

有两种使用此编辑器的方法,即创建用于 ImageMapster 的 <map> html,或者创建功能与图像映射完全相同但具有更多功能的 fabric 画布。如果您使用 ImageMapster,需要注意的一件事是,ImageMapster 的“p.addAltImage = function (context, image, mapArea, options) {”函数实际上并不是为了使用小图像通过将“图案”应用于图像映射的某个部分来填充大区域的想法而编写的。所以,作为一个提醒,我想指出您需要修改 ImageMapster 的“p.addAltImage”或在 ImageMapster 插件中添加一个类似于以下内容的新函数,以达到此目的: 

    // 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

我还向项目中添加了一个文件,即 map2json.htm,其中包含一个示例图像映射以及将现有图像映射转换为具有相应图像映射元素的 fabric 画布以进行编辑的代码。您需要调整代码以更改变量名以匹配您自己的变量名。

关注点

正如我之前所说,当我意识到我可以使用 Fabric 画布来替换旧的图像映射时,我有一个顿悟,但这个编辑器可以同时满足这两种需求。 此外,如上所述,使用“javascript:void(0)”而不是“#”可以防止滚动,点击导航栏滚动是一个我从网上找到的非常有用的技巧。
我使用 VisualStudio 作为我的 Web 编辑器,但编辑器本身只是一个普通的“html”文件,即“ImageMapEditor.htm”,您可以双击它并在任何 Web 浏览器中运行它来使用它。

Chrome Frame 插件。我建议安装 Chrome Frame 插件: 使用 Chrome Frame 插件的优点是,一旦安装,Internet Explorer 将支持较旧版本 IE 不支持的最新 HTML、JavaScript 和 CSS 标准功能。此插件对 Web 开发人员还有一个额外的好处,它允许他们使用现代 Web 功能编写应用程序,而不会让 IE 用户掉队。想想 Web 开发人员可以节省多少时间,而无需为 IE 编写 hack 和变通方法。

结论 

您可以自己决定哪种更好,是 Imagemapster 和标准图像映射,还是使用具有更多炫酷功能的 fabric 画布和 fabric 对象。当然,这取决于您的需求和客户的要求!至少这个编辑器可以让您创建这两种,并将它们进行比较测试。祝您使用愉快!
 

© . All rights reserved.