警告
截止到2024年08月30日,已经内建使用 canvas 模式渲染:
前言
quartz 的关系图谱用的是 d3 实现的,是通过 svg 生成的,固然 svg 的编写和开发相对来说是容易的,但是核心问题在于性能并不足以支撑较大规模的关系图谱展示,譬如下面这两个;
这两个有较大规模的关系图谱,此时预览整个图谱时有巨大的卡顿感、或者延迟。
即使关系图谱并不大可能数百个的情况下,对于缩放的情况下就会出现明显的掉帧的情况。
对此我预期还是需要进行一些优化的。
事实上对于大面积的图形展示,d3 + svg 并不是一个 合理、有效 的解决方案。这样数量级的渲染使用 canvas 、webgl 是更好的手段。
技术栈选择
这里我选择使用 pixi.js + d3 实现,这样可以在不大面积修改 d3 实现的情况下替换整个渲染引擎,同时导入 tween.js 来做动画的渲染。
你可以点击链接,前往 demo 查看效果,内部使用同 https://jzhao.xyz/ 一样的数据, 可以明显感觉相对于之前要流畅不少。
如果之前你使用过本站右侧的关系图谱的话,也能感受到在相对较小的数量下,low 帧的数量要更低。
实现细节
具体的代码方面的话可以参考 https://github.com/iceprosurface/quartz-blog/tree/v4/packages/quartz-graph-plugin (带hash 版本,防止未来更新目录)
结构、d3 数据
d3 在操作数据会直接修改原始数据,也就是说譬如我们设定下面两种数据结构:
他有个很大的问题是 LinkData 是不正确的,source 和 targe 在经过 d3 处理以后就不是 string 了。
后面在编写代码的时候需要时刻注意这一点。
css 变量的获取
原始代码那边是使用 css 变量获取具体的样式的,所以需要提供一个简单的辅助函数用来获取。
这样我们就能获取对应的颜色代码了,需要注意的是他们都是 dom 访问api ,不要在 render 中调用 getColor 他太慢了。
贴士
事实上你是可以把 colorMap 内置到 getColor 中的,这里暂时没有实现。
devicePixelRatio
需要注意的是默认情况下一般不会设置高 dpi 这样会在 高分屏 上会比较模糊,这也是相对于 svg 的劣势。
app 配置
一般而言只需要 获取 window.devicePixelRatio
并设置即可,所以你需要在初始化 pixijs 时主动设置:
线条字体设置
由于我们会使用滚轮缩放,所以有一个明显的问题就是在放大后页面会模糊,这里有几个方法可以减少这种情况的发生:
- 使用 resolution 将页面设置为
最大可缩放的大小
window.devicePixelRatio
。 - 对一些主要元素做处理,将他们按照固定大小放大后缩小,这样即使 scale 放大后还是可以不模糊的显示的
- 实现一套统一的重刷机制,当 zoom 的时候重建整个 graph 并按照 k 适配大小
危险
注意在浏览器上一个很重要的参数就是 MAX_TEXTURE_SIZE,对于 PC 而言 移动端是 如果你在绘制元素时使用的 buffer 片大小大于这个大小,可能会显示不出东西,或是渲染黑色块上去,这种情况你需要想办法裁切元素分段渲染。如果绘制的是关系图谱,而不实现贴图绘制的话一般不会触及到这个限制。
这两个事情都有优劣,第一个方案对性能的要求是蛮高的,放大倍率为 4 的情况下就画布已经达到 ,对于性能较差的设备肯定不太友好。而第二个方案则是实现起来简单,但是 node 和 link 都没变,显示效果有点差。第三个方案肯定是最好的,但是问题很明显,改动比较大,后续看看有没有时间实现了。
动画
动画实现上我们用 tween.js 并在主循环中 update, 写了一个辅助函数用来在全局注册、销毁 动画。
拖拽
另一个值得注意的点是拖拽元素。拖元素我们需要记录起始拖拽点,随后在拖拽中计算相对位移后设置 fx,fy 。
偷懒警告
这里由于 event 是 any 类型,所以我偷懒直接在 subject 上设置了
__initialDragPos
这是不好的行为!千万别学
点击
点击事件的判断使用 dragend 来判断而不是直接用 node 的 click 事件,因为跨越两个系统判断一个异步事件不是一个方便的事情,不如直接在 dragend 中判断点击的间隔是不是在 200ms 以内,在 200ms 以内就当作是 click 就好了。
排序、显示在最前
这里 hover 时是需要把元素显示在最前面的,这里可以通过 zindex 实现。
由于 link 是 统一一次性 的用不了 zindex 所以只能加个 sort 用 active 状态判断。
这里也有个不太好权衡的点:
pixijs 会对 graphic 元素进行 cache ,也就是说如果渲染过一次以后,又没有修改,那么会使用之前缓存的 array buffer 直接绘制。对于 node 来说这个特性很好,可以直接使用,但是对于 link 而言,这种操作并不友好,因为 link 本身每个 tick 都在修改,大量的 link 重绘制,肯定是不如在 gragh 中一次绘制所有 link 来的好。