高中,读过几本 3D 图形编程相关的书。怎么说呢,自那以后,图形学相关的东西,都不在我的兴趣范围里了。直到最近,我重新燃起了一点兴趣:
欢迎尝试在线 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,前者用于网页渲染,后者用于导出。
所以,从上述的几个工具里,我们就能得到一个绘图工具底层的基本要素:
为了丰富这些功能:
结合这些功能,我们就可以造出一些有意思的东西,比如 Feakin 中的二阶段渲染。
为了 Feakin 能进行下去,我们所要做的就是快速实现一个 PoC(概念证明)。在这个 PoC 里,主要实现如下的功能:
这样一来,我们就有一个「It works」了。
在实现第一个 PoC 的时候,遇到的第一个困难是技术选型,到底是:SVG 还是 Canvas?SVG 可以方便于我们进行 TDD(测试驱动开发),只要所有的测试是通过的,理论上结果就是过的。但是,如我们所看到的那样,SVG 容易遇到性能瓶颈。Canvas 提供自由的绘制 API,测试时依赖于快照测试(snapshot),不易于编写测试。所以,结论就是:我们都要了吧。只需要像 MaxGraph 提供一个抽象图形接口,我们就能实现对于两种模式的支持。
随后,发现这样是不合理的,只在 PoC 阶段,并且没有经验的情况下,做一个 AbstractCanvas 还是存在很高的成本。于是乎,需要寻找一个合理的绘制引擎,诸如于 Raphaël、Fabric、Konva 等。最后,选择了 Konva,因为它支持了 React 框架。正所谓,工作用 Angular 心不累,业余用 React 放我自我。
在构建了基本的图形领域的相关知识之后,要构建出一个绘图工具并不困难。
<Rect>
组件、Edge 对应于 <Line>
、 <Arrow>
等。过程中,遇到的一个比较坑的点是:Lerna + Nx.js 管理 monorepo。React + Craco 的组合、风格各异的代码库,带来了持续失败的 CI,还好 GitHub Action 不会统计失败率。持续集成不来点失败,怎么能发挥它的用处呢。
在 Poc 里,我们需要遇到不同的模型转换:
所以,如何设计一个有用的模型,成为了个有意思的问题。
在那一篇《图的抽象:概念与模型的构建》中,我们介绍了从认知语义学的角度,如何仅凭基本的概念,设计出可用的模型?不过,这样的模型是未经验证的。那么,什么样的模型是经常验证的呢?自然是开源社区中,已经充分使用的代码模型。虽然说,各个模型受限于自己的场景,与其他软件的模型存在一定的差距。但是呢,在基本的核心概念图的表示上,它们是大差不差的。于是乎,我们有了一个 GIM(Graph Intermedia Model),图中间模型。
所以,在持续的建模、提炼之后,我们可以轻松地进行我们的图模型转换。在有了 TDD 的加持之后,这个过程就更加地简单了。
在模型这一点上,Feakin 的设计初衷与 ArchGuard 底层的 Chapi (https://github.com/modernizing/chapi) 语言模型的想法是一致的。而这种所谓的通用模型会遇到的问题是,需要抛弃一些细节,诸如于只实现 80% 的核心功能。
对于一个图(Graph)来说,它的模型也就变得相当的简单:
export interface Graph {
nodes: Node[];
edges: Edge[];
props?: GraphProperty;
subgraphs?: Graph[];
}
围绕于这四个核心元素再往下展开:
而如上所述 Shape 和 Edge 就是两个大家庭,包含了一系列的子类,诸如于 Shape 包含了 PolygonShape(又包含了 TriangleShape、DiamondShape、RectangleShape 、HexagonShape)。
对于属性来主,尚未进一步展开,但是初步分为:FillState、FontState、StrokeState、ImageState 等。
如果你也感兴趣的话,欢迎一起来设计。
在反复的设计了各种 Importer/Exporter 之后,并持续不断的进行模型重构之后,就构成了的核心特性的基础:二阶段绘图。简单来说,就是把绘图分为了两阶段:
以在不同的工具之间转换,并实现图的互转。
在这里就可以尝试使用:https://online.feakin.com/ ,虽然还只是一个早期的版本,仍旧还有一系列的 bug,但是还可以尝试的。
左边的编辑器是使用 Monaco Editor,配合了简单的 Dot 语法支持;右边则是一个早期版本的 Feakin Render,只能简单地渲染一下,看看效果。当前,仅支持简单的拖拉拽,还容易出错。
在这个过程中,还有一系列有意思的东西,比如 Shape 在不同的图形工具是不一样的。先让我们看个代码示例:
digraph G {
a [shape="triangle"];
b [shape="diamond"];
a -> b;
}
也是截图中的代码简化,节点 a 的 shape 是一个 Triangle(三角形),然而:
mxgraph.basic.acute_triangle
。于是乎,为了结果上的一致,我们需要在对应的 ExcalidrawExporter、DrawioExporter 进行对应的 Shape 的处理和 Mapping。
在这个 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 的协作部分。
如果你也有兴趣,欢迎一起来用爱发电。如此一来,也不枉我花三个小时写的这篇文章。
围观我的Github Idea墙, 也许,你会遇到心仪的项目