wangyuheng's Blog

time for change

github地址: https://github.com/wangyuheng/painter

DEMO地址: http://painter.crick.wang/

拖拽

使用svgjs官方推荐的拖拽插件https://github.com/wout/svg.draggable.js 实现元素的拖拽效果

暴露了4种事件监听

  • beforedrag
  • dragstart
  • dragmove (you can call preventDefault on this one)
  • dragend

可以根据需要结合。

在Element 的mouseover事件监听中,执行 _ele.draggable(); 方法,实现元素的可拖拽效果

为了避免拖拽后,触发click事件,导致pick状态变更,在这里监听了dragend事件,并将pick状态设置为未选中,则click事件触发时,一定会处理为选中。

(function() {

    SVG.extend(SVG.Doc, {
        transformPoint: function(event) {
            event = event || window.event;
            var touches = event.changedTouches && event.changedTouches[0] || event;
            var node = this.node;
            var point = node.createSVGPoint();
            point.x = touches.pageX - window.scrollX;
            point.y = touches.pageY - window.scrollY;
            var matrix = node.getScreenCTM().inverse();
            return point.matrixTransform(matrix);
        }
    });

    SVG.extend(SVG.Element, {
        pickable: function(enabled) {
            var _ele = this;
            GlobalStatus.pushElements(_ele);
            var color = _ele._stroke;
            var width = _ele.attr("stroke-width");
            _ele.on("mouseover", function() {
                console.log("element mouseover");
                if (GlobalStatus.isPicked()) {
                    _ele.stroke({
                        width: width * 2,
                        color: 'red'
                    });
                    $("#svgPanel").css("cursor", "pointer");
                    _ele.draggable();
                    return false;
                } else if (GlobalStatus.isPreFilled()) {
                    $("#svgPanel").css("cursor", "url(style/img/cur/tool_fill.cur), auto");
                } else if (GlobalStatus.isRecycle()) {
                    $("#svgPanel").css("cursor", "url(style/img/cur/tool_delete.cur), auto");
                }
                _ele.draggable(false);

            });
            _ele.on("mouseout", function() {
                if (GlobalStatus.isPicked()) {
                    _ele.stroke({
                        width: width,
                        color: color
                    });
                    $("#svgPanel").css("cursor", "default");
                } else if (GlobalStatus.isPreFilled()) {
                    $("#svgPanel").css("cursor", "default");
                } else if (GlobalStatus.isRecycle()) {
                    $("#svgPanel").css("cursor", "default");
                }

            });
            _ele.on("click", function() {
                console.log("click");
                if (GlobalStatus.isPreFilled()) {
                    if ($("#fill_color").hasClass("active")) {
                        _ele.fill(GlobalStatus.getFillColor());
                        _ele.style("fill-opacity", GlobalStatus.getFillOpacity());
                    } else {
                        _ele.style("stroke", GlobalStatus.getFontColor());
                    }

                } else if (GlobalStatus.isPicked()) {
                    if (_ele.attr("picked")) {
                        _ele.fire("unPick");
                    } else {
                        _ele.fire("pick");
                    }

                } else if (GlobalStatus.isRecycle()) {
                    _ele.remove();
                }
            });
            _ele.on("mousedown", function(event) {
                console.log("element mousedown");
                if (GlobalStatus.isPicked()) {
                    event.preventDefault();
                    event.stopPropagation();
                    return false;
                }
            });

            _ele.on("dragend", function(event) {
                console.log("element dragend");
                _ele.fire("unPick");
            });
            _ele.on("beforedrag", function(event) {
                console.log("element beforedrag");
            });
            _ele.on("pick", function() {
                console.log("pick");
                _ele.attr("picked", true);
                _ele.handleBorder = _ele.handleBorder || new HandleBorder(svgDoc);
                _ele.handleBorder.showShade(_ele);
                GlobalStatus.pushPicked(_ele);
            });
            _ele.on("unPick", function() {
                console.log("unPick");
                _ele.attr("picked", null);
                _ele.handleBorder && _ele.handleBorder.hideShade(_ele);
                GlobalStatus.removePicked(_ele);

            });
            return this;
        }

    });
})();

mouseover中的draggable();和 dragend事件中的fire(“unPick”);都不是很好的设计,需要重构。

bug修复

dragend事件中的fire(“unPick”)

针对dragend事件中的fire(“unPick”); 修改设计思路,在element记录开始拖拽时的坐标点数据,和dragend进行比较,如果坐标改变,表示元素发生移动,则按拖拽处理,需要触发fire(“unPick”); 否则,按照click事件处理。

_ele.dragStartPoint = null;
_ele.on("dragstart", function(event) {
    console.log("element dragstart");
    _ele.dragStartPoint = event.detail.p;
});
_ele.on("dragend", function(event) {
    console.log("element dragend");
    if (_ele.dragStartPoint.x == event.detail.p.x && _ele.dragStartPoint.y == event.detail.p.y) {

    } else {
        _ele.fire("unPick");
    }
});

github地址: https://github.com/wangyuheng/painter

DEMO地址: http://painter.crick.wang/

多选元素

原理

在选择按钮状态下,可以绘制一个曲线矩形,遍历所有元素判断,如果当前元素在矩形的坐标范围内,则元素被选中。

矩形四个点的坐标范围为x,x+width,y,y+height

变更element click事件

选择元素时,需要触发元素选中事件。如果触发click,会增加额外判断,并延时。所以单独抽离pick事件,将click中的事件处理指向pick

_ele.on("click", function() {
    if (GlobalStatus.isPreFilled()) {
        if ($("#fill_color").hasClass("active")) {
            _ele.fill(GlobalStatus.getFillColor());
            _ele.style("fill-opacity", GlobalStatus.getFillOpacity());
        } else {
            _ele.style("stroke", GlobalStatus.getFontColor());
        }

    } else if (GlobalStatus.isPicked()) {
        _ele.fire("pick");
    } else if (GlobalStatus.isRecycle()) {
        _ele.remove();
    }
});
_ele.on("pick", function() {
    if (_ele.attr("picked")) {
        _ele.attr("picked", null);
        _ele.handleBorder && _ele.handleBorder.hideShade(_ele);
        GlobalStatus.removePicked(_ele);
    } else {
        _ele.attr("picked", true);
        _ele.handleBorder = _ele.handleBorder || new HandleBorder(svgDoc);
        _ele.handleBorder.showShade(_ele);
        GlobalStatus.pushPicked(_ele);

    }
});

新增draw.tool.pick.js

新增DrawTool.Pick,操作与Rect类似,唯一的区别在于mouseup时判断元素是否在绘制的虚线矩形范围内,并对应触发pick事件。

(function() {

    var parent = null;
    var drawing = false;
    var element = null;
    var startPoint = null;

    function mousedown(event) {
        console.log('pick mousedown');
        drawing = true;
        startPoint = svgDoc.transformPoint(event);
        element = parent.rect(0, 0).fill(GlobalStatus.getFillColor()).style({
            "fill-opacity": GlobalStatus.getFillOpacity(),
            "stroke-dasharray": "13 10"
        }).stroke({
            width: "1",
            color: "grey"
        });
        return false;
    }

    function mousemove(event) {
        console.log('pick mousemove');
        if (drawing) {
            var svgPoint = svgDoc.transformPoint(event);
            var x = svgPoint.x;
            var y = svgPoint.y;

            var newWidth = x - startPoint.x;
            var newHeight = y - startPoint.y;
            var startX = startPoint.x;
            var startY = startPoint.y;
            if (newWidth < 0) {
                startX += newWidth;
            }

            if (newHeight < 0) {
                startY += newHeight;
            }
            newWidth = Math.abs(newWidth);
            newHeight = Math.abs(newHeight);
            element.x(startX).y(startY).width(newWidth).height(newHeight);
        }
        return false;
    };

    function mouseup(event) {
        console.log('pick mouseup ' + element);
        drawing = false;
        if (element.attr("width") > 0) {
            var sx = element.x();
            var ex = element.x() + element.width();
            var sy = element.y();
            var ey = element.y() + element.height();
            $(GlobalStatus.getAllElements()).each(function() {
                console.log(this.x(), this.y(), sx < this.x() && this.x() < ex && sy < this.y() && this.y() < ey);
                if (sx < this.x() && this.x() < ex && sy < this.y() && this.y() < ey) {
                    if (!this.attr("picked")) {
                        this.fire("pick");
                    }
                } else if (this.attr("picked")) {
                    this.fire("pick");
                }
            })
        }
        parent.removeElement(element);
        return false;
    }

    var listener = {
        mousedown: mousedown,
        mousemove: mousemove,
        mouseup: mouseup,
    };


    var Pick = function(parentEle) {
        parent = parentEle;
        console.log(parent);
        svgDoc = parent.doc();
        DrawTool.init(svgDoc, listener);
        this.stop = function() {
            DrawTool.stop(svgDoc, listener);
        };
    };

    this.DrawTool.Pick = Pick;

})();

首页监听选择按钮

在首页监听选择按钮,被选中时,创建DrawTool.Pick对象

$("#tool_pick").on("click", function() {
    resetCurrentDrawTool();
    currentDrawTool = new DrawTool.Pick(svgDoc);
});

弊端

有一个不足的地方,元素必须全在范围内,才能被选中。如果被选中部分,则无法选中。没想到好的解决方案。

bug修复

picked状态混乱

click和mouseup互相冲突,导致选中状态丢失。

将pick拆分为pick和unPick两个事件,分别处理选中状态,而不进行判断。将判断交给上层调用方法

 _ele.on("click", function() {
      console.log("click");
      if (GlobalStatus.isPreFilled()) {
          if ($("#fill_color").hasClass("active")) {
              _ele.fill(GlobalStatus.getFillColor());
              _ele.style("fill-opacity", GlobalStatus.getFillOpacity());
          } else {
              _ele.style("stroke", GlobalStatus.getFontColor());
          }

      } else if (GlobalStatus.isPicked()) {
          if (_ele.attr("picked")) {
              _ele.fire("unPick");
          } else {
              _ele.fire("pick");
          }

      } else if (GlobalStatus.isRecycle()) {
          _ele.remove();
      }
  });
  _ele.on("pick", function() {
      console.log("pick");
      _ele.attr("picked", true);
      _ele.handleBorder = _ele.handleBorder || new HandleBorder(svgDoc);
      _ele.handleBorder.showShade(_ele);
      GlobalStatus.pushPicked(_ele);
  });
  _ele.on("unPick", function() {
      console.log("unPick");
      _ele.attr("picked", null);
      _ele.handleBorder && _ele.handleBorder.hideShade(_ele);
      GlobalStatus.removePicked(_ele);

  });
  

Pick中

function mouseup(event) {
    console.log('pick mouseup ' + element);
    if (drawing) {
        drawing = false;
        if (element && element.attr("width") > 20) {
            var sx = element.x();
            var ex = element.x() + element.width();
            var sy = element.y();
            var ey = element.y() + element.height();
            $(GlobalStatus.getAllElements()).each(function() {
                console.log(this.x(), this.y(), sx < this.x() && this.x() < ex && sy < this.y() && this.y() < ey);
                if (sx < this.x() && this.x() < ex && sy < this.y() && this.y() < ey) {
                    if (!this.attr("picked")) {
                        this.fire("pick");
                    }
                } else if (this.attr("picked")) {
                    this.fire("unPick");
                }
            })
        }
        element && element.remove();
    }
    return false;
}

Pick拥有背景色

修改Pick的mousedown方法,独立设置style,不关联颜色选择器

function mousedown(event) {
    console.log('pick mousedown');
    if (!drawing) {
        drawing = true;
        startPoint = svgDoc.transformPoint(event);
        element = parent.rect(0, 0).style({
            "fill-opacity": "0.0",
            "stroke-dasharray": "10"
        }).stroke({
            width: "1",
            color: "grey"
        });
    }
    return false;
}

mousedown状态鼠标移出画板范围,松开鼠标,再次回到画板范围内,导致状态丢失

再回到画板时,鼠标已经移开,但是并未执行mouseup方法,因为时间的监听范围为画板内。所以mouseover事件继续执行,导致多生成了一个element。为了避免此问题,在mouse事件中增加drawing状态判断,以Rect为例

(function() {

    var parent = null;
    var drawing = false;
    var element = null;
    var startPoint = null;

    function mousedown(event) {
        console.log('rect mousedown');
        if (!drawing) {
            drawing = true;
            startPoint = svgDoc.transformPoint(event);
            element = parent.rect(0, 0).fill(GlobalStatus.getFillColor()).style("fill-opacity", GlobalStatus.getFillOpacity()).stroke({
                width: GlobalStatus.getLineSize(),
                color: GlobalStatus.getFontColor()
            });
        }
        return false;
    }

    function mousemove(event) {
        console.log('rect mousemove');
        if (drawing) {
            var svgPoint = svgDoc.transformPoint(event);
            var x = svgPoint.x;
            var y = svgPoint.y;

            var newWidth = x - startPoint.x;
            var newHeight = y - startPoint.y;
            var startX = startPoint.x;
            var startY = startPoint.y;
            if (newWidth < 0) {
                startX += newWidth;
            }

            if (newHeight < 0) {
                startY += newHeight;
            }
            newWidth = Math.abs(newWidth);
            newHeight = Math.abs(newHeight);
            element.x(startX).y(startY).width(newWidth).height(newHeight);
        }
        return false;
    };

    function mouseup(event) {
        console.log('rect mouseup ' + element);
        if (drawing) {
            drawing = false;
            if (element.attr("width") > 0) {
                element.pickable();
            } else {
                parent.removeElement(element);
            }
        }
        return false;
    }

    var listener = {
        mousedown: mousedown,
        mousemove: mousemove,
        mouseup: mouseup,
    };


    var Rect = function(parentEle) {
        parent = parentEle;
        console.log(parent);
        svgDoc = parent.doc();
        DrawTool.init(svgDoc, listener);
        this.stop = function() {
            DrawTool.stop(svgDoc, listener);
        };
    };

    this.DrawTool.Rect = Rect;

})();

所有DrawTool类方法都要增加此

bug修复

选择其他DrawTool后,选中状态不丢失。

在GlobalStatus增加清除所有选中状态的方法

unPickAll() {
    $(GlobalStatus.getPickeds()).each(function() {
        this.fire("unPick");
    });
    return this;
}

在index的resetCurrentDrawTool方法中调用执行

function resetCurrentDrawTool() {
    currentDrawTool && currentDrawTool.stop();
    GlobalStatus.unPickAll();
    $("#svgPanel").css("cursor", "default");
}

同时给右键的取消按钮增加此方法调用

label: ' 取消',
action: function () {
    GlobalStatus.unPickAll();
    return false;
}

github地址: https://github.com/wangyuheng/painter

DEMO地址: http://painter.crick.wang/

自定义右键菜单

使用开源的jquery插件jquery-simple-context-menu.js-1.3.2 实现自定义右键菜单的效果。
插件内容比较简单,方便进行二次开发和定制。

插件的设计思路主要是基于jquery,监听contextmenu事件,并append自定义的div菜单列表。

新增mouse_action.js,用于监听鼠标事件。给svg区域设置插件自定义的contextPopup事件。

(function(){
    $('#svgPanel').contextPopup({
    items: [
        {
            label: ' 取消',
            action: function () {
            }
        },
        {
            label: ' 选择',
            action: function () {
                $("#tool_pick").click();
            }
        },
        {
            label: ' 清屏',
            action: function () {
                $("#clear_all").click();
            }
        }
    ]
});
})();

修改了js文件的命名规则,用’.’替换了’_’

github地址: https://github.com/wangyuheng/painter

DEMO地址: http://painter.crick.wang/

删除

GlobalStatus增加isRecycle()方法,用来判断删除按钮是否被点击

isRecycle: function(){
    return $("#tool_delete").hasClass("active");
},

在Element扩展的click方法中,增加删除判断,执行remove()方法。并在mouseover时修改鼠标图标显示

(function() {

    SVG.extend(SVG.Doc, {
        transformPoint: function(event) {
            event = event || window.event;
            var touches = event.changedTouches && event.changedTouches[0] || event;
            var node = this.node;
            var point = node.createSVGPoint();
            point.x = touches.pageX - window.scrollX;
            point.y = touches.pageY - window.scrollY;
            var matrix = node.getScreenCTM().inverse();
            return point.matrixTransform(matrix);
        }
    });

    SVG.extend(SVG.Element, {
        pickable: function(enabled) {
            var _ele = this;
            GlobalStatus.pushElements(_ele);
            var color = _ele._stroke;
            var width = _ele.attr("stroke-width");
            _ele.on("mouseover", function() {
                if (GlobalStatus.isPicked()) {
                    _ele.stroke({
                        width: width * 2,
                        color: 'red'
                    });
                    $("#svgPanel").css("cursor", "pointer");
                } else if (GlobalStatus.isPreFilled()) {
                    $("#svgPanel").css("cursor", "url(style/img/cur/tool_fill.cur), auto");
                } else if (GlobalStatus.isRecycle()) {
                    $("#svgPanel").css("cursor", "url(style/img/cur/tool_delete.cur), auto");
                }


            });
            _ele.on("mouseout", function() {
                if (GlobalStatus.isPicked()) {
                    _ele.stroke({
                        width: width,
                        color: color
                    });
                    $("#svgPanel").css("cursor", "default");
                } else if (GlobalStatus.isPreFilled()) {
                    $("#svgPanel").css("cursor", "default");
                } else if (GlobalStatus.isRecycle()) {
                    $("#svgPanel").css("cursor", "default");
                }

            });
            _ele.on("click", function() {
                if (GlobalStatus.isPreFilled()) {
                    if ($("#fill_color").hasClass("active")) {
                        _ele.fill(GlobalStatus.getFillColor());
                        _ele.style("fill-opacity", GlobalStatus.getFillOpacity());
                    } else {
                        _ele.style("stroke", GlobalStatus.getFontColor());
                    }

                } else if (GlobalStatus.isPicked()) {
                    if (!_ele.attr("picked")) {
                        _ele.attr("picked", true);
                        _ele.handleBorder = _ele.handleBorder || new HandleBorder(svgDoc);
                        _ele.handleBorder.showShade(_ele);
                        GlobalStatus.pushPicked(_ele);
                    } else {
                        _ele.attr("picked", null);
                        _ele.handleBorder && _ele.handleBorder.hideShade(_ele);
                        GlobalStatus.removePicked(_ele);
                    }
                } else if (GlobalStatus.isRecycle()) {
                    _ele.remove();
                }
            });
            return this;
        }

    });
})();

清屏

单击清屏按钮时,清空所有的element元素,不需要按钮被点击状态。

此时有一个思考,是遍历所有的元素执行删除操作,还是重新设置svg doc。

先用遍历方法执行。

将elementList抽象到GlobalStatus方法中管理,并在元素完成后执行添加操作。

elementList : [],
pushElements: function(o){
    return this.elementList.push(o);
},
removeElements: function(o) {
    return this.elementList.remove(o);
},
getAllElements: function(){
    return this.elementList;
}

监听清屏按钮,遍历删除element

    $("#clear_all").on("click", function() {
        $(GlobalStatus.getAllElements()).each(function(){
            this.remove();
        });
    });

github地址: https://github.com/wangyuheng/painter

DEMO地址: http://painter.crick.wang/

图层移动

svgjs提供了图层移动的方法,实现方式如下

, forward: function() {
    var i = this.position() + 1
  , p = this.parent()

    // move node one step forward
  p.removeElement(this).add(this, i)

    // make sure defs node is always at the top
  if (p instanceof SVG.Doc)
      p.node.appendChild(p.defs().node)

    return this
  }
  // Send given element one step backward
, backward: function() {
    var i = this.position()

    if (i > 0)
      this.parent().removeElement(this).add(this, i - 1)

    return this
  }
  // Send given element all the way to the front
, front: function() {
    var p = this.parent()

    // Move node forward
  p.node.appendChild(this.node)

    // Make sure defs node is always at the top
  if (p instanceof SVG.Doc)
      p.node.appendChild(p.defs().node)

    return this
  }
  // Send given element all the way to the back
, back: function() {
    if (this.position() > 0)
      this.parent().removeElement(this).add(this, 0)

    return this
  }
 

在按钮上绑定对应函数,并结合已选中元素,即可实现图层移动效果。此时需要注意,避免g元素,并且判断是否为最上(下)层元素,因为源码种只是执行+1 -1操作,如果最上层继续向上移动多次后,需要向下移动多次,才可以实现图层的切换效果。g元素的判断为偷懒方法,需要考虑是否考虑白名单的数组,以及改用迭代的方式进行

    $("#tool_layer_front").on("click", function() {
        var elements = GlobalStatus.getPickeds();
        if (elements.length > 0) {
            $(elements).each(function(){
                this
                this.front();
            });
        }
    });
    $("#tool_layer_back").on("click", function() {
        var elements = GlobalStatus.getPickeds();
        if (elements.length > 0) {
            $(elements).each(function(){
                this.back();
            });
        }
    });
    $("#tool_layer_backward").on("click", function() {
        var elements = GlobalStatus.getPickeds();
        if (elements.length > 0) {
            $(elements).each(function(){
                if (this.previous()) {
                    //TODO 迭代 且使用白名单数组
                    if (this.previous().type == "g") {
                        this.backward();
                    }
                    this.backward();
                }
            });
        }
    });
    $("#tool_layer_forward").on("click", function() {
        var elements = GlobalStatus.getPickeds();
        if (elements.length > 0) {
            $(elements).each(function(){
                if (this.next()) {
                    if (this.next().type == "g") {
                        this.forward();
                    }
                    this.forward();
                }
            });
        }
    });
    

这里发现了一个bug。在图层移动的时候,单步移动有时会失效。排查后发现,是因为mousedown时,已经生成了一个element,但在页面中看不出来。为了解决此bug,在mouseup时,增加了一个判断,如果页面不可见,则将此element在parent中移除。parent为svgDoc

function mouseup(event) {
    console.log('rect mouseup ' + element);
    drawing = false;
    if (element.attr("width") > 0) {
        element.pickable();
    } else {
        parent.removeElement(element);
    }
    return false;
}

github地址: https://github.com/wangyuheng/painter

DEMO地址: http://painter.crick.wang/

自定义鼠标图标

可以通过css设置cursor的方式指定鼠标图标样式

| default | 默认光标(通常是一个箭头) |
| auto | 默认。浏览器设置的光标。 |
| crosshair | 光标呈现为十字线。 |
| pointer | 光标呈现为指示链接的指针(一只手) |
| move | 此光标指示某对象可被移动。 |
| e-resize | 此光标指示矩形框的边缘可被向右(东)移动。 |
| ne-resize | 此光标指示矩形框的边缘可被向上及向右移动(北/东)。 |
| nw-resize | 此光标指示矩形框的边缘可被向上及向左移动(北/西)。 |
| n-resize | 此光标指示矩形框的边缘可被向上(北)移动。 |
| se-resize | 此光标指示矩形框的边缘可被向下及向右移动(南/东)。 |
| sw-resize | 此光标指示矩形框的边缘可被向下及向左移动(南/西)。 |
| s-resize | 此光标指示矩形框的边缘可被向下移动(南)。 |
| w-resize | 此光标指示矩形框的边缘可被向左移动(西)。 |
| text | 此光标指示文本。 |
| wait | 此光标指示程序正忙(通常是一只表或沙漏)。 |
| help | 此光标指示可用的帮助(通常是一个问号或一个气球)。 |

也可以通过url指定自定义的图标样式,通常为.cur文件。

$("#svgPanel").css("cursor", "url(style/img/cur/tool_pencil.cur), auto");

auto为指定图标不存在时,鼠标显示的样式。

铅笔

通过svg的path属性,可以绘制出鼠标移动轨迹,实现铅笔画图效果。svgjs提供了path()函数,值为

  • M = moveto
  • L = lineto
  • H = horizontal lineto
  • V = vertical lineto
  • C = curveto
  • S = smooth curveto
  • Q = quadratic Belzier curve
  • T = smooth quadratic Belzier curveto
  • A = elliptical Arc
  • Z = closepath

所以mousedown时做 M+坐标点,mousemove时,为L+坐标点,可以指定不同的路径数据以达到不同的绘制效果,所以在构造函数中增加了一个prefix字段。

pencil.js 代码如下

(function() {

    var parent = null;
    var drawing = false;
    var element = null;
    var startPoint = null;

    var plot = null;
    var plotPrefix = null;
    var defaultPlotPrefix = 'L';

    function mousedown(event) {
        console.log('pencil mousedown');
        drawing = true;
        startPoint = svgDoc.transformPoint(event);
        plot = 'M' + startPoint.x + ' ' + startPoint.y;
        element = parent.path(plot).fill(GlobalStatus.getFillColor()).style("fill-opacity", GlobalStatus.getFillOpacity()).stroke({
            width: GlobalStatus.getLineSize(),
            color: GlobalStatus.getFontColor()
        });
        return false;
    }

    function mousemove(event) {
        console.log('pencil mousemove');
        if (drawing) {
            var startPoint = svgDoc.transformPoint(event);
            console.log(plot);
            plot += plotPrefix + startPoint.x + ' ' + startPoint.y;
            element.plot(plot);
        }
        return false;
    };

    function mouseup(event) {
        console.log('pencil mouseup ' + element);
        drawing = false;
        if (element.attr("d").split(plotPrefix).length > 2) {
            element.pickable();
        }
        return false;
    }

    var listener = {
        mousedown: mousedown,
        mousemove: mousemove,
        mouseup: mouseup,
    };


    var Pencil = function(parentEle, prefix) {
        parent = parentEle;
        svgDoc = parent.doc();
        DrawTool.init(svgDoc, listener);
        plotPrefix = prefix || defaultPlotPrefix;
        this.stop = function() {
            DrawTool.stop(svgDoc, listener);
        };
    };

    this.DrawTool.Pencil = Pencil;

})();

在首页监听铅笔的点击事件,并在页面加载完成后默认调用 $(“#tool_pencil”).click();

    $("#tool_pencil").on("click", function() {
        resetCurrentDrawTool();
        currentDrawTool = new DrawTool.Pencil(svgDoc);
        $("#svgPanel").css("cursor", "url(style/img/cur/tool_pencil.cur), auto");
    });
    

加了一个彩蛋,监听铅笔按钮的双击事件,可以绘制映射线。

    $("#tool_pencil").on("dblclick", function() {
        resetCurrentDrawTool();
        currentDrawTool = new DrawTool.Pencil(svgDoc, 'T');
        $("#svgPanel").css("cursor", "url(style/img/cur/tool_pencil.cur), auto");
    });

在Tools的listener方法中,都加了一个return false; 避免事件向上传递。

在模拟的下拉列表lizeSize中,通过属性data-line-size记录width值,在GlobalStatus.getLineSize()中调用。

将已选择元素抽象到在GlobalStatus中,并提供管理方法。

pickedElementList: [],
pushPicked: function(o){
    return this.pickedElementList.push(o);
},
removePicked: function(o) {
    return this.pickedElementList.remove(o);
},
getPickeds: function(){
    return this.pickedElementList;
}

github地址: https://github.com/wangyuheng/painter

DEMO地址: http://painter.crick.wang/

边框与背景色

首先,使用了开源的 spectrum.js 作为颜色选择器,
之前以SVG对象保存全局方法与对象,现在通过新定义一个GlobalStatus,存在全局属性,并通过按钮状态判断状态

var GlobalStatus = {
    defaultFontColor:"#0073c6",
    defaultFillColor:"#ececec",
    defaultLineSize:"2",
    isPicked:function(){
        return $("#tool_pick").hasClass("active");
    },
    isPreFilled:function(){
        return $("#tool_fill").hasClass("active");
    },
    getFontColor:function(){
        return $("#font_color").attr("data-color");
    },
    getFillColor:function(){
        return $("#fill_color").attr("data-color");
    },
    getLineSize:function(){
        return this.defaultLineSize;
    },
    getFillOpacity:function(){
        var fillOpacity = "0.0";
        if (this.getFillColor() != this.defaultFillColor) {
            fillOpacity = "1.0";
        }
        return fillOpacity;
    }
}

在首页给默认线条颜色及背景颜色赋值,并设置状态data-color保存颜色值

    $("#font_color span").css("background", GlobalStatus.defaultFontColor);
    $("#font_color").attr("data-color", GlobalStatus.defaultFontColor);
    $("#fill_color span").css("background", GlobalStatus.defaultFillColor);
    $("#fill_color").attr("data-color", GlobalStatus.defaultFillColor);

颜色选择器发生变化时,同时变更颜色与data-color属性值

    $("#colorPalettes").spectrum({
        flat: true,
        showPaletteOnly: true,
        color: '#0073c6',
        move: function(tinycolor) {
            console.log("color move");
            console.log(tinycolor.toHexString())
            if ($("#fill_color").hasClass("active")) {
                $("#fill_color").attr("data-color", tinycolor.toHexString());
                $("#fill_color span").eq(0).css("background", tinycolor.toHexString());

            } else if ($("#font_color").hasClass("active")) {
                $("#font_color").attr("data-color", tinycolor.toHexString());
                $("#font_color span").eq(0).css("background", tinycolor.toHexString());
            }

        },

        palette: spectrumColors
    });
    

这里有一个取巧的地方,为了让背景颜色透明显示,规定ececec为透明颜色。选择此颜色时,将透明度opacity设置为全透明。

这样做有一个问题,如果用户自定义此颜色,可以将自定义的颜色修改为ececed,不会在视觉上有较大影响。

在绘制元素时,通过GlobalStatus设置元素的边框、颜色、及透明度。已rect为例

function mousedown(event) {
    console.log('rect mousedown');
    drawing = true;
    startPoint = svgDoc.transformPoint(event);
    element = parent.rect(0, 0).fill(GlobalStatus.getFillColor()).style("fill-opacity", GlobalStatus.getFillOpacity()).stroke({
        width: GlobalStatus.getLineSize(),
        color: GlobalStatus.getFontColor()
    });
}

此时,绘制的颜色就会根据设置的颜色及背景色发生变化。

喷枪

在GlobalStatus中增加了isPreFilled()方法,判断喷枪是否被选中。在Element的扩展方法中,监听click事件。
如果为选中喷枪的点击,则修改元素的对应属性值即可。

_ele.on("click", function() {
    if (GlobalStatus.isPreFilled()) {
        if ($("#fill_color").hasClass("active")) {
            _ele.fill(GlobalStatus.getFillColor());
            _ele.style("fill-opacity", GlobalStatus.getFillOpacity());
        } else {
            _ele.style("stroke", GlobalStatus.getFontColor());
        }

    } else if (GlobalStatus.isPicked()) {
        if (!_ele.attr("picked")) {
            _ele.attr("picked", true);
            console.log(_ele.handleBorder);
            _ele.handleBorder = _ele.handleBorder || new HandleBorder(svgDoc);
            _ele.handleBorder.showShade(_ele);
            pickedElementList.push(_ele);
        } else {
            _ele.attr("picked", null);
            _ele.handleBorder && _ele.handleBorder.hideShade(_ele);
            pickedElementList.remove(_ele);
        }
    }
});

这里有一个用户体验的问题,喷枪是同时修改边框色和背景色,还是分别修改?

现在的实现方法为分别修改。用户需要点击线条与背景色,然后再用喷枪工具分别修改元素颜色。

github地址: https://github.com/wangyuheng/painter

DEMO地址: http://painter.crick.wang/

针对元素进行操作时,如果采用更改边框颜色的形式,会和修改边框冲突。所以采用增加4个操作边框的形式,既能表示元素被选中,又能在后续增加拖拽,变形等操作。

增加了一个HandleBorder类,并扩展其prototype属性对象,增加方法

  1. createShade 用于创建4个边框
  2. judgeShade 用于将4个小矩形,放至所选元素的4个边角
  3. showShade 显示操作边框
  4. 隐藏操作边框

加入了一些距离计算和矩阵,详细代码如下

(function() {

    var sbw = 6;
    var bw = {
        width: 1
    };

    var HandleBorder = function(svgDoc) {
        this.init(svgDoc);
    }

    HandleBorder.prototype = {
        constructor: HandleBorder,
        init: function(svgDoc) {
            this.currentSvgDoc = svgDoc;
            this.createShade();
            return this;
        },
    };

    HandleBorder.prototype.createShade = function() {
        var _this = this;

        _this.transformerGroup = _this.currentSvgDoc.group();

        _this.blockGroup = _this.transformerGroup.group();

        _this.rectLT = this.blockGroup.rect(sbw, sbw).stroke(bw).attr({
            '_operate-type': 'scale',
            '_direction': 'lt'
        });
        _this.rectLB = this.blockGroup.rect(sbw, sbw).stroke(bw).attr({
            '_operate-type': 'scale',
            '_direction': 'lb'
        });
        _this.rectRT = this.blockGroup.rect(sbw, sbw).stroke(bw).attr({
            '_operate-type': 'scale',
            '_direction': 'rt'
        });
        _this.rectRB = this.blockGroup.rect(sbw, sbw).stroke(bw).attr({
            '_operate-type': 'scale',
            '_direction': 'rb'
        });

    };

    HandleBorder.prototype.judgeShade = function(bbox, matrix) {
        var x1 = bbox.x;
        var y1 = bbox.y;
        var x2 = bbox.x2;
        var y2 = bbox.y2;


        this.rectLT.move(x1 - sbw, y1 - sbw);
        this.rectLB.move(x1 - sbw, y2);
        this.rectRT.move(x2, y1 - sbw);
        this.rectRB.move(x2, y2);

        this.blockGroup.matrix(matrix);

    };

    HandleBorder.prototype.showShade = function(svgEle) {
        if (!svgEle) {
            return;
        }
        this.currentElement = svgEle;
        this.transformerGroup.show();

        this.judgeShade(svgEle.bbox(), new SVG.Matrix(svgEle));
    };

    HandleBorder.prototype.hideShade = function() {
        this.transformerGroup.hide();
    };

    this.HandleBorder = HandleBorder;

})();

在Element的扩展方法中,利用HandleBorder替换边框颜色变更操作,并将new的HandleBorder对象绑定到元素中

_ele.on("click", function() {
    if (SVG.isPicked()) {
        if (!_ele.attr("picked")) {
            _ele.attr("picked", true);
            console.log(_ele.handleBorder);
            _ele.handleBorder = _ele.handleBorder || new HandleBorder(svgDoc);
            _ele.handleBorder.showShade(_ele);
            pickedElementList.push(_ele);
        } else {
            _ele.attr("picked", null);
            _ele.handleBorder && _ele.handleBorder.hideShade(_ele);
            pickedElementList.remove(_ele);
        }
    }
});

扩展了一个pickedElementList数组,并在SVG扩展中提供获取方法

    getPickedElementList: function(){
        return pickedElementList;
    }
    

为了方便移除操作,强化了数组操作,提供了remove方法

Array.prototype.indexOf = function(val) {
    for (var i = 0; i < this.length; i++) {
        if (this[i] == val) return i;
    }
    return -1;
};
Array.prototype.remove = function(val) {
    var index = this.indexOf(val);
    if (index > -1) {
        this.splice(index, 1);
    }
};

选择元素操作已经完成,可以在此基础上绑定颜色操作,以及拖拽及变形操作。

github地址: https://github.com/wangyuheng/painter

DEMO地址: http://painter.crick.wang/

为了针对某个元素进行操作,增加了一个选中操作的按钮。点击此按钮后,鼠标变为手指选择,滑动到元素上方时,元素特殊显示,单击后即为选中状态。

首先,增加一个全局的状态位,判断选择按钮是否被选中

SVG.isPicked = function(){
    return $("#tool_pick").hasClass("active");
}

单击 pick 按钮时,鼠标变为手指

$("#tool_pick").on("click", function(){
    if (SVG.isPicked()) {
        $("#svgPanel").css("cursor","pointer");
    }
});

因为要清空当前drawtool的事件监听,同时也要重置鼠标样式,所以抽离函数resetCurrentDrawTool,在现有handle btn点击时,进行调用

function resetCurrentDrawTool() {
    currentDrawTool && currentDrawTool.stop();
    $("#svgPanel").css("cursor","default");
}

针对选择元素操作,通过监听mouseover、mouseout、click事件,通过样式以及线框宽度标识。 为了方便调用,将此事件绑定,抽象为Element元素的扩展方法

SVG.extend(SVG.Element, {
    pickable: function(enabled) {
        var _ele = this;
        elementList.push(_ele);
        var color = _ele._stroke;
        var width = _ele.attr("stroke-width");
        _ele.on("mouseover", function() {
            if (SVG.isPicked() && !_ele.attr("picked")) {
                _ele.stroke({
                    width: width * 2,
                    color: 'red'
                });
            }
        });
        _ele.on("mouseout", function() {
            if (SVG.isPicked() && !_ele.attr("picked")) {
                _ele.stroke({
                    width: width,
                    color: color
                });
            }
        });
        _ele.on("click", function() {
            if (SVG.isPicked()) {
                if (!_ele.attr("picked")) {
                    _ele.attr("picked", true);
                    _ele.stroke({
                        width: width * 2,
                        color: 'yellow'
                    });
                } else {
                    _ele.attr("picked", null);
                    _ele.stroke({
                        width: width,
                        color: color
                    });
                }
            }
        });
        return this;
    }

});

在DrawTool的具体实现的mouseup方法中,独立判断并执行。以Rect为例,则为

function mouseup(event) {
    drawing = false;
    if (element.attr("width") > 0) {
        element.pickable();
    }
}

首页增加elementList数据,记录所有函数,方便操作。

TODO 选中元素,现在为修改边框颜色,应该修改为其他方式。
TODO 针对所有选中元素,应该存在统一数据内,方便批量操作。

github地址: https://github.com/wangyuheng/painter

DEMO地址: http://painter.crick.wang/

针对上个版本的基础代码进行抽象和封装

将所有的图形绘制工具,都“继承”一个DrawTool工具类,在此工具类中绑定、解绑事件,代码如下

(function() {
    var defaultListener = {
        mousedown: function() {
            console.log('mousedown you should implement this function!');
        },
        mousemove: function() {
            console.log('mousemove you should implement this function!');
        },
        mouseup: function() {
            console.log('mouseup   you should implement this function!');
        }
    };
    var DrawTool = {
        init: function(svgDoc, listeners) {
            var l = $.extend({}, defaultListener, listeners);
            svgDoc.on('mousedown', l.mousedown);
            svgDoc.on('mousemove', l.mousemove);
            svgDoc.on('mouseup', l.mouseup);
        },
        stop: function(svgDoc, listeners) {
            var l = $.extend({}, defaultListener, listeners);
            svgDoc.off('mousemove', l.mousemove);
            svgDoc.off('mousedown', l.mousedown);
            svgDoc.off('mouseup', l.mouseup);
        }
    };


    this.DrawTool = DrawTool;
})();

所有js独立代码都采用闭包形式,避免的参数污染。 拆分后的目录结构如下

painter-2-0

还是以矩形为例,Rect在构造函数中继承使用了DrawTool的init方法,并重写stop方法,暴露出去。

var Rect = function(parentEle) {
    parent = parentEle;
    svgDoc = parent.doc();
    DrawTool.init(svgDoc, listener);
    this.stop = function () {
        DrawTool.stop(svgDoc, listener);
    };
};

this.DrawTool.Rect = Rect;

通过在Rect类内部定义mousedown、mousemove、mouseup事件方法,调用svgjs提供的绘制方法,实现图形的绘制工作。完整代码如下:

(function() {

    var parent = null;
    var drawing = false;
    var element = null;
    var startPoint = null;

    function mousedown(event) {
        console.log('rect mousedown');
        drawing = true;
        startPoint = svgDoc.transformPoint(event);
        element = parent.rect(0, 0).style("fill-opacity", '0.0').stroke({
            width: '2',
            color: '#000000'
        });
    }

    function mousemove(event) {
        console.log('rect mousemove');
        if (drawing) {
            var svgPoint = svgDoc.transformPoint(event);
            var x = svgPoint.x;
            var y = svgPoint.y;

            var newWidth = x - startPoint.x;
            var newHeight = y - startPoint.y;
            var startX = startPoint.x;
            var startY = startPoint.y;
            if (newWidth < 0) {
                startX += newWidth;
            }

            if (newHeight < 0) {
                startY += newHeight;
            }
            newWidth = Math.abs(newWidth);
            newHeight = Math.abs(newHeight);
            element.x(startX).y(startY).width(newWidth).height(newHeight);
        }
    };

    function mouseup(event) {
        console.log('rect mouseup ' + element);
        drawing = false;
    }

    var listener = {
        mousedown: mousedown,
        mousemove: mousemove,
        mouseup: mouseup,
    };


    var Rect = function(parentEle) {
        parent = parentEle;
        svgDoc = parent.doc();
        DrawTool.init(svgDoc, listener);
        this.stop = function () {
            DrawTool.stop(svgDoc, listener);
        };
    };

    this.DrawTool.Rect = Rect;

})();

有一个特殊的类为折线polyline工具类,此图形为连续绘制,需要记录上一个element,并且在stop方法中清空之前的element,并且监听鼠标右键点击事件,结束图形绘制。
阻止鼠标右键方法为

function mousedown(event) {
    if (event.button == 2) {
        document.oncontextmenu=function(){return false;}
        drawing=false;
        points  = [];
        element = null;
        return;
    }
    if (!drawing) {
        drawing = true;
        var currPoint = svgDoc.transformPoint(event);
        points.push([currPoint.x, currPoint.y]);
    }
}

至此,已完成图形的基本绘制功能

0%