我正在参加「掘金·启航计划」
系列文章:
- 手摸手使用G6实现(轻)图编辑应用系列-初识G6
- 手摸手使用G6实现(轻)图编辑应用系列-自定义节点
- 手摸手使用G6实现(轻)图编辑应用系列-自定义边、箭头
- 手摸手使用G6实现(轻)图编辑应用系列-自定义行为
- 手摸手使用G6实现(轻)图编辑应用系列-后续优化
前言
上一节,进行数据处理最后渲染到画布上的节点默认是 Circle 圆节点,但内置圆节点并不能满足实际需求,接下来通过自定义节点来实现效果
节点分析
自定义节点时,首先要分析节点内有哪些图形
- 圆形
- 圆形中的图标
- 圆形下方的文字
- 连线、删除操作的图标
- 填补操作图标与圆形之间空隙的矩形
- 动画效果所需的三个圆形
官方方法
- G6.registerNode(typeName: string, nodeDefinition: object, extendedTypeName?: string)
typeName:该新节点类型名称;nodeDefinition:该新节点类型的定义。当有 extendedTypeName 时,没被复写的函数将会继承 extendedTypeName 的定义。{ /** * 绘制节点,包含文本 * @param {Object} cfg 节点的配置项 * @param {G.Group} group 图形分组,节点中图形对象的容器 * @return {G.Shape} 返回一个绘制的图形作为 keyShape,通过 node.get('keyShape') 可以获取。 * 关于 keyShape 可参考文档 核心概念-节点/边/Combo-图形 Shape 与 keyShape */ draw(cfg, group) {}, /** * 绘制后的附加操作,默认没有任何操作 * @param {Object} cfg 节点的配置项 * @param {G.Group} group 图形分组,节点中图形对象的容器 */ afterDraw(cfg, group) {}, /** * 更新节点,包含文本 * @override * @param {Object} cfg 节点的配置项 * @param {Node} node 节点 */ update(cfg, node) {}, /** * 更新节点后的操作,一般同 afterDraw 配合使用 * @override * @param {Object} cfg 节点的配置项 * @param {Node} node 节点 */ afterUpdate(cfg, node) {}, /** * 响应节点的状态变化。 * 在需要使用动画来响应状态变化时需要被复写,其他样式的响应参见下文提及的 [配置状态样式] 文档 * @param {String} name 状态名称 * @param {Object} value 状态值 * @param {Node} node 节点 */ setState(name, value, node) {}, /** * 获取锚点(相关边的连入点) * @param {Object} cfg 节点的配置项 * @return {Array|null} 锚点(相关边的连入点)的数组,如果为 null,则没有控制点 */ getAnchorPoints(cfg) {}, }
extendedTypeName:被继承的节点类型,可以是内置节点类型名,也可以是其他自定义节点的类型名。extendedTypeName 未指定时代表不继承其他类型的节点;
节点实现
G6.registerNode('customNode', {},'single-node');
目前使用 single-node 扩展新节点,nodeDefinition 中 draw 方法是必须的
draw 方法
-
此方法必须返回一个 keyShape 作为关键图形,用于确定节点包围盒,从而计算相关边的连入点(与相关边的交点)。若 keyShape 不同,节点与边的交点计算结果不同
-
当节点的 keyShape 为 circle 时
-
当节点的 keyShape 为 rect 时:
-
绘制图形的坐标系
- 每个节点都是一个图形分组group,group内有很多shape,所有shape都是相对于节点自身的坐标系,即 (0, 0) 是该节点的中心,向右为x轴正半轴,向下为y轴正半轴。
- draw方法就是要添加所有的shape
- 圆形:x、y圆心坐标,r圆半径
- 矩形:x、y图形左上角坐标,width、height图形宽高
- 图片:与矩形相同
-
效果
- 代码实现
draw (cfg, group) {
// 关键图形
const cricle = group.addShape('circle', {
attrs: Object.assign(
{
x: 0,
y: 0,
r: cfg.size,
},
styleObj.cricleDefault // 节点默认样式
),
name: 'key-shape',
zIndex: 1, // 图形层级
draggable: true, // 图形是否可拖动
});
// 节点中心的图标
group.addShape('image', {
attrs: {
x: -cfg.size / 2,
y: -cfg.size / 2,
width: cfg.size,
height: cfg.size,
img: `${window.location.origin}/graphIcon/${cfg.icon}.svg`
},
name: 'center-image',
zIndex: 2,
draggable: true,
});
// 连接图标后的背景(保证鼠标移动到操作过程中不触发节点移出事件,导致无法点击到操作图标)
const connectBk = group.addShape('rect', {
attrs: {
x: 0,
y: 0 - cfg.size,
width: cfg.size + styleObj.connectImage.size, // 节点半径 + 图标宽
height: cfg.size,
fill: 'green',
opacity: 0.3
},
name: 'text-shape-connectBk',
zIndex: -1,
});
// 连接图标
const connect = group.addShape('image', {
attrs: {
x: cfg.size,
y: 0 - cfg.size + 5,
width: styleObj.connectImage.size,
height: styleObj.connectImage.size,
img: `${window.location.origin}/graphIcon/connectLine.svg`,
cursor: 'pointer',
},
name: 'text-shape-connect',
zIndex: 9,
})
// 默认隐藏图形
connect.hide();
connectBk.hide();
// 删除图标与连接图标一样的处理
..........
// 绘制label(这里借助的是源码中绘制label的方法,不再单独实现)
if (cfg.label) {
var label = this.drawLabel(cfg, group);
label.set('className', this.itemType + '-label');
group['shapeMap'][this.itemType + '-label'] = label;
}
group.sort();
return cricle;
afterDraw 方法
- 用于绘制节点动画需要的三个圆形
- 层级不同
- r半径与关键图形一样即可
update 方法
- 目前发现此方法只会在graph.updateItem时触发,实现中没有使用此方法(改动样式都是通过shape的attr方法),所以没有进行复写,直接继承single-node的update方法
- 使用了updateItem并且single-node的update方法无法满足时,有两个方法
- 复写update赋值为undefined,这样会直接触发draw方法,不过性能耗损大
- 复写update函数,根据cfg配置的变化进行指定shape的更新,最佳方法
afterUpdate 方法
- 类似于update
setState 方法
- 通过graph.setItemState给节点设置不同状态值,针对状态值做出相应效果
- 大概就是通过获取图形分组,根据设置的图形name找到指定图形shape,改变属性值
setState(name, value, item) {
// 获取图形分组
const group = item.getContainer();
// 找到具体图形
const cricleShape = group.find(child => child.get('name') === "key-shape");
const textShape = group.find(child => child.get('name') === "text-shape");
if (name === 'active') {
if (value) {
// 开启动画
this.nodeAnimate(true, group);
// 改变图形的属性
// 根据不同模式,选择性的显示操作图标(可能条件更多,因情况自行处理)
.........
} else {
const model = item.getModel();
// 关闭动画
this.nodeAnimate(false, group);
.......
}
}
},
其他方法
- 这些方法不是用于复写 G6 内部默认节点函数的,而且方便在其他复写方法使用this调用的
- 节点动画被封装成一个方法
nodeAnimate(animate, group){
// afterDraw 绘制时,挂在到 this 上的
const r = this.cfg.r;
["back1-shape", "back2-shape", "back3-shape"].forEach((shapeName, index) =>{
this.singleAnimate(
shapeName,
[
{
r: r + 10,
opacity: 0.1,
},
{
duration: 3000,
easing: 'easeCubic',
delay: index * 1000,
repeat: true,
},
],
animate,
group
)
})
}
singleAnimate(shapeName, animateCfg, animate, group){
const r = this.cfg.r;
const targetShage = group.find(child => child.get('name') === shapeName);
if(animate){
targetShage.animate(...animateCfg);
}else{
targetShage.stopAnimate();
targetShage.attr('r', r);
}
},
结尾
截至到此,自定节点实现完成,下一节会介绍实现自定义边、箭头的步骤
希望看完的朋友可以点个喜欢/关注,您的支持是对我最大的鼓励