[譯] 使用 SVG 和 Vue.Js 構建動態樹圖

僱個城管打天下發表於2019-07-12

使用 SVG 和 Vue.Js 構建動態樹圖

本文將會帶你瞭解到我是如何建立一個動態樹圖的,該圖使用 SVG(可縮放向量圖形)繪製三次貝塞爾曲線(Cubic Bezier)路徑並通過 Vue.js 以實現資料響應。

在開始前,先讓我們來看一個 demo

[譯] 使用 SVG 和 Vue.Js 構建動態樹圖

基於 SVG 和 Vue.js 框架的強大功能,我們可以輕鬆建立基於資料驅動、可互動和可配置的圖表與資訊圖。

該圖是一個三次貝塞爾曲線的集合,它基於使用者提供的資料,從單點出發,並在不同的點結束,且點和點之間的距離相同。 因此,該圖會響應使用者輸入的內容。

我們將首先學習如何製作三次貝塞爾曲線,然後通過剪下蒙版在座標系中嘗試找到 <svg> 元素可用的 xy 點。

我在這個案例中使用了很多視覺動畫以保證趣味性。本文的主要思想是幫助你為類似的專案設計出自己的圖表。

SVG

Cubic Bezier 曲線是如何形成的?

你在上面的 demo 中看到的曲線被稱為三次貝塞爾曲線。我已在下面高亮顯示了此曲線結構的每個部分。

[譯] 使用 SVG 和 Vue.Js 構建動態樹圖

它總共有 4 對座標。第一對座標 —— (x0, y0) —— 是起始錨點,最後一對座標 —— (x3, y3) —— 是結束錨點,指示完成路徑的位置。

中間的兩對座標是:

  • 貝塞爾控制點 #1 (x1, y1)
  • 貝塞爾控制點 #2 (x2, y2)

基於這些點實現的路徑是一條平滑曲線。如果沒有這些控制點,這條路徑就是一條筆直的線!

讓我們把這四個座標放入 SVG 語法的 <path> 元素中。

// 三次貝塞爾曲線的路徑語法

<path D="M x0,y0  C x1,y1  x2,y2  x3,y3" />
複製程式碼

語法中的字母 c 代表三次貝塞爾曲線。小 c 表示相對值,而大寫 C 表示絕對值。我用絕對值 C 來建立這個圖。

實現對稱性

對稱性是實現該圖的關鍵點。為了實現這一點,我只使用一個變數來派生出類似於高度,寬度和中點等值。

就讓我們把這個變數命名為 size 吧。由於此樹形圖的方向是水平的,因此可以將變數 size 視為整張圖的水平空間。

讓我們為這個變數賦予實際值。這樣,你還可以計算路徑的座標。

size = 1000
複製程式碼

尋找座標

在我們尋找座標前,我們需要新建一個座標系!

座標系和 viewBox

<svg> 元素的 viewBox 屬性非常重要,因為它定義了 SVG 的使用者座標系。簡而言之,viewBox 定義了使用者空間的位置和維度以便於繪製 SVG。

viewBox 由四個數字組成,順序需要保持一致 —— min-x, min-y, width, height

<svg viewBox="min-x min-y width height">...</svg>
複製程式碼

我們之前定義的 size 變數將控制此座標系的 widthheight

稍後在 Vue.js 部分,viewBox 將繫結到計算屬性以填充 widthheight,而 min-xmin-y 在此例項中始終為零。

請注意,我們沒有使用 SVG 元素本身的 widthheight 屬性。因為,我們稍後會通過 CSS 設定 <svg>width: 100%height: 100%,以便自適應填滿整個 viewport。

現在整張圖的使用者空間 / 座標系已準備好,讓我們看看 size 變數如何通過使用不同的 % 值來幫助計算座標。

恆定和動態座標

Diagram Concept

圓是圖的一部分。這就是為什麼從一開始就把它包含在計算中是很重要的。如上圖所示,讓我們開始匯出一個一個樣本路徑的座標值。

垂直高度分為兩部分:topHeightsize 的 20%)和 bottomHeightsize 剩餘的 80%)。水平寬度分為兩部分 —— 分別是 size 的 50%

這樣圓座標(halfSize, topHeight)就顯而易見了。圓的 radius 屬性設定為 topHeight 的一半,這樣的可用空間非常合適。

現在,讓我們看一下路徑座標……

  • x0, y0 —— 第一對錨點始終保持不變。這裡,x0 是圖表 size 的中心,y0 是圓圈停止的垂直點(因此增加了一個 radius)並且是路徑的起點。
    =(50% 的 size, 20% 的 size + radius)
  • x1, y1 —— 貝塞爾控制點 1,對於所有路徑也保持不變。考慮到對稱性,x1y1 總是圖表 size 的一半。 = (50% 的 size, 50% 的 size)
  • x2, y2 —— 貝塞爾控制點 2,其中 x2 指示哪一側形成曲線並且為每條路徑動態計算。同樣,y2 是圖表 size 的一半。
    = (x2, 50% 的 size)
  • x3, y3 —— 最後一對錨點,指示路徑繪製結束的位置。這裡,x3 模仿 x2 的值,這是動態計算的。y3 佔據了 size 的 80%。
    = (x3, 80% 的 size)

在合併上述計算結果後,請參閱下面的通用路徑語法。為了表示 %,我只是簡單的將 % 值除以 100。

<path d="M size*0.5, (size*0.2) + radius  
         C size*0.5,  size*0.5
           x2,        size*0.5
           x3,        size*0.8"
>
複製程式碼

注意:整個程式碼邏輯中 % 的選擇最初看起來似乎全是主觀推斷,但它是為了實現對稱而選擇的正確比例。一旦你瞭解了構建此圖表的目的,你就可以嘗試自己的 % 值並檢查不同的結果。

下一部分重點是找到剩餘座標 x2x3 的值 —— 這使得能夠根據它們的陣列索引動態地形成多個彎曲路徑。

根據陣列中的多個元素,可用的水平空間應分配到相等的部分,以便每個路徑在 x-axis 上獲得相同的空間量。

公式最終應適用於任意數量的專案,但出於本文的目的,我已經使用了 5 個陣列項 —— [0,1,2,3,4]。意思是,我將繪製 5 條貝塞爾曲線。

尋找動態座標(x2 和 x3)

首先,我將 size 除以元素數,即陣列長度,並命名為 distance —— 作為兩個元素之間的距離。

distance = size/arrayLength
// distance = 1000/5 = 200
複製程式碼

然後,我迴圈遍歷陣列中的每個元素,並將其 index 值乘以 distance。 為了描述簡單,我用 x 表示 x2x3

// value of x2 and x3
x = index * distance
複製程式碼

當我使用 x 的值來表示 x2x3 時,這張圖看起來有點奇怪。

[譯] 使用 SVG 和 Vue.Js 構建動態樹圖

如你所見,座標的位置是正確的,但不是很對稱。左側的元素看起來比右側的元素多。

此時因為一些原因,我需要將 x3 座標放在 distance 的中心,而不是在一開始的地方。

為了解決這個問題,讓我們重新審視下變數 distance —— 對於給定的場景,它的值是 200。我只是給 x 又加了 distance 的一半

x = index * distance + (distance * 0.5)
複製程式碼

上式意思是,我找到了 distance 的中點並將最終的 x3 座標放在那裡,並調整了貝塞爾曲線 #2 的 x2

[譯] 使用 SVG 和 Vue.Js 構建動態樹圖

x2x3 座標中新增 distance 的一半,適用於陣列的奇數項和偶數項元素。

圖層蒙版

為了使蒙版形狀為圓形,我已經在 mask 元素中定義了一個 circle

<defs>
  <mask id="svg-mask">
     <circle :r="radius" 
             :cx="halfSize" 
             :cy="topHeight" 
             fill="white"/>
  </mask>
</defs>
複製程式碼

接下來,使用 <svg> 元素中的 <image> 標籤作為內容,我使用 mask 屬性將影像繫結到 <mask> 元素裡(已在上述程式碼中建立)。

<image mask="url(#svg-mask)" 
      :x="(halfSize-radius)" 
      :y="(topHeight-radius)"
...
> 
</image>
複製程式碼

由於我們試圖將方形影像擬合成圓形,我通過減小圓的 radius 來調整影像位置,以通過圓形蒙版實現影像的完全可見性。

讓我們將所有的值都放入圖表中,以幫助我們看到完整的影像。

[譯] 使用 SVG 和 Vue.Js 構建動態樹圖

使用 Vue.js 的動態 SVG

到目前為止,我們已經瞭解了貝塞爾曲線的本質,以及它的工作原理。因此,我們有了靜態 SVG 圖的概念。使用 Vue.js 和 SVG,我們現在將用資料驅動圖表,並將其從靜態轉換為動態。

在本節中,我們將把 SVG 圖分解為 Vue 元件,並將 SVG 屬性繫結到計算屬性,並使其響應資料更改。

最後,我們還將檢視配置皮膚元件,該元件用於向動態 SVG 圖提供資料。

我們將在本節中瞭解以下關鍵主題。

  • 繫結 SVG viewBox
  • 計算 SVG 路徑座標
  • 實現貝塞爾曲線路徑的兩個選項
  • 配置皮膚
  • 家庭作業 ❤

繫結 SVG viewBox

首先,我們需要一個座標系統才能在 SVG 內部繪製。 計算屬性 viewbox 將使用 size 變數。它包含由空格分隔的四個值 —— 它被送入 <svg> 元素的 viewBox 屬性。

viewbox() 
{
   return "0 0 " + this.size + " " + this.size;
}
複製程式碼

在 SVG 中,viewBox 屬性已經使用駝峰命名法(camelCase)。

<svg viewBox="0 0 1000 1000">
</svg>
複製程式碼

因此為了正確繫結上計算屬性,我在 .camel 修飾符後對該變數使用了短橫線命名(kebab-case)的方式(如下所示)。通過這種方式,HTML 才得以正確繫結此屬性。

現在,每次我們更改 size 時,圖表都會自行調整,而無需手動更改標記。

計算 SVG 路徑座標

由於大多數值都是從單個變數 size 派生的,所以我已經為所有常量座標使用了計算屬性。不要被這裡的常量混淆。這些值是從 size 中派生出來的,但在之後,無論建立多少曲線路徑,它們都保持不變。

如果你改變 SVG 的大小,這些值會再次被計算出來。考慮到這一點,這裡列出了繪製貝塞爾曲線所需的五個值。

  • topHeight — size * 0.2
  • bottomHeight — size * 0.8
  • width — size
  • halfSize — size * 0.5
  • distance — size/arrayLength

此時,我們只剩下兩個未知值,即 x2x3,我們有一個公式可以確定它們的值。

x = index * distance + (distance * 0.5)
複製程式碼

為了找到上面的 x,我們需要一次將 index 輸入到每個路徑的公式中。所以……

在這使用計算屬性合適嗎?肯定不合適。

我們不能將引數傳遞給計算屬性 —— 因為它是一個屬性,而不是函式。另外,需要一個引數來計算意味著——使用計算屬性對快取也沒什麼好處。

注意:上面有一個例外,Vuex。如果我們正在使用 Vuex Getters,那麼,我們可以通過返回一個函式將引數傳遞給 getter。

在本文所述的情況下,我們不使用 Vuex。可即便如此,我們仍有兩個選擇。

選擇一

我們可以定義一個函式,在這裡我們將陣列 index 作為引數傳遞並返回結果。如果要在模板中的多個位置使用此值,選擇 Bit cleaner

<g v-for="(item, i) in itemArray">
  <path :d="'M' + halfSize + ','         + (topHeight+r) +' '+
            'C' + halfSize + ','         + halfSize +' '+    
                  calculateXPos(i) + ',' + halfSize +' '+ 
                  calculateXPos(i) + ',' + bottomHeight" 
  />
</g>
複製程式碼

calculateXPos() 方法將在每次呼叫時進行評估。並且此方法接受索引 —— i —— 作為引數(程式碼如下)。

<script>
  methods: {
    calculateXPos (i)
    {
      return distance * i + (distance * 0.5)
    }
  }
</script>
複製程式碼

下面是執行在 CodePen 上 Option 1 的結果。

Option 1 - Bezier Curve Tree Diagram with Vue Js

選擇二

更好的是,我們可以將這個小的 SVG 路徑標記提取到它自己的子元件中,並將 index 作為一個屬性傳遞給它 —— 當然,還有其他必需的屬性。

在這個例子中,我們甚至可以使用計算屬性來查詢 x2x3

<g v-for="(item, i) in items"> 
    <cubic-bezier  :index="i" 
                   :half-size="halfSize" 
                   :top-height="topHeight" 
                   :bottom-height="bottomHeight" 
                   :r="radius"
                   :d="distance"
     >
     </cubic-bezier>
</g>
複製程式碼

這種方法可以讓我們的程式碼更具條理,例如,我們可以為一個圓形剪下蒙版建立一個或多個子元件,如下所示。

<clip-mask :title="title"
           :half-size="halfSize" 
           :top-height="topHeight"                     
           :r="radius"> 
</clip-mask>
複製程式碼

配置皮膚

Config Panel

您可能已經在 CodePen 左上角看到了 控制皮膚。它可以新增和刪除陣列中的元素。在 Option 2 中,我建立了一個子元件來容納 Config Panel,使頂級 Vue 元件清晰可讀。我們的 Vue 元件樹看起來就像下面這樣。

[譯] 使用 SVG 和 Vue.Js 構建動態樹圖

想知道 Option 2 的程式碼是什麼樣子的?下面的連結是在 CodePen 上使用了 Option 2 的程式碼。

Option 2 - Bezier Curve Tree Diagram with Vue Js

GitHub 倉庫

最後,這裡有一個為你準備的 GitHub Repo,你可以在進入下一部分之前檢視該專案(使用選項 2)。

家庭作業

嘗試基於本文中介紹的邏輯在垂直模式下建立相同的圖表。

如果你認為,它是交換座標系中的 x 值和 y 值一樣簡單的話,那麼你是對的!因為最艱難的部分已經完成,在交換了所需的座標後,再用適當的變數和方法更新程式碼。

在 Vue.js 的幫助下,該圖可以通過更多功能進一步擴充套件,例如,

  • 建立一個開關以便於在水平和垂直模式之間切換
  • 可以使用 GSAP 為路徑設定動畫
  • 從配置皮膚控制路徑屬性(例如顏色和筆觸寬度)
  • 使用第三方工具庫將圖表儲存並下載為影像/PDF

現在試一試,如果需要的話,下面是家庭作業的答案連結。

祝你好運!

總結

<path> 是 SVG 中眾多強大的元素之一,因為它允許你精確地建立圖形和圖表。在本文中,我們瞭解了貝塞爾曲線的工作原理以及如何建立一個自定義圖表應用。

利用現代 JavaScript 框架所使用的資料驅動方法進行調整總是令人生畏的,但 Vue.js 使它變得非常簡單,並且還可以處理諸如 DOM 操作之類的簡單任務。因此,作為一名開發人員,即使在處理具有明顯視覺效果的專案時,你也可以用資料的方式進行思考。

我已經意識到建立這個看起來很複雜的圖表需要 Vue.js 和 SVG 的一些簡單概念。如果你還沒有準備好,我建議您閱讀有關使用 Vue.js 構建互動式資訊圖的內容。讀完那篇文章後再回過頭閱讀本文就會容易很多。❤這是家庭作業的答案

我希望你從這篇文章中學到了一些東西,並在閱讀本文時能夠感受到我當時創作時的樂趣。

如果發現譯文存在錯誤或其他需要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可獲得相應獎勵積分。文章開頭的 本文永久連結 即為本文在 GitHub 上的 MarkDown 連結。


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章