g6/packages/site/docs/manual/middle/animation.zh.md
2023-02-02 10:31:36 +08:00

493 lines
19 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
title: 基础动画
order: 5
---
G6 中的动画分为两个层次:
- 全局动画:全局性的动画,图整体变化时的动画过渡;
- 元素(边和节点)动画:节点或边上的独立动画。
<br />
## 全局动画
G6 的全局动画指通过图实例进行某些全局操作时,产生的动画效果。例如:
- `graph.updateLayout(cfg)` 布局的变化
- `graph.changeData()` 数据的变化
通过实例化图时配置 `animate: true`,可以达到每次进行上述操作时,动画效果变化的目的。配合 `animateCfg` 配置动画参数,`animateCfg` 具体配置参见 [animateCfg](#animateCfg)<br />
```javascript
const graph = new G6.Graph({
// ... // 图的其他配置项
animate: true, // Boolean切换布局时是否使用动画过度默认为 false
animateCfg: {
duration: 500, // Number一次动画的时长
easing: 'linearEasing', // String动画函数
},
});
```
## 元素动画
由于 G6 的内置节点和边是没有动画的,需要实现节点和边上的动画需要通过[自定义节点](/zh/docs/manual/middle/elements/nodes/custom-node)、[自定义边](/zh/docs/manual/middle/elements/edges/custom-edge)时复写  `afterDraw`  实现。
### 节点动画
节点上的动画,即每一帧发生变化的是节点上的某一个图形。关于节点动画,以下面三个动画示例进行讲解:
- 节点上图形的动画(如下图左);
- 增加带有动画的背景图形(如下图中);
- 节点上部分图形的旋转动画(如下图右)。
<img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*aAjWQ4n_OOEAAAAAAAAAAABkARQnAQ' alt='download' width='150'/>
<img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*FxDJQ5eY-5oAAAAAAAAAAABkARQnAQ' alt='download' width='150'/>
<img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*uFQsQqxIa_QAAAAAAAAAAABkARQnAQ' alt='download' width='150'/>
<br />
以上三个动画节点的 demo 代码见: <a href='/zh/examples/scatter/node' target='_blank'>节点动画</a>
#### 节点上图形的动画
<br />
<img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*aAjWQ4n_OOEAAAAAAAAAAABkARQnAQ' alt='download' width='150'/>
本例实现节点放大缩小,通过  `group.get('children')[0]` 找到需要更新的图形(这里找到该节点上第 0 个图形),然后调用该图形的 `animate` 方法指定动画的参数及每一帧的变化(  第一个参数是返回每一帧需要变化的参数集的函数,其参数 `ratio` 是当前正在进行的一次动画的进度,范围 [0, 1];第二个参数是动画的参数,动画参数的具体配置参见 [animateCfg](#animateCfg))。
```javascript
// 放大、变小动画
G6.registerNode(
'circle-animate',
{
afterDraw(cfg, group) {
// 获取该节点上的第一个图形
const shape = group.get('children')[0];
// 该图形的动画
shape.animate(
(ratio) => {
// 每一帧的操作,入参 ratio这一帧的比例值Number。返回值这一帧需要变化的参数集Object
// 先变大、再变小
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,
};
},
{
// 动画重复
repeat: true,
duration: 3000,
easing: 'easeCubic',
},
); // 一次动画持续的时长为 3000动画效果为 'easeCubic'
},
},
'circle',
); // 该自定义节点继承了内置节点 'circle',除了被复写的 afterDraw 方法外,其他按照 'circle' 里的函数执行。
```
#### 增加带有动画的背景图形
`afterDraw` 方法中为已有节点添加额外的 shape ,并为这些新增的图形设置动画。<br />
本例在 `afterDraw` 方法中,绘制了三个背景 circle ,分别使用不同的颜色填充,再调用 `animate` 方法实现这三个 circle 逐渐变大、变淡的动画。本例中没有使用函数参数的形式,直接在 `animate` 函数的第一个参数中设置每次动画结束时的最终目标样式,即半径增大 10透明度降为 0.1。第二个参数设置动画的配置,动画参数的具体配置参见 [animateCfg](#animateCfg)。<br />
<img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*FxDJQ5eY-5oAAAAAAAAAAABkARQnAQ' alt='download' width='150'/>
```javascript
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,
},
// 在 G6 3.3 及之后的版本中,必须指定 name可以是任意字符串但需要在同一个自定义元素类型中保持唯一性
name: 'circle-shape1',
});
// 第二个背景圆
const back2 = group.addShape('circle', {
zIndex: -2,
attrs: {
x: 0,
y: 0,
r,
fill: 'blue', // 为了显示清晰,随意设置了颜色
opacity: 0.6,
},
// 在 G6 3.3 及之后的版本中,必须指定 name可以是任意字符串但需要在同一个自定义元素类型中保持唯一性
name: 'circle-shape2',
});
// 第三个背景圆
const back3 = group.addShape('circle', {
zIndex: -1,
attrs: {
x: 0,
y: 0,
r,
fill: 'green',
opacity: 0.6,
},
// 在 G6 3.3 及之后的版本中,必须指定 name可以是任意字符串但需要在同一个自定义元素类型中保持唯一性
name: 'circle-shape3',
});
group.sort(); // 排序,根据 zIndex 排序
// 第一个背景圆逐渐放大,并消失
back1.animate(
{
r: r + 10,
opacity: 0.1,
},
{
repeat: true, // 循环
duration: 3000,
easing: 'easeCubic',
delay: 0, // 无延迟
},
);
// 第二个背景圆逐渐放大,并消失
back2.animate(
{
r: r + 10,
opacity: 0.1,
},
{
repeat: true, // 循环
duration: 3000,
easing: 'easeCubic',
delay: 1000, // 1 秒延迟
},
); // 1 秒延迟
// 第三个背景圆逐渐放大,并消失
back3.animate(
{
r: r + 10,
opacity: 0.1,
},
{
repeat: true, // 循环
duration: 3000,
easing: 'easeCubic',
delay: 2000, // 2 秒延迟
},
);
},
},
'circle',
);
```
#### 部分图形旋转动画
这一例也是在 `afterDraw` 方法中为已有节点添加额外的 shape (本例中为 image并为这些新增的图形设置旋转动画。旋转动画较为复杂需要通过矩阵的操作实现。`animate` 函数的第一个参数是返回每一帧需要变化的参数集的函数,其参数 `ratio` 是当前正在进行的一次动画的进度,范围 [0, 1];第二个参数是动画的参数,动画参数的具体配置参见 [animateCfg](#animateCfg)。<br />
<img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*uFQsQqxIa_QAAAAAAAAAAABkARQnAQ' alt='download' width='150'/>
```javascript
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,
},
// 在 G6 3.3 及之后的版本中,必须指定 name可以是任意字符串但需要在同一个自定义元素类型中保持唯一性
name: 'image-shape',
});
// 该图片 shape 的动画
image.animate(
(ratio) => {
// 每一帧的操作,入参 ratio这一帧的比例值Number。返回值这一帧需要变化的参数集Object
// 旋转通过矩阵来实现
// 当前矩阵
const matrix = [1, 0, 0, 0, 1, 0, 0, 0, 1];
// 目标矩阵
const toMatrix = Util.transform(matrix, [['r', ratio * Math.PI * 2]]);
// 返回这一帧需要的参数集,本例中只有目标矩阵
return {
matrix: toMatrix,
};
},
{
repeat: true, // 动画重复
duration: 3000,
easing: 'easeCubic',
},
);
},
},
'rect',
);
```
### 边动画
关于边动画,以下面三个动画示例进行讲解:
- 圆点在沿着线运动(下图左);
- 虚线运动的效果下图中gif 图片的帧率问题导致看起来是静态的,可以访问下面的 demo 链接查看);
- 线从无到有的效果(下图右)。
<img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*OAGPRZbYpw4AAAAAAAAAAABkARQnAQ' alt='download' width='150'/>
<img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*VUgETK6aMzcAAAAAAAAAAABkARQnAQ' alt='download' width='150'/>
<img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*-l9lQ7Ck1QcAAAAAAAAAAABkARQnAQ' alt='download' width='150'/>
以上三个边动画的 demo 代码见:<a href='/zh/examples/scatter/edge' target='_blank'>边动画</a>
#### 圆点运动
本例通过在 `afterDraw` 方法中为边增加了一个 circle 图形,该图形沿着线运动。沿着线运动的原理:设定每一帧中,该 circle 在线上的相对位置。`animate` 函数的第一个参数是返回每一帧需要变化的参数集的函数,其参数 `ratio` 是当前正在进行的一次动画的进度,范围 [0, 1];第二个参数是动画的参数,动画参数的具体配置参见 [animateCfg](#animateCfg)。<br />
<img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*OAGPRZbYpw4AAAAAAAAAAABkARQnAQ' alt='download' width='150'/>
```javascript
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,
},
// 在 G6 3.3 及之后的版本中,必须指定 name可以是任意字符串但需要在同一个自定义元素类型中保持唯一性
name: 'circle-shape',
});
// 对红色圆点添加动画
circle.animate(
(ratio) => {
// 每一帧的操作,入参 ratio这一帧的比例值Number。返回值这一帧需要变化的参数集Object
// 根据比例值,获得在边 path 上对应比例的位置。
const tmpPoint = shape.getPoint(ratio);
// 返回需要变化的参数集,这里返回了位置 x 和 y
return {
x: tmpPoint.x,
y: tmpPoint.y,
};
},
{
repeat: true, // 动画重复
duration: 3000,
},
); // 一次动画的时间长度
},
},
'cubic',
); // 该自定义边继承内置三阶贝塞尔曲线 cubic
```
#### 虚线运动的效果
虚线运动的效果是通过计算线的 `lineDash` ,并在每一帧中不断修改实现。`animate` 函数的第一个参数是返回每一帧需要变化的参数集的函数,其参数 `ratio` 是当前正在进行的一次动画的进度,范围 [0, 1];第二个参数是动画的参数,动画参数的具体配置参见 [animateCfg](#animateCfg)。<br />
<img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*VUgETK6aMzcAAAAAAAAAAABkARQnAQ' alt='download' width='150'/>
```javascript
// lineDash 的差值,可以在后面提供 util 方法自动计算
const lineDash = [4, 2, 1, 2];
G6.registerEdge(
'line-dash',
{
afterDraw(cfg, group) {
// 获得该边的第一个图形,这里是边的 path
const shape = group.get('children')[0];
let index = 0;
// 边 path 图形的动画
shape.animate(
() => {
index++;
if (index > 9) {
index = 0;
}
const res = {
lineDash,
lineDashOffset: -index,
};
// 返回需要修改的参数集,这里修改了 lineDash,lineDashOffset
return res;
},
{
repeat: true, // 动画重复
duration: 3000, // 一次动画的时长为 3000
},
);
},
},
'cubic',
); // 该自定义边继承了内置三阶贝塞尔曲线边 cubic
```
#### 线从无到有
线从无到有的动画效果,同样可以通过计算 `lineDash` 来实现。`animate` 函数的第一个参数是返回每一帧需要变化的参数集的函数,其参数 `ratio` 是当前正在进行的一次动画的进度,范围 [0, 1];第二个参数是动画的参数,动画参数的具体配置参见 [animateCfg](#animateCfg)。<br />
<img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*-l9lQ7Ck1QcAAAAAAAAAAABkARQnAQ' alt='download' width='150'/>
```javascript
G6.registerEdge(
'line-growth',
{
afterDraw(cfg, group) {
const shape = group.get('children')[0];
const length = group.getTotalLength();
shape.animate(
(ratio) => {
const startLen = ratio * length;
// 计算 lineDash
const cfg = {
lineDash: [startLen, length - startLen],
};
return cfg;
},
{
repeat: true, // 是否重复执行
duration: 2000, // 一次动画持续时长
},
);
},
},
'cubic',
); // 该自定义边继承了内置三阶贝塞尔曲线边 cubic
```
### 交互动画
在交互的过程中也可以添加动画。如下图所示,当鼠标移到节点上时,所有与该节点相关联的边都展示虚线运动的动画。<br />![交互动画.gif](https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*-90pSrm4hkUAAAAAAAAAAABkARQnAQ)<br />上图完整 demo 即代码参见:<a href='/zh/examples/scatter/stateChange' target='_blank'>状态切换动画</a>
这种动画涉及到了边的 [状态](/zh/docs/manual/middle/states/state)。在自定义边时复写 `setState` 方法,可对边的各种状态进行监听。鼠标移动到节点上,相关边的某个状态被开启,`setState` 方法中监听到后开启动画效果。步骤如下:
- 自定义边中复写 `setState` 方法监听该边的状态,以及某状态下的动画效果;
- 监听中间的节点的 `mouseenter``mouseleave` 事件,触发相关边的状态变化。
下面代码节选自 demo <a href='/zh/examples/scatter/stateChange' target='_blank'>状态切换动画</a>,请注意省略了部分代码,只展示了交互相关以及边动画相关的代码。`animate` 函数的第一个参数是返回每一帧需要变化的参数集的函数,其参数 `ratio` 是当前正在进行的一次动画的进度,范围 [0, 1];第二个参数是动画的参数,动画参数的具体配置参见 [animateCfg](#animateCfg)。
```javascript
// const data = ...
// const graph = new G6.Graph({...});
const lineDash = [4, 2, 1, 2];
// 注册名为 'can-running' 的边
G6.registerEdge(
'can-running',
{
// 复写setState方法
setState(name, value, item) {
const shape = item.get('keyShape');
// 监听 running 状态
if (name === 'running') {
// running 状态为 true 时
if (value) {
let index = 0; // 边 path 图形的动画
shape.animate(
() => {
index++;
if (index > 9) {
index = 0;
}
const res = {
lineDash,
lineDashOffset: -index,
};
// 返回需要修改的参数集,这里修改了 lineDash,lineDashOffset
return res;
},
{
repeat: true, // 动画重复
duration: 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();
```
<span style="background-color: rgb(251, 233, 231); color: rgb(139, 53, 56)"> &nbsp;&nbsp;<strong>⚠️ 注意:</strong></span> `running``false` 时,要停止动画,同时把 `lineDash` 清空。
## animateCfg
| 配置项 | 类型 | 默认值 | 描述 |
| -------------- | -------- | -------------- | ---------------------------------------- |
| duration | Number | 500 | 一次动画的时长 |
| easing | boolean | 'linearEasing' | 动画函数,见 [easing 函数](#easing-函数) |
| delay | Number | 0 | 延迟一段时间后执行动画 |
| repeat | boolean | false | 是否重复执行动画 |
| callback | Function | undefined | 动画执行完时的回调函数 |
| pauseCallback | Function | undefined | 动画暂停时(`shape.pause()`)的回调函数 |
| resumeCallback | Function | undefined | 动画恢复时(`shape.resume()`)的回调函数 |
### easing 函数
easing 函数是指动画的函数。例如线性插值、先快后慢等。<br />G6 支持所有 d3.js 中的动画函数。因此,上面代码中 `animateCfg` 配置中的 String 类型的 `easing` 可以取值有:<br />`'easeLinear'` <br />`'easePolyIn'` `'easePolyOut'` `'easePolyInOut'` <br />`'easeQuad'` `'easeQuadIn'` `'easeQuadOut'`  `'easeQuadInOut'`
更多取值及所有取值含义参见:<a href='https://github.com/d3/d3/blob/master/API.md#easings-d3-ease' target='_blank'>d3 Easings</a>