如何快速绘制带动效的定制版流程图

上周有个业务组让我们做一个流程图,数据是固定不变的,但是要求流程之间的线条可以动起来。考虑到这个不通用,且我们这边也抽不出人力,因此我让他们自己的开发去学一下,自己写。

后面我空的时候想了下,这个需求应该还是有不少场景可能会用到,因此自己尝试写了下,这里做个笔记。

效果展示

先看下效果,类似这样:

技术选型

我大致采用如下的流程来实现这个效果:

1、用ProcessOn画出静态流程图,导出为SVG文件

2、将SVG内容放入网页,形成一个静态HTML

3、通过JS解析SVG中的线条信息,即path标签的d属性

4、通过D3.js画出圆点,并给其设置transition动画,修改transform属性来实现动效

代码

代码里面唯一特殊的地方,就是如果一条线分成了多个折线,那么每一个直线我都会单独通过setTimeout设置动画:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
d3.selectAll('path')
.attr('test', function () {
let d = d3.select(this).attr('d');
let id = d3.select(this).attr('id');
// SvgjsPath1096 折线
// SvgjsPath1124 直线

if (id === 'SvgjsPath1096') {
console.log(`%c ${d}`, 'color:red');
} else {
// return false;
console.log(d);
}

let paths = dAttrToPath(d)
if (paths.length <= 0) {
return false;
}

let ball = d3.select(this.parentNode)
.append('circle')
.attr('r', 10)
.attr('fill', 'red')
.classed('ball', true)

// 绑定球的移动动画
play(paths, ball, 1)
})


// 因为路径可能是由多条线段组成的,所以要对每一段线段都绑定动画
// 为了保持匀速,绑定的时间应该根据线段的长度来计算
// 注意:这里的参数i是目标位置的i,不是起点的i
function play(paths, ball, i)
{
if (i === 1) {
ball.attr('transform', `translate(${paths[0][0]}, ${paths[0][1]})`)
}
let currentCoordinate = i === 0 ? paths[0] : paths[i - 1]
let nextCoordinate = paths[i]
let duration = computeSpeed(currentCoordinate, nextCoordinate)

try {
ball.transition()
.attr('transform', `translate(${paths[i][0]}, ${paths[i][1]})`)
.duration(duration)
// 平滑动画,详见:https://github.com/d3/d3-ease
.ease(d3.easeLinear)
.on('end', async function () {
if (i === paths.length - 1) {
play(paths, ball, 1)
} else {
play(paths, ball, i + 1)
}
})
} catch (e) {
console.log(`translate(${paths[i][0]}, ${paths[i][1]})`);
}
}

/**
* 计算2个坐标点的距离
*/
function computeSpeed(coordinate1, coordinate2)
{
let distance = Math.sqrt(Math.pow(coordinate1[0] - coordinate2[0], 2) + Math.pow(coordinate1[1] - coordinate2[1], 2))
return distance * 20;
}

/**
* 将d属性转换为坐标数组
* @param {*} dValue
*/
function dAttrToPath(dValue) {
let coordinates = [];
// 临时先排除圆弧
if (dValue.includes('C') || dValue.includes('Z')) {
return coordinates;
}
dValue.replace('M', '').split('L').forEach(d => {
let coord = d.trim().split(' ');
// 剔除非线条的内容,比如方框
if (coord.length > 1) {
coordinates.push(d.trim().split(' '))
}
})

// 去重
for (let i = 1; i < coordinates.length; i ++) {
// coordinates[i - 1]是为了确保上一个没被删除过
if (coordinates[i - 1] && coordinates[i][0] === coordinates[i - 1][0] && coordinates[i][1] === coordinates[i - 1][1]) {
delete coordinates[i]
}
}

let result = [];
coordinates.forEach(d => result.push(d))

return result;
}