天天看點

如何建構一個線上繪圖工具:Feakin 是如何設計與建構的?

高中,讀過幾本 3D 圖形程式設計相關的書。怎麼說呢,自那以後,圖形學相關的東西,都不在我的興趣範圍裡了。直到最近,我重新燃起了一點興趣:

  • 架構治理工具 ArchGuard 依賴于「圖即代碼」,用于生成架構圖,以更好的進行架構治理。
  • 年初,開源的知識管理工具 Quake 中,需要支援「概念建構系統」這樣一個理念。
  • 需要管理多種不同的圖形格式。

當然了,作為一個 Firefox 浏覽器的忠實使用者,Firefox 在 Feakin 裡自然是支援最好的。開始之前,歡迎嘗試線上 Demo:https://online.feakin.com/ , GitHub:https://github.com/feakin/feakin/,當然了 Bug 超級多。

引子:開源繪圖工具實作的淺析

在設計 Feakin 的時候,參考了一些幾個常用的圖形工具。分析了它們的大緻實作,以及部分的源碼:

Graphviz

AT&A 實驗室的作品,作為最古老的圖形即代碼的工具,它還提供了一個圖形描述語言:Dot,可以直接将代碼轉換為圖形。它的生态體系足夠的完善,是以你在哪都能看到它的影子。

Mermaid

同樣也是一個圖形即代碼的工具,使用的是純 JavaScript 實作,從文法解析到圖形渲染。Mermaid 使用 Jison 作為解析器,然後将其轉換為不同的圖模型,如流、時序等,再使用 graphlib、dargre 進行布局,最後使用 dagre-d3、d3 進行渲染。是以,在 Mermaid 裡有三個核心要素:文法解析、圖形布局、圖形渲染。而,Mermaid 不存在一個圖形模型,也變成了一個神奇的存在。

Cytoscape

第一次看到這個圖形引擎的時候,是看到 ArchGuard 前人留下的一個功能:布局算法切換。是以,在源碼實作上,Cytoscape 提供了這種算法上的擴充性,具體可以看官方網站。布局上的抽象,提供了更好的可擴充性 —— 我們就可以參考它的實作了。在它的圖形模型裡,Node(節點) 和 Edge(邊) 從形式上都算是 Element,然後在渲染時根據圖形類型展開。于是在渲染時,直接采用 HTML5 裡的 Canvas 進行繪制即可。

Excalidraw

對我來說,其最有意思的是引入了射影幾何,來進行節點變化時的,自動 Edge 跟蹤;即當 A 從 B 的左邊移動到右邊時,對應的線自動連接配接到 B 右邊的邊上。當然了,其中的各種神奇算法,我也沒看懂。對于其他人,可能就是使用 roughjs 來生成手繪風格的圖。當然了, 就目前的代碼實作來說,roughjs 在 renderElement 裡過度的耦合,圖形模型也耦合在其中。

MaxGraph

MaxGraph 是 Draw.io 底層的 mxGraph 的 TypeScript 實作,最開始研究時,是為了導入 Draw.io 生成的圖。從模型上來說,MaxGraph 應該是幾個工具裡做得最好的,包含一系列的可參考的 Shape、Edge 等等。其次,也提供了 AbstractCanvas2D 這樣的實作,雖然它沒有實作真正的 HTML5 Canvas2D,但是抽象接口已經非常像了,諸如 

.moveTo

、 

.lineTo

 等。可能它就提供 SVG 和 XML,前者用于網頁渲染,後者用于導出。

是以,從上述的幾個工具裡,我們就能得到一個繪圖工具底層的基本要素:

  • 圖形模型。即對圖形模組化,理清 Diagram/Graph、Node、Edge、Shape、Element 之間的關系,并包含基本的圖形表示關系。
  • 圖形繪制。即定義如何對圖形進行繪制/渲染,如采用 SVG、Canvas 等不同的形式。

為了豐富這些功能:

  • 布局算法。提供自動化的布局方式,如 Cytoscape 這一類自動計算的方式。
  • 文法解析。諸如于為了支援圖即代碼(即 DSL)的形式來提供快捷的繪制方式。
  • 自動連線。即如 Excalidraw、Draw.io 中提供的功能,兩者實作的方式完全不一樣。
  • 圖形風格。諸如于 Excalidraw 提供的手繪圖形的功能。
  • 圖形庫。這也是 Drawio 最受歡迎的地方,也是 Excalidraw 一個很有意思的功能。
  • 等等

結合這些功能,我們就可以造出一些有意思的東西,比如 Feakin 中的二階段渲染。

Step 1:實作第一個概念證明

為了 Feakin 能進行下去,我們所要做的就是快速實作一個 PoC(概念證明)。在這個 PoC 裡,主要實作如下的功能:

  • DSL (領域特定語言)解析。
  • 圖形模型生成。
  • 圖形繪制。

如下圖所示:

如何建構一個線上繪圖工具:Feakin 是如何設計與建構的?

這樣一來,我們就有一個「It works」了。

從圖形引擎的誤區中出來

在實作第一個 PoC 的時候,遇到的第一個困難是技術選型,到底是:SVG 還是 Canvas?SVG 可以友善于我們進行 TDD(測試驅動開發),隻要所有的測試是通過的,理論上結果就是過的。但是,如我們所看到的那樣,SVG 容易遇到性能瓶頸。Canvas 提供自由的繪制 API,測試時依賴于快照測試(snapshot),不易于編寫測試。是以,結論就是:我們都要了吧。隻需要像 MaxGraph 提供一個抽象圖形接口,我們就能實作對于兩種模式的支援。

随後,發現這樣是不合理的,隻在 PoC 階段,并且沒有經驗的情況下,做一個 AbstractCanvas 還是存在很高的成本。于是乎,需要尋找一個合理的繪制引擎,諸如于 Raphaël、Fabric、Konva 等。最後,選擇了 Konva,因為它支援了 React 架構。正所謂,工作用 Angular 心不累,業餘用 React 放我自我。

原型:文法解析-圖形模型-圖形繪制

在建構了基本的圖形領域的相關知識之後,要建構出一個繪圖工具并不困難。

  • 參考(複制) Mermaid 的文法解析。将通過 parser 解析類似于 Graphviz、Mermaid 設計的文法,将其轉換為圖形模型。
  • 引入 Dagre.js 作為圖形布局引擎。通過 Dagre.js 來計算布局,傳回我們所需要的圖形模型。
  • 使用 React Konva 進行渲染。将圖形模型比對到 Konva 中的圖形,如 RectangleShap 對應于 

    <Rect>

     元件、Edge 對應于 

    <Line>

    、 

    <Arrow>

    等。

過程中,遇到的一個比較坑的點是:Lerna + Nx.js 管理 monorepo。React + Craco 的組合、風格各異的代碼庫,帶來了持續失敗的 CI,還好 GitHub Action 不會統計失敗率。持續內建不來點失敗,怎麼能發揮它的用處呢。

Step 2:對模型進行反複重構(持續)

在 Poc 裡,我們需要遇到不同的模型轉換:

  • 解析器獲得的模型。包含節點的資訊,以及節點的關系(諸如于 A 到 B、A 依賴于 B 等)。
  • 布局引擎生成的模型。通常來說,隻是補充一下模型裡的層次關系(children/parent)、坐标資訊(x、y)、幾何資訊(width、height)等。
  • 圖形繪制引擎的模型。我們需要将上述的資訊,再次轉換到 Konva 的模型中。而其中會存在一些差距,比如 Konva 使用 Polygon(多邊型)來表示Triangle(三角型)、Diamond(菱形)等。

是以,如何設計一個有用的模型,成為了個有意思的問題。

GIM:圖中間模型

在那一篇《圖的抽象:概念與模型的建構》中,我們介紹了從認知語義學的角度,如何僅憑基本的概念,設計出可用的模型?不過,這樣的模型是未經驗證的。那麼,什麼樣的模型是經常驗證的呢?自然是開源社群中,已經充分使用的代碼模型。雖然說,各個模型受限于自己的場景,與其他軟體的模型存在一定的差距。但是呢,在基本的核心概念圖的表示上,它們是大差不差的。于是乎,我們有了一個 GIM(Graph Intermedia Model),圖中間模型。

這個圖模型的來源是源自其他圖形工具成熟的模型,如下圖所示:

如何建構一個線上繪圖工具:Feakin 是如何設計與建構的?

是以,在持續的模組化、提煉之後,我們可以輕松地進行我們的圖模型轉換。在有了 TDD 的加持之後,這個過程就更加地簡單了。

在模型這一點上,Feakin 的設計初衷與 ArchGuard 底層的 Chapi (https://github.com/modernizing/chapi) 語言模型的想法是一緻的。而這種所謂的通用模型會遇到的問題是,需要抛棄一些細節,諸如于隻實作 80% 的核心功能。

圖的模型

對于一個圖(Graph)來說,它的模型也就變得相當的簡單:

export interface Graph {nodes: Node[];edges: Edge[];props?: GraphProperty;subgraphs?: Graph[];}           

複制

圍繞于這四個核心元素再往下展開:

  • 節點(Node)。主要包含坐标資訊,形态資訊等,可以用于建構出不同的 shape。
  • 邊(Edge)。主要包含點(Point),可以用于建構普通的直線、貝塞爾曲線(Bézier)曲線等,還有
  • 屬性(Props)。這裡隻屬于命名為 props 是為了對齊 🐶 ,對應于圖形屬性,諸如于 fillColor、color、strokeColor 等。
  • 子圖(Graph[])。一個抽象的概念,在不同的圖示中有不同的形式,如 Group、子集等。

而如上所述 Shape 和 Edge 就是兩個大家庭,包含了一系列的子類,諸如于 Shape 包含了 PolygonShape(又包含了 TriangleShape、DiamondShape、RectangleShape 、HexagonShape)。

狀态與屬性(TBD)

對于屬性來主,尚未進一步展開,但是初步分為:FillState、FontState、StrokeState、ImageState 等。

如果你也感興趣的話,歡迎一起來設計。

Step 3:核心特性基礎:二階段繪圖

在反複的設計了各種 Importer/Exporter 之後,并持續不斷的進行模型重構之後,就構成了的核心特性的基礎:二階段繪圖。簡單來說,就是把繪圖分為了兩階段:

  • 通過 DSL 生成圖或者導入生成圖。
  • 使用圖形工具對生成的圖進行編輯。

以在不同的工具之間轉換,并實作圖的互轉。

二階段繪圖示例

在這裡就可以嘗試使用:https://online.feakin.com/ ,雖然還隻是一個早期的版本,仍舊還有一系列的 bug,但是還可以嘗試的。

如何建構一個線上繪圖工具:Feakin 是如何設計與建構的?

如上圖所示,我們可以

  1. 通過 File → Import 導入 Draw.io 或者 Excalidraw,又或者是通過 Graphviz 的 Dot 文法編寫。
  2. 通過 Export 導出到 Draw.io 或者是 Excalidraw

圖中,左邊的編輯器是使用 Monaco Editor,配合了簡單的 Dot 文法支援;右邊則是一個早期版本的 Feakin Render,隻能簡單地渲染一下,看看效果。目前,僅支援簡單的拖拉拽,還容易出錯。

決策:過程一緻或者結果一緻?

在這個過程中,還有一系列有意思的東西,比如 Shape 在不同的圖形工具是不一樣的。先讓我們看個代碼示例:

digraph G {a [shape="triangle"];b [shape="diamond"];a -> b;}           

複制

也是截圖中的代碼簡化,節點 a 的 shape 是一個 Triangle(三角形),然而:

  • 在 Excalidraw 中不存在三角形,需要自己用 Line 繪制一個。
  • 在 Draw.io 中預設的 Triangle 和正确的三角形不一樣,正确的類似應該是 

    mxgraph.basic.acute_triangle

     。

于是乎,為了結果上的一緻,我們需要在對應的 ExcalidrawExporter、DrawioExporter 進行對應的 Shape 的處理和 Mapping。

Step 4:從 MVP 到真實世界

在這個 MVP(最小可行性産品)裡,我們所建構的隻是一個可以工作的原型,依舊有一系列的工作要完成。諸如于:

更豐富的圖形

目前隻支援基礎的圖形,在未來,支援其它工具的圖形庫 —— 有了 GIM,我們就不需要自己設計了。

圖形的屬性

從顔色到邊框,一個功能也沒有。難點主要在于,如何進行對應的屬性抽象。在 MaxGraph 是一個胖模型,這種模型不利于維護,會帶來額外的知識負載,它還是按字母順序排序的,頭疼。

生态相容性

諸如于,雖然我們能成功導出 Excalidraw 的圖形,也可以實作模型之間的綁定。但是,在一些屬性上還是有差別的。

當然,這也是其中非常有意思的地方 —— 原來你隻需要寫一份未經驗證的代碼,現在你要看 N 份。

下一步:遠端多人協作

既然,我們将代碼作為第一等公民,那麼實作代碼的遠端協作,也就是這個工具非常有意思的地方。一提到代碼的多人協作,我就想起了我熟知的 Intellij IDEA。作為一個熟悉 Intellij IDEA Community 源碼的人,我就聯想到了 Fleet 架構裡的 Rope Architecture Model 與 State Management 兩篇相關文章。大體是關于如何使用 Rope 模型來管理 AST(抽象文法樹),以及如何管理多人協作的狀态問題。

除此,之前讀過的 Xi Editor,也有關于 Rope 模型也有很好的介紹:https://xi-editor.io/docs.html。它提供了一個很好的 Rust 實作,這樣一來,我們就可以使用 Rust 來開發 Feakin 的協作部分。

如果你也有興趣,歡迎一起來用愛發電。如此一來,也不枉我花三個小時寫的這篇文章。

線上 Demo:https://online.feakin.com/

GitHub:https://github.com/feakin/feakin/

如何建構一個線上繪圖工具:Feakin 是如何設計與建構的?