基础动画

阅读时间约 6 分钟

G6 中的动画分为两个层次:

  • 全局动画:全局性的动画,图整体变化时的动画过渡;
  • 元素(边和节点)动画:节点或边上的独立动画。

## 全局动画 G6 的全局动画指通过图实例进行某些全局操作时,产生的动画效果。例如:
  • graph.updateLayout(cfg) 布局的变化
  • graph.changeData() 数据的变化

通过实例化图时配置 animate: true,可以达到每次进行上述操作时,动画效果变化的目的。配合 animateCfg 配置动画参数:

const graph = new G6.Graph({
  // ...                   // 图的其他配置项
  animate: true, // Boolean,切换布局时是否使用动画过度,默认为 false
  animateCfg: {
    duration: 500, // Number,一次动画的时长
    easing: 'linearEasing', // String,动画函数
  },
});

easing 函数

easing 函数是指动画的函数。例如线性插值、先快后慢等。
G6 支持所有 d3.js 中的动画函数。因此,上面代码中 animateCfg 配置中的 String 类型的 easing 可以取值有:
'easeLinear'
'easePolyIn''easePolyOut''easePolyInOut'
'``easeQuad``''easeQuadIn''easeQuadOut' , 'easeQuadInOut'

更多取值及所有取值含义参见:d3 Easings

元素动画

由于 G6 的内置节点和边是没有动画的,需要实现节点和边上的动画需要通过自定义节点自定义边时复写 afterDraw 实现。

节点动画

节点上的动画,即每一帧发生变化的是节点上的某一个图形。 关于节点动画,以下面三个动画示例进行讲解:

  • 节点上图形的动画(如下图左);
  • 增加带有动画的背景图形(如下图中);
  • 节点上部分图形的旋转动画(如下图右)。
download download download

以上三个动画节点的 demo 代码见: 节点动画

节点上图形的动画


download

本例实现节点放大缩小,通过 group.get('children')[0] 找到需要更新的图形(这里找到该节点上第 0 个图形),然后调用该图形的 animate 方法指定动画的参数及每一帧的变化( onFrame 方法返回每一帧需要变化的参数集)。

// 放大、变小动画
G6.registerNode(
  'circle-animate',
  {
    afterDraw(cfg, group) {
      // 获取该节点上的第一个图形
      const shape = group.get('children')[0];
      // 该图形的动画
      shape.animate(
        {
          // 动画重复
          repeat: true,
          // 每一帧的操作,入参 ratio:这一帧的比例值(Number)。返回值:这一帧需要变化的参数集(Object)。
          onFrame(ratio) {
            // 先变大、再变小
            const diff = ratio <= 0.5 ? ratio * 10 : (1 - ratio) * 10;
            let radius = cfg.size;
            if (isNaN(radius)) radius = radius[0];
            // 返回这一帧需要变化的参数集,这里只包含了半径
            return {
              r: radius / 2 + diff,
            };
          },
        },
        3000,
        'easeCubic',
      ); // 一次动画持续的时长为 3000,动画效果为 'easeCubic'
    },
  },
  'circle',
); // 该自定义节点继承了内置节点 'circle',除了被复写的 afterDraw 方法外,其他按照 'circle' 里的函数执行。

增加带有动画的背景图形

afterDraw 方法中为已有节点添加额外的 shape ,并为这些新增的图形设置动画。

本例在 afterDraw 方法中,绘制了三个背景 circle ,分别使用不同的颜色填充,再调用 animate 方法实现这三个 circle 逐渐变大、变淡的动画。本例中没有使用 onFrame 函数,直接在 animate 函数的入参中设置每次动画结束时的最终目标样式,即半径增大 10,透明度降为 0.1。

download
G6.registerNode(
  'background-animate',
  {
    afterDraw(cfg, group) {
      let r = cfg.size / 2;
      if (isNaN(r)) {
        r = cfg.size[0] / 2;
      }
      // 第一个背景圆
      const back1 = group.addShape('circle', {
        zIndex: -3,
        attrs: {
          x: 0,
          y: 0,
          r,
          fill: cfg.color,
          opacity: 0.6,
        },
      });
      // 第二个背景圆
      const back2 = group.addShape('circle', {
        zIndex: -2,
        attrs: {
          x: 0,
          y: 0,
          r,
          fill: 'blue', // 为了显示清晰,随意设置了颜色
          opacity: 0.6,
        },
      });
      // 第三个背景圆
      const back3 = group.addShape('circle', {
        zIndex: -1,
        attrs: {
          x: 0,
          y: 0,
          r,
          fill: 'green',
          opacity: 0.6,
        },
      });
      group.sort(); // 排序,根据 zIndex 排序

      // 第一个背景圆逐渐放大,并消失
      back1.animate(
        {
          r: r + 10,
          opacity: 0.1,
          repeat: true, // 循环
        },
        3000,
        'easeCubic',
        null,
        0,
      ); // 无延迟

      // 第二个背景圆逐渐放大,并消失
      back2.animate(
        {
          r: r + 10,
          opacity: 0.1,
          repeat: true, // 循环
        },
        3000,
        'easeCubic',
        null,
        1000,
      ); // 1 秒延迟

      // 第三个背景圆逐渐放大,并消失
      back3.animate(
        {
          r: r + 10,
          opacity: 0.1,
          repeat: true, // 循环
        },
        3000,
        'easeCubic',
        null,
        2000,
      ); // 2 秒延迟
    },
  },
  'circle',
);

部分图形旋转动画

这一例也是在 afterDraw 方法中为已有节点添加额外的 shape (本例中为 image),并为这些新增的图形设置旋转动画。旋转动画较为复杂,需要通过矩阵的操作实现。

download
G6.registerNode(
  'inner-animate',
  {
    afterDraw(cfg, group) {
      const size = cfg.size;
      const width = size[0] - 12;
      const height = size[1] - 12;
      // 添加图片 shape
      const image = group.addShape('image', {
        attrs: {
          x: -width / 2,
          y: -height / 2,
          width: width,
          height: height,
          img: cfg.img,
        },
      });
      // 该图片 shape 的动画
      image.animate(
        {
          // 动画重复
          repeat: true,
          // 每一帧的操作,入参 ratio:这一帧的比例值(Number)。返回值:这一帧需要变化的参数集(Object)。
          onFrame(ratio) {
            // 旋转通过矩阵来实现
            // 当前矩阵
            const matrix = Util.mat3.create();
            // 目标矩阵
            const toMatrix = Util.transform(matrix, [
              ['r', ratio * Math.PI * 2],
            ]);
            // 返回这一帧需要的参数集,本例中只有目标矩阵
            return {
              matrix: toMatrix,
            };
          },
        },
        3000,
        'easeCubic',
      );
    },
  },
  'rect',
);

边动画

关于边动画,以下面三个动画示例进行讲解:

  • 圆点在沿着线运动(下图左);
  • 虚线运动的效果(下图中,gif 图片的帧率问题导致看起来是静态的,可以访问下面的 demo 链接查看);
  • 线从无到有的效果(下图右)。
download download download

以上三个边动画的 demo 代码见:边动画

圆点运动

本例通过在 afterDraw 方法中为边增加了一个 circle 图形,该图形沿着线运动。沿着线运动的原理:设定每一帧中,该 circle 在线上的相对位置。

download
G6.registerEdge(
  'circle-running',
  {
    afterDraw(cfg, group) {
      // 获得当前边的第一个图形,这里是边本身的 path
      const shape = group.get('children')[0];
      // 边 path 的起点位置
      const startPoint = shape.getPoint(0);

      // 添加红色 circle 图形
      const circle = group.addShape('circle', {
        attrs: {
          x: startPoint.x,
          y: startPoint.y,
          fill: 'red',
          r: 3,
        },
      });

      // 对红色圆点添加动画
      circle.animate(
        {
          // 动画重复
          repeat: true,
          // 每一帧的操作,入参 ratio:这一帧的比例值(Number)。返回值:这一帧需要变化的参数集(Object)。
          onFrame(ratio) {
            // 根据比例值,获得在边 path 上对应比例的位置。
            const tmpPoint = shape.getPoint(ratio);
            // 返回需要变化的参数集,这里返回了位置 x 和 y
            return {
              x: tmpPoint.x,
              y: tmpPoint.y,
            };
          },
        },
        3000,
      ); // 一次动画的时间长度
    },
  },
  'cubic',
); // 该自定义边继承内置三阶贝塞尔曲线 cubic

虚线运动的效果

虚线运动的效果是通过计算线的 lineDash ,并在每一帧中不断修改实现。

download
// lineDash 的差值,可以在后面提供 util 方法自动计算
const dashArray = [
  [0, 1],
  [0, 2],
  [1, 2],
  [0, 1, 1, 2],
  [0, 2, 1, 2],
  [1, 2, 1, 2],
  [2, 2, 1, 2],
  [3, 2, 1, 2],
  [4, 2, 1, 2],
];

const lineDash = [4, 2, 1, 2];
const interval = 9; // lineDash 的和
G6.registerEdge(
  'line-dash',
  {
    afterDraw(cfg, group) {
      // 获得该边的第一个图形,这里是边的 path
      const shape = group.get('children')[0];
      // 获得边的 path 的总长度
      const length = shape.getTotalLength();
      let totalArray = [];
      // 计算出整条线的 lineDash
      for (var i = 0; i < length; i += interval) {
        totalArray = totalArray.concat(lineDash);
      }

      let index = 0;
      // 边 path 图形的动画
      shape.animate(
        {
          // 动画重复
          repeat: true,
          // 每一帧的操作,入参 ratio:这一帧的比例值(Number)。返回值:这一帧需要变化的参数集(Object)。
          onFrame(ratio) {
            const cfg = {
              lineDash: dashArray[index].concat(totalArray),
            };
            // 每次移动 1
            index = (index + 1) % interval;
            // 返回需要修改的参数集,这里只修改了 lineDash
            return cfg;
          },
        },
        3000,
      ); // 一次动画的时长为 3000
    },
  },
  'cubic',
); // 该自定义边继承了内置三阶贝塞尔曲线边 cubic

线从无到有

线从无到有的动画效果,同样可以通过计算 lineDash 来实现。

download
G6.registerEdge(
  'line-growth',
  {
    afterDraw(cfg, group) {
      const shape = group.get('children')[0];
      const length = group.getTotalLength();
      shape.animate(
        {
          // 动画重复
          repeat: true,
          // 每一帧的操作,入参 ratio:这一帧的比例值(Number)。返回值:这一帧需要变化的参数集(Object)。
          onFrame(ratio) {
            const startLen = ratio * length;
            // 计算线的lineDash
            const cfg = {
              lineDash: [startLen, length - startLen],
            };
            return cfg;
          },
        },
        2000,
      ); // 一次动画的时长为 2000
    },
  },
  'cubic',
); // 该自定义边继承了内置三阶贝塞尔曲线边 cubic

交互动画

在交互的过程中也可以添加动画。如下图所示,当鼠标移到节点上时,所有与该节点相关联的边都展示虚线运动的动画。
交互动画.gif
上图完整 demo 即代码参见:状态切换动画

这种动画涉及到了边的 状态。在自定义边时复写 setState 方法,可对边的各种状态进行监听。鼠标移动到节点上,相关边的某个状态被开启,setState 方法中监听到后开启动画效果。步骤如下:

  • 自定义边中复写 setState 方法监听该边的状态,以及某状态下的动画效果;
  • 监听中间的节点的 mouseentermouseleave 事件,触发相关边的状态变化。

下面代码节选自 demo 状态切换动画,请注意省略了部分代码,只展示了交互相关以及边动画相关的代码。

// const data = ...
// const graph = new G6.Graph({...});

// lineDash 的差值,可以在后面提供 util 方法自动计算
const dashArray = [
  [0, 1],
  [0, 2],
  [1, 2],
  [0, 1, 1, 2],
  [0, 2, 1, 2],
  [1, 2, 1, 2],
  [2, 2, 1, 2],
  [3, 2, 1, 2],
  [4, 2, 1, 2],
];

const lineDash = [4, 2, 1, 2];
const interval = 9; // lineDash 的总长度。

// 注册名为 'can-running' 的边
G6.registerEdge(
  'can-running',
  {
    // 复写setState方法
    setState(name, value, item) {
      const shape = item.get('keyShape');
      // 监听 running 状态
      if (name === 'running') {
        // running 状态为 true 时
        if (value) {
          const length = shape.getTotalLength();
          let totalArray = [];
          for (var i = 0; i < length; i += interval) {
            totalArray = totalArray.concat(lineDash);
          }
          let index = 0;
          shape.animate(
            {
              // 动画重复
              repeat: true,
              // 每一帧的操作,入参 ratio:这一帧的比例值(Number)。返回值:这一帧需要变化的参数集(Object)。
              onFrame(ratio) {
                const cfg = {
                  lineDash: dashArray[index].concat(totalArray),
                };
                index = (index + 1) % interval;
                return cfg;
              },
            },
            3000,
          ); // 一次动画的时长为 3000
        } else {
          // running 状态为 false 时
          // 结束动画
          shape.stopAnimate();
          // 清空 lineDash
          shape.attr('lineDash', null);
        }
      }
    },
  },
  'cubic-horizontal',
); // 该自定义边继承了内置横向三阶贝塞尔曲线边 cubic-horizontal

// 监听节点的 mouseenter 事件
graph.on('node:mouseenter', ev => {
  // 获得当前鼠标操作的目标节点
  const node = ev.item;
  // 获得目标节点的所有相关边
  const edges = node.getEdges();
  // 将所有相关边的 running 状态置为 true,此时将会触发自定义节点的 setState 函数
  edges.forEach(edge => graph.setItemState(edge, 'running', true));
});

// 监听节点的 mouseleave 事件
graph.on('node:mouseleave', ev => {
  // 获得当前鼠标操作的目标节点
  const node = ev.item;
  // 获得目标节点的所有相关边
  const edges = node.getEdges();
  // 将所有相关边的 running 状态置为 false,此时将会触发自定义节点的 setState 函数
  edges.forEach(edge => graph.setItemState(edge, 'running', false));
});

// graph.data(data);
// graph.render();

  ⚠️注意: runningfalse 时,要停止动画,同时把 lineDash 清空。