G6 中被人吐槽最多的就是默认箭头不符合要求啊,怎么自定义啊。。那么我们来看看这个默认箭头,是长这样:
这是G中实现的默认箭头效果,好像确实是有点丑。那么我们在 G6 中第一步,就是要将这个箭头默认箭头样式改掉。
坎坷的默认箭头替换之路
内置节点的箭头
幸好 G 提供箭头自定义的机制,我们想象中箭头应该是左图这样的,但是在第一次实现的时候,却是右图这样的。
右图箭头戳入节点的原因是在绘图时,箭头相当于是附加在线条上的,也就是说,同一条 path, 加了箭头会比不加箭头的长,长多少取决于箭头的大小,这个本身可以用 G 自定义箭头的 d 来解决。但是当线条比较宽的时候,可能就会露出线条末端,箭头很难看。解决这个问题,有两个解决方案:
- 将线条缩进一段距离:计算在尾部的角度,加上箭头的情况下,箭头超出的距离,在线上缩进去。这种方案需要计算两次线条的 path,并且在线条过宽的时候可能不可控,因为缩进距离只与箭头大小有关系。
- 给线条加一个 clip然后将箭头往回缩进一段距离。这种计算比较麻烦,但是没有线宽问题。有问题的是在空心箭头下(比如 G 的默认箭头样式),会有线与箭头不连的问题。
最后我们选择方案将线条缩进一段距离。如何衡量这个缩进的距离又是一个麻烦事。原本 G 中自定义箭头默认的约定是:以 x 轴正方向为箭头指向的 path 来定义箭头的样式,并且可以用 d 来指定箭头。G6 中基于这个约定略微拓展,G6 中自定义箭头是以 (0, 0)为中心,x 轴正方向为箭头指向的path, 用 d 来指定箭头与连线连接的距离。G6便可以在内部获取绘图属性时替换默认的箭头,并在线条绘制结束时根据 d 来计算缩进的距离。例如默认的箭头实现是:
const path = group.addShape('path', {
attrs: {
endArrow: {
// path 的 width, height, radius 是基于线宽来计算的,详情可见源码。
path: [
[ 'M', -width, height ],
[ 'L', width, 0],
[ 'L', -width, -height ],
[ 'A', radius, radius, 0, 0, 1, -width, height ],
[ 'Z' ]
],
d: width
}
...attrs
}
});
自定义线条怎么办?
上述方法在内置节点上看起来十分简洁有效,但是如果用户自行注册了边,或者覆写了afterDraw 方法,箭头效果就没有了。因为当用户直接覆写 draw 方法时,绘图属性是直接透传到并不会让 G6 去修改,并且并不会去缩进。这种情况下我们又想出了两种解决方案:
- 直接扩展 G 的箭头能力:将绘制箭头后缩线的逻辑直接沉到 G 中。在 G 绘制自定义箭头的时候就缩短线条。这种方法比较直接了当,能统一内置线条和自定义线条的箭头样式问题。但是 G 作为底层渲染引擎,影响面较广,需要慎重改造。
- 在 G6 的 shape 绘制流程中再插入一个方法来处理箭头和缩短线条。这种方式不好的一点是线条会先经过 G 的计算绘制,然后再在 G6 重新定义后,再计算一遍。
最后,出于从根源解决这个问题的考虑,我们决定要将这个能力沉淀到 G。
箭头样式的复用
默认样式的问题解决以后,我们进一步的,来考虑自定义箭头的复用问题。如果我们要用 G6 来绘制一个 UML 图,箭头的样式会很多,简单举例如下:
如果我们在定义每个箭头的时候都需要像上面的代码一样去定义整个 Object 会造成大量的重复代码,降低代码可读性。理想情况下,最好能注册几种箭头类型,在绘制时在绘图属性中指定一种箭头类型即可。这点可以在 G6 开放注册箭头,透传注册到 G 实现。
// 没有箭头注册机制时
const edge = graph.addItem('edge', {
style: {
endArrow: {
path: [...],
d
}
},
...options
});
// 提供箭头注册机制
G6.registerArrow('custom-arrow', {
path: function(attrs) {
return [...];
},
d: function(attrs) { return d; }
});
const edge = graph.addItem('edge', {
style: {
endArrow: 'custom-arrow',
},
...options
});