手摸手使用G6实现(轻)图编辑应用系列-自定义节点

2,531 阅读5分钟

我正在参加「掘金·启航计划」

系列文章:

前言

上一节,进行数据处理最后渲染到画布上的节点默认是 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 时

    keyShape1.png
  • 当节点的 keyShape 为 rect 时:

    keyShape2.png
  • 绘制图形的坐标系

    • 每个节点都是一个图形分组group,group内有很多shape,所有shape都是相对于节点自身的坐标系,即 (0, 0) 是该节点的中心,向右为x轴正半轴,向下为y轴正半轴。
    • draw方法就是要添加所有的shape
      • 圆形:x、y圆心坐标,r圆半径
      • 矩形:x、y图形左上角坐标,width、height图形宽高
      • 图片:与矩形相同
  • 效果

node.png

  • 代码实现
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);
    }
  },

结尾

截至到此,自定节点实现完成,下一节会介绍实现自定义边、箭头的步骤

希望看完的朋友可以点个喜欢/关注,您的支持是对我最大的鼓励