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

直接運行查看

(本文為原創,在引用代碼和文字時請注明出處)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM