貝塞爾曲線(Bezier curve)實現節點連線

wls1036發表於2020-09-25

背景

長久以來都想找一個畫流程圖的工具,有幾個需求,可以將元件拖到繪圖皮膚中,並且元件間可以透過線進行關聯,在屬性皮膚可以配置元件的屬性,這裡的元件可能是html的元件,也可能是一個功能,為什麼需要這麼一個東西呢?如果有這東西,很多想法就可以實現,比如

  • 工作流
  • 服務編排

但一直找不到滿意的框架,很大原因就是這些框架顏值太低,直到遇到NodeRed,NodeRed是物聯網開發工具,提供視覺化介面透過配置就能實現物聯網程式的開發,讓我感興趣的是NodeRed視覺化介面,顏值高,操作便利,簡潔不花哨

因為NodeRed是開源的,也花了一段時間去研究他的程式碼,想把視覺化工具抽離出為我所用,種種原因失敗了,既然抽離不出來,能不能自己實現一個?之前從來沒想過自己開發一個,但細想覺得並不難實現,視覺化介面核心有兩點

  • 元件的繪製
  • 連線線的繪製

整個介面都是透過svg相關標籤實現,元件的繪製相對比較簡單,就是一些svg元素組合,卡在了連線線這裡,因為NodeRed節點連線線操作平滑,曲線合理,一開始以為是用很複雜的演算法實現的,在原始碼裡去找這個演算法,其實是把問題想複雜了,這個就是一條普通的貝塞爾曲線,既然知道了實現的原理,自己做一個也不難,這一篇部落格就介紹如何用貝塞爾曲線實現節點連線。

貝塞爾曲線

我本身不是數學專業,對貝塞爾曲線在數學上的原理我也不懂,我覺得也沒必要弄懂,就當作一個工具用,貝塞爾曲線就是可以實現兩個點平滑的進行連線的一個工具,貝塞爾曲線又四個點進行控制,一個起點一個終點,兩個控制點,透過對控制點的調整可以實現曲線像皮筋一樣變形,達到我們想要的效果。

這裡有個很棒的網站 可以進行線上繪製貝塞爾曲線

實現

從上面可以看出,兩個控制點的位置決定了曲線的路徑,所以這兩個點的位置很重要,我檢視了NodeRed的實現,NodeRed是分別選取距離起點水平75px和終點75px兩個點作為控制點,以下圖為例

兩個節點之間距離長是200px,寬是160px,曲線的svg程式碼如下

<path class="link_line link_path" d="M 340 260 C 415 260 465 100 540 100"></path>

M代表起點(340,260),C後面跟著兩個控制點(415,260),(465,100)最後(540,100)是終點,第一個控制點和起點Y是相同的,說明在同一個水平面上,x相距415-340=75,第二個控制點和終點也是Y相同,x相距540-465=75,就算改變元件位置,這個長度依然不會變,效果和下面這個是一樣的

既然知道了規則,我們就自己實現一個,以下是距離75px的效果,基本和NodeRed一樣

參考程式碼:

<html>
<body>
<svg id="svg" width='100%' height='100%' style="background-color: #f5f5df">
</svg>
</body>
<script type="text/javascript">
    var startX = 0;
    var startY = 0;
    var drawable = false;
    document.addEventListener('mousedown', function (event) {
        startX = event.clientX;
        startY = event.clientY;
        drawable = true;
    });
    document.addEventListener('mouseup', function (event) {
        drawable = false;
    })
    document.addEventListener('mousemove', function (event) {
        if (drawable) {
            let x1 = startX;
            let y1 = startY;
            let level=75;
            let c1x = x1 + level;
            let c1y = y1;
            let c2x = event.clientX - level;
            let c2y = event.clientY;
            let path = '<path d=\'M ' + x1 + ' ' + y1 + 'C ' + c1x + ' ' + c1y + ' ' + c2x + ' ' + c2y + ' ' + event.clientX + ' ' + event.clientY + '\' style="stroke-width:3;stroke: purple;fill:none"></path>'
            let p1 = '<path d="M ' + x1 + ' ' + y1 + ' L' + ' ' + (c1x - 3) + ' ' + c1y + '" style="stroke-width: 3;stroke: red;stroke-dasharray: 2 1;"></path>';
            let p2 = '<path d="M ' + event.clientX + ' ' + event.clientY + ' L' + ' ' + (c2x - 3) + ' ' + c2y + '" style="stroke-width: 3;stroke: red;stroke-dasharray: 2 1;"></path>';
            let c1 = '<circle cx="' + c1x + '" cy="' + c1y + '" r="5" stroke="red" stroke-width="2" fill="red" stroke-dasharray: 2 1;/>';
            let c2 = '<circle cx="' + c2x + '" cy="' + c2y + '" r="5" stroke="red" stroke-width="2" fill="red" stroke-dasharray: 2 1;/>';
            let p3 = path + p1 + p2 + c1 + c2;
            document.getElementById("svg").innerHTML = p3;
        }
    })
</script>
</html>

可以嘗試更改level檢視不同長度的效果,以下是距離一半的效果

就修改了level的長度為距離的一半

...
let off = Math.abs(event.clientX - x1);
let level = off / 2;
...

總結

後續我們也會討論如何實現節點拖拽

相關文章