zoukankan      html  css  js  c++  java
  • 7.组件连线(贝塞尔曲线)从零起步实现基于Html5的WEB设计器Jquery插件(含源码)

    上节讲到如何创建组件,清除设计器视图,以及设计视图的持久化和恢复,本节将重点讲如何实现组件间的连线,前面章节有提到为了方便从持久化文件中恢复,组件和连线是分别存放的:nodes和lines对象,两个组件实现连线主要也还是通过鼠标拖动事件实现,但前提是有一个连接点的概念,即我们要从组件上、下、左、右四个锚点中开始拖动,在拖动过程中绘制跟随线,拖到目标组件上时出现锚点,在锚点上释放鼠标,在两个锚点间绘制连线,并将连线加到lines数组中。

    下图是要实现的锚点图样例:

     

    锚点为红色边框,整个组件可以作为一个锚点,同时四个x也可作为特定方位的锚点,锚点出现时鼠标为十字形状,代表允许按下鼠标拖动连线了。大家注意,我在打开按钮的边上增加了一个checkbox连线,用来指示是在连线状态与否,取消这个勾选,是不会出现连线锚点的,选择、拖动组件只有在非连线状态下进行,两者互斥。

        function Connector(node) {
    
            this.node = node;
            this.group = null;
        }
        Connector.prototype = {
    
            destroy: function () {
                this.group.remove();
            },
            hiTest: function (event) {
                var bounds = this.node.getBound();
                if (event.point.x >= bounds.x - 5 && event.point.x <= bounds.x + 5 && event.point.y >= bounds.y + bounds.height / 2 - 5 && event.point.y <= bounds.y + bounds.height / 2 + 5)
                {
                    //在左连线指示器框中
                    this.group.children[0].bounds.x = bounds.x - 5;
                    this.group.children[0].bounds.y = bounds.y + bounds.height / 2 - 5;
                    this.group.children[0].bounds.width = 10;
                    this.group.children[0].bounds.height = 10;
                }
                else if (event.point.x >= bounds.x + bounds.width - 5 && event.point.x <= bounds.x + bounds.width  + 5 && event.point.y >= bounds.y + bounds.height / 2 - 5 && event.point.y <= bounds.y + bounds.height / 2 + 5) {
                    //在右连线指示器框中
                    this.group.children[0].bounds.x = bounds.x + bounds.width  - 5;
                    this.group.children[0].bounds.y = bounds.y + bounds.height / 2 - 5;
                    this.group.children[0].bounds.width = 10;
                    this.group.children[0].bounds.height = 10;
                }
                else if (event.point.x >= bounds.x + bounds.width / 2 - 5 && event.point.x <= bounds.x + bounds.width / 2  + 5 && event.point.y >= bounds.y  - 5 && event.point.y <= bounds.y  + 5) {
                    //在上连线指示器框中
                    this.group.children[0].bounds.x = bounds.x + bounds.width / 2  - 5;
                    this.group.children[0].bounds.y = bounds.y  - 5;
                    this.group.children[0].bounds.width = 10;
                    this.group.children[0].bounds.height = 10;
                }
                else if (event.point.x >= bounds.x + bounds.width / 2 - 5 && event.point.x <= bounds.x + bounds.width / 2  + 5 && event.point.y >= bounds.y + bounds.height - 5 && event.point.y <= bounds.y + bounds.height+ 5) {
                    //在下连线指示器框中
                    this.group.children[0].bounds.x = bounds.x + bounds.width / 2  - 5;
                    this.group.children[0].bounds.y = bounds.y + bounds.height  - 5;
                    this.group.children[0].bounds.width = 10;
                    this.group.children[0].bounds.height = 10;
                }
                else
                {
                    this.group.children[0].bounds.x = bounds.x 
                    this.group.children[0].bounds.y = bounds.y;
                    this.group.children[0].bounds.width = bounds.width;
                    this.group.children[0].bounds.height = bounds.height;
                }
            },
            render: function () {
                var me = this;
                var color = 'white';
                this.group = new paper.Group();
                var rect = new paper.Path.Rectangle({
                    point: [this.node.getBound().x, this.node.getBound().y],
                    size: [this.node.getBound().width, this.node.getBound().height],
                    strokeColor: 'red',
                    strokeWidth: 2
                })
                rect.onMouseDown = function (event) {
                    debugger;
    
                };
                this.group.addChild(rect);
                var bounds = this.node.getBound();
                var topCross1 = new paper.Path.Line({ from: [bounds.x + bounds.width / 2 - 2.5, bounds.y - 2.5], to: [bounds.x + bounds.width / 2 + 2.5, bounds.y + 2.5], strokeColor: 'blue' });
                this.group.addChild(topCross1);
                var topCross2 = new paper.Path.Line({ from: [bounds.x + bounds.width / 2 - 2.5, bounds.y + 2.5],to: [bounds.x + bounds.width / 2 + 2.5, bounds.y - 2.5], strokeColor: 'blue' });
                this.group.addChild(topCross2);
    
                var rightCross1 = new paper.Path.Line({ from: [bounds.x + bounds.width - 2.5, bounds.y + bounds.height / 2 - 2.5], to: [bounds.x + bounds.width + 2.5, bounds.y + bounds.height / 2 + 2.5], strokeColor: 'blue' });
                this.group.addChild(rightCross1);
                var rightCross2 = new paper.Path.Line({ from: [bounds.x + bounds.width - 2.5, bounds.y + bounds.height / 2 + 2.5], to: [bounds.x + bounds.width + 2.5, bounds.y + bounds.height / 2 - 2.5], strokeColor: 'blue' });
                this.group.addChild(rightCross2);
    
                var leftCross1 = new paper.Path.Line({ from: [bounds.x - 2.5, bounds.y + bounds.height / 2 - 2.5], to: [bounds.x + 2.5, bounds.y + bounds.height / 2 + 2.5], strokeColor: 'blue' });
                this.group.addChild(leftCross1);
                var leftCross2 = new paper.Path.Line({ from: [bounds.x - 2.5, bounds.y + bounds.height / 2 + 2.5], to: [bounds.x + 2.5, bounds.y + bounds.height / 2 - 2.5], strokeColor: 'blue' });
                this.group.addChild(leftCross2);
    
                var bottomCross1 = new paper.Path.Line({ from: [bounds.x + bounds.width / 2 - 2.5, bounds.y + bounds.height - 2.5], to: [bounds.x + bounds.width / 2 + 2.5, bounds.y + bounds.height + 2.5], strokeColor: 'blue' });
                this.group.addChild(bottomCross1);
                var bottomCross2 = new paper.Path.Line({ from: [bounds.x + bounds.width / 2 - 2.5, bounds.y + bounds.height + 2.5], to: [bounds.x + bounds.width / 2 + 2.5, bounds.y + bounds.height  - 2.5], strokeColor: 'blue' });
                this.group.addChild(bottomCross2);
                this.group.bringToFront();
                var drag = false;
                return this;
            }
    
        };

    上面代码hiTest方法用于测式当前鼠标位置是显示哪个锚点:整个组件/上/下/左/右,并移动对应的锚点红色矩形。render画了一个红色矩形和四个连接点x。

    在VisualDesigner中增加lining属性指示是否画线状态,

    在Component中增加activeConnector指示当前活动的连接器锚点

    在Component.init方法中增加了鼠标进入,退出后的连接点创建和删除,如下代码片断:

        Component.prototype.init = function (options) {
            if (options == undefined)
                options = {};
            this.properties = $.extend(options, Component.DEFAULTS);
            this.group = new paper.Group();
            this.designer = undefined; //当前设计器,createElement时赋值
            var me = this;
            var drag = false;
    
            this.activateConnector = null; //活动的连线指示符
            this.group.onClick = function (event) {
                if (!me.designer.lining) //非画线状态才允许选中
                    me.group.children[0].selected = !me.group.children[0].selected;
            }
            this.group.onMouseDown = function (event) {
                if (!me.designer.lining) //非画线状态才允许拖动
                    drag = (event.event.button == 0);
                else {
                    drawing = true;
                }
            }
            this.group.onMouseUp = function () {
                drag = false;
                document.body.style.cursor = 'default';
            }
            this.group.onMouseDrag = function (event) {
                if (drag && !me.designer.lining) //非画线状态才允许拖动
                {
                    if (me.activateConnector) //在拖动元素时如果有连线指示器则清除。
                    {
                        me.activateConnector.destroy();
                        me.activateConnector = null;
                    }
                     me.properties.x += event.delta.x;
                    me.properties.y += event.delta.y;
                    this.translate(event.delta.x, event.delta.y);
                    document.body.style.cursor = 'move';
                }
            }
    
            this.group.onMouseEnter = function (event) {
                if (!me.activateConnector && me.designer.lining) //还没有创建连接指示框,且当前为连线状态
                {
                    me.designer.selectAll(false);//取消选中所有元素,if any
                    me.activateConnector = new Connector(me).render();
                    document.body.style.cursor = 'crosshair';
            }
            }
            this.group.onMouseLeave = function (event) {
                if (me.designer.lining && me.activateConnector) { //当前为连线状态,且移出了组件范围 ,擦除连线指示框
                    me.activateConnector.destroy();
                    me.activateConnector = null;
                    console.log("delete in group")
                    document.body.style.cursor = 'default';
    
                }
            }
            this.group.onMouseMove = function (event) {
                if (me.designer.lining && me.activateConnector) { //当前为连线状态,且在组件范围 ,检测四个边线连线指示框
                    me.activateConnector.hiTest(event)
                }
            }
            return this;
        }

    这里说一个小插曲,因为要在组件的代码里访问设计器的成员(如是否画线状态visualDesigner.lining),我在Component里增加了一个designer对象来保存当前设计器,并在代码中访问,可保存设计视图时出现JSON对象序例化时出现递归的异常 ,因为序列化nodes组件对象数组时,每一个组件里有VisualDesigner对象而VisualDesigner对象里又有nodes对象的数组,首先想到的是特定的属性不要序列化,查资料后发现JSON.stringify里有第二个参数,可以为可序列化属性名称的白名单数组,也可以为函数,此外因为属性名称并不完全确定,所以用函数:

        VisualDesigner.prototype.getContent = function () {
            debugger;
            return JSON.stringify({ "nodes": this.nodes, "lines": this.lines },
                function (k, v) {
                    if (k == "designer") {
                        return undefined;
                    }
                    return v;
                });
        }

     依据面向对象的编程方法单一职责原则,增加了一个类(lineManager),专门用来管理连线的过程管理,在连线时要保持住前一个结点,在拖动结束时画出线,代码如下:

        function LineManager(designer) {
            this.designer = designer;
            this.line = null;//当前跟随线
            this.start = null;//当前正在画线的起点元素
            this.startPos=null;
            var tool=new paper.Tool();
            //设计器元素之外的移动也要显示跟随线,
            var me=this;
            tool.onMouseMove=function(event){
                me.draging(event.point);
            }
            tool.onMouseUp=function(event)
            {
                //设计器元素之外的释放不生成连线,清除已有开始结点等信息,
                if (me.line)
                    me.line.remove();
                me.start=null;
                me.startPos=null;
                me.line=null;
            }
        }
        LineManager.prototype = {
            dragStart: function (co,pos) {
                    this.start = co;
                    var xy = co.node.getConnectorCenter(pos); //获取当前鼠标位置处连接点的中央坐标
                    this.startPos=xy;
                    this.line = new paper.Path.Line({
                        from: [xy.x, xy.y],
                        to: [xy.x, xy.y],
                        strokeWidth: 2,
                        strokeColor: 'red'
                    });
            },
            draging: function (pos) {
                if (this.line !== null ) {
                    var txy = this.calcLine(this.startPos.x, this.startPos.y, pos.x, pos.y);
                    this.line.set({ pathData: 'M' + this.startPos.x + ',' + this.startPos.y + ' L' + txy.x + ',' + txy.y });
                }
            },
            dragEnd:function(co,pos)
            {
                var xy = co.node.getConnectorCenter(pos); //获取当前鼠标位置处连接点的中央坐标
                if (this.line !== null  ) {
                    if (this.start.node.properties.id!=co.node.properties.id){
                        this.designer.createLine("曲线",{targetType:co.node.getConnectorDirection(this.startPos,pos),source:this.start.node.properties.id,target:co.node.properties.id,sxy:this.startPos,txy:xy});
                    }
                    this.line.remove();
    
                }
                this.start=null; //清除画线状态,等待重新画线
                this.startPos=null;
                
            },
    
            calcLine: function (x1, y1, x2, y2) {
                var vx = x2 - x1;
                var vy = y2 - y1;
                var d = Math.sqrt(vx * vx + vy * vy);
                vx /= d;
                vy /= d;
                d = Math.max(0, d - 5);
                return {
                    'x': Math.round(x1 + vx * d),
                    'y': Math.round(y1 + vy * d)
                }
            }
        }

    同时增加了曲线的类(贝塞尔曲线),

    function BezierLine() {
            
        }
        BezierLine.prototype = $.extend({}, Component.prototype);
        BezierLine.prototype = $.extend(BezierLine.prototype, {
            render: function (options) {
                this.properties.typeName = "曲线";
                this.properties.strokeWidth = 2;
                this.properties.strokeColor = 'red';
                this.properties=$.extend(this.properties,options)
                this.properties.x = Math.min(this.properties.sxy.x, this.properties.txy.x);
                this.properties.y = Math.min(this.properties.sxy.y, this.properties.txy.y);
                this.properties.width = Math.abs(this.properties.txy.x - this.properties.sxy.x);
                this.properties.height = Math.abs(this.properties.txy.y - this.properties.sxy.y);
    
    
                var wire = new paper.Path(this.calcPath(this.properties.targetType, this.properties.sxy.x, this.properties.sxy.y, this.properties.txy.x, this.properties.txy.y));
                wire.strokeWidth = this.properties.strokeWidth;
                wire.strokeColor=this.properties.strokeColor;
                wire.sendToBack();
                this.group=new paper.Group();
                this.group.addChild(wire);
                //this.group.translate(this.properties.x, this.properties.y);
                return this;
            },
            calcPath:function(type, x1, y1, x2, y2)
                {
                var path= "";
                if(type =="left" || type == "right")
                path= 'M ' + x1 + ', ' + y1 + 'C ' +
                (x1 + (x2 - x1) / 2) + ', ' + y1 + ' ' +
                (x2 - (x2 - x1) / 2) + ', ' + y2 + ' ' +
                x2 + ', ' + y2;
                else if (type=="up" || type == "down")
                path='M' + x1 + ', ' + y1 + 'C ' +
                x1 + ', ' + (y1 + (y2 - y1) / 2) + ' ' +
                x2 + ', ' + (y2 - (y2 - y1) / 2) + ' ' +
                x2 + ', ' + y2;
                return path;
            }
        });

    最后效果图如下:

     

    源代码:sample.1.5.rar

    直接运行查看

    (本文为原创,在引用代码和文字时请注明出处)

  • 相关阅读:
    二级缓存
    java面试题
    BRD,MRD,PRD文档
    程序做处理时,try..catch和if..else的区别
    关于java中字符串截取
    导出oracle数据库表(备份表)操作命令
    WSDL文件生成java类
    Linux下redis安装(单机版)
    Springboot2.0访问Redis集群
    Spring Cloud Sleuth通过Kafka将链路追踪日志输出到ELK
  • 原文地址:https://www.cnblogs.com/coolalam/p/9644645.html
Copyright © 2011-2022 走看看