0%

SVG Path 补间动画

SVG元素Path标签的d属性同样可以使用transition来实现平滑过度、本文简单介绍如何使用transition来平滑过度path标签的路径,有那些限制,以及如何解决。

简单实践

以上为SVG元素Path标签使用transition的一个简单实例,可以看到PathL,C, Q, A等属性均可实现平滑过度,关于SVG Path的属性可以见SVG Path 属性

实现条件

简单做个实验验证在什么情况下可以实现平滑过渡。

场景 是否可以平滑过渡
路径命令完全一致且长度一致
路径命令大小写不同且长度一致
路径命令不一致
路径命令长度不一致

但在一些稍微复杂点的场景下除非在设计之初就有考量,不然很难有命令类型且长度一致的两个Path

真实场景

有如下两个图形,可以看到太阳的圆形以及月亮均是由path来实现的,要实现太阳的圆形的Path直接平滑过度到月亮的Path根据上表可知无法直接实现。

实现原理

这两个Path无法直接平滑过度的原因即Path的命令长度和类型不一致,如果我们可以在不破坏原有图案的前提下将两个Path的命令和长度调整为一致即可实现平滑过度。从以上元素路径中多数命令为ca。我们可以通过一下步骤在不破坏图案类型的前提下将两个Path调整为命令和长度一致。

  1. 将两个Path的命令取并集
  2. 在原有的Path插入一些命令节点
  3. 使新插入的命令节点的前后坐标一致

以上

实现过程

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
// 太阳的圆形路径
const sumPathString = `M512 789.302c-152.9 0-277.302-124.372-277.302-277.302 0-152.916 124.402-277.32 277.302-277.32 152.934 0 277.336 124.404 277.336 277.32 0 152.932-124.402 277.302-277.336 277.302z`;

const moonPathString = `M524.8 938.666667h-4.266667a439.893333 439.893333 0 0 1-313.173333-134.4 446.293333 446.293333 0 0 1-11.093333-597.333334 432.213333 432.213333 0 0 1 170.666666-116.906666 42.666667 42.666667 0 0 1 45.226667 9.386666 42.666667 42.666667 0 0 1 10.24 42.666667 358.4 358.4 0 0 0 82.773333 375.893333 361.386667 361.386667 0 0 0 376.746667 82.773334 42.666667 42.666667 0 0 1 54.186667 55.04A433.493333 433.493333 0 0 1 836.266667 810.666667a438.613333 438.613333 0 0 1-311.466667 128z`

// parse(sumPathString)
// 解析路径
const sumPathArray = [
['M', 512, 789.302],
['c', -152.9, 0, -277.302, -124.372, -277.302, -277.302],
['c', 0, -152.916, 124.402, -277.32, 277.302, -277.32],
['c', 152.934, 0, 277.336, 124.404, 277.336, 277.32],
['c', 0, 152.932, -124.402, 277.302, -277.336, 277.302],
['z'],
];

// parse(moonPathString)
// 解析路径

const moonPathString = [
['M', 524.8, 938.666667],
['h', -4.266667],
['a', 439.893333, 439.893333, 0, 0, 1, -313.173333, -134.4],
['a', 446.293333, 446.293333, 0, 0, 1, -11.093333, -597.333334],
['a', 432.213333, 432.213333, 0, 0, 1, 170.666666, -116.906666],
['a', 42.666667, 42.666667, 0, 0, 1, 45.226667, 9.386666],
['a', 42.666667, 42.666667, 0, 0, 1, 10.24, 42.666667],
['a', 358.4, 358.4, 0, 0, 0, 82.773333, 375.893333],
['a', 361.386667, 361.386667, 0, 0, 0, 376.746667, 82.773334],
['a', 42.666667, 42.666667, 0, 0, 1, 54.186667, 55.04],
['A', 433.493333, 433.493333, 0, 0, 1, 836.266667, 810.666667],
['a', 438.613333, 438.613333, 0, 0, 1, -311.466667, 128],
['z'],
];


const sumPathArrayMergedMoon = [
['M', 512, 789.302],
// 这里增加了一个h命令(水平移动,移动距离为0)且开始于结束节点一致,对图形本身无影响
['h', 0],
// 在这里处插入了一个a命令(画弧、且移动离为0)且开始于结束节点一致,对图形本身无影响
// 以下在间隔着插入moonPath中的指令,且保证该指令的开始于结束节点一致
['a', 439.893333, 439.893333, 0, 0, 1, 0, 0],
['c', -152.9, 0, -277.302, -124.372, -277.302, -277.302],
['a', 446.293333, 446.293333, 0, 0, 1, 0, 0],
['a', 432.213333, 432.213333, 0, 0, 1, 0, 0],
['a', 42.666667, 42.666667, 0, 0, 1, 0, 0],
['a', 42.666667, 42.666667, 0, 0, 1, 0, 0],
['c', 0, -152.916, 124.402, -277.32, 277.302, -277.32],
['a', 358.4, 358.4, 0, 0, 0, 0, 0],
['a', 361.386667, 361.386667, 0, 0, 0, 0, 0],
['c', 152.934, 0, 277.336, 124.404, 277.336, 277.32],
['a', 42.666667, 42.666667, 0, 0, 1, 0, 0],
['A', 433.493333, 433.493333, 0, 0, 1, 789.336, 512],
['a', 438.613333, 438.613333, 0, 0, 1, 0, 0],
['c', 0, 152.932, -124.402, 277.302, -277.336, 277.302],
['z'],
]


const moonPathArrayMergedSun = [
['M', 524.8, 938.666667],
['h', -4.266667],
['a', 439.893333, 439.893333, 0, 0, 1, -313.173333, -134.4],
// 这里插入了一个c命令(画弧且移动距离为0)且开始于结束节点一致,对图形本身无影响
// 以下在间隔着插入sunPath中的指令,且保证该指令的开始于结束节点一致
['c', 0, 0, 0, 0, 0, 0],
['a', 446.293333, 446.293333, 0, 0, 1, -11.093333, -597.333334],
['a', 432.213333, 432.213333, 0, 0, 1, 170.666666, -116.906666],
['a', 42.666667, 42.666667, 0, 0, 1, 45.226667, 9.386666],
['a', 42.666667, 42.666667, 0, 0, 1, 10.24, 42.666667],
['c', 0, 0, 0, 0, 0, 0],
['a', 358.4, 358.4, 0, 0, 0, 82.773333, 375.893333],
['a', 361.386667, 361.386667, 0, 0, 0, 376.746667, 82.773334],
['c', 0, 0, 0, 0, 0, 0],
['a', 42.666667, 42.666667, 0, 0, 1, 54.186667, 55.04],
['A', 433.493333, 433.493333, 0, 0, 1, 836.266667, 810.666667],
['a', 438.613333, 438.613333, 0, 0, 1, -311.466667, 128],
['c', 0, 0, 0, 0, 0, 0],
['z'],
]

// pathCommandsToString(sumPathArrayMergedMoon)

const sumPathStringMergedMoon = `M 512 789.302,h 0,a 439.893333 439.893333 0 0 1 0 0,c -152.9 0 -277.302 -124.372 -277.302 -277.302,a 446.293333 446.293333 0 0 1 0 0,a 432.213333 432.213333 0 0 1 0 0,a 42.666667 42.666667 0 0 1 0 0,a 42.666667 42.666667 0 0 1 0 0,c 0 -152.916 124.402 -277.32 277.302 -277.32,a 358.4 358.4 0 0 0 0 0,a 361.386667 361.386667 0 0 0 0 0,c 152.934 0 277.336 124.404 277.336 277.32,a 42.666667 42.666667 0 0 1 0 0,A 433.493333 433.493333 0 0 1 789.336 512,a 438.613333 438.613333 0 0 1 0 0,c 0 152.932 -124.402 277.302 -277.336 277.302,z`

// pathCommandsToString(moonPathArrayMergedSun)

const moonPathArrayMergedSun = `M 524.8 938.666667,h -4.266667,a 439.893333 439.893333 0 0 1 -313.173333 -134.4,c 0 0 0 0 0 0,a 446.293333 446.293333 0 0 1 -11.093333 -597.333334,a 432.213333 432.213333 0 0 1 170.666666 -116.906666,a 42.666667 42.666667 0 0 1 45.226667 9.386666,a 42.666667 42.666667 0 0 1 10.24 42.666667,c 0 0 0 0 0 0,a 358.4 358.4 0 0 0 82.773333 375.893333,a 361.386667 361.386667 0 0 0 376.746667 82.773334,c 0 0 0 0 0 0,a 42.666667 42.666667 0 0 1 54.186667 55.04,A 433.493333 433.493333 0 0 1 836.266667 810.666667,a 438.613333 438.613333 0 0 1 -311.466667 128,c 0 0 0 0 0 0,z`

效果

鼠标移入会有效果。


结束

以上手动演示了一遍如何合并命令数组、输入插入绘制命令、如何保证开始节点于结束节点一致。上述演示中为了保证动画尽量平滑,采用的为间隔插入的方式。由于为手动演示对于c(三次贝塞尔曲线)以及a(弧线)绘制命令的控制参数均未做优化仅处理了长度。所以过渡效果仍不够平滑,这部分可做深入优化。

自动化的实例可以看alexk111/SVG-Morpheus

工具函数

Parse svg path

来源jkroso/parse-svg-path

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
/**
* expected argument lengths
* @type {Object}
*/
var length = {a: 7, c: 6, h: 1, l: 2, m: 2, q: 4, s: 4, t: 2, v: 1, z: 0}

/**
* segment pattern
* @type {RegExp}
*/

var segment = /([astvzqmhlc])([^astvzqmhlc]*)/ig

/**
* parse an svg path data string. Generates an Array
* of commands where each command is an Array of the
* form `[command, arg1, arg2, ...]`
*
* @param {String} path
* @return {Array}
*/

function parse(path) {
var data = []
path.replace(segment, function(_, command, args){
var type = command.toLowerCase()
args = parseValues(args)

// overloaded moveTo
if (type == 'm' && args.length > 2) {
data.push([command].concat(args.splice(0, 2)))
type = 'l'
command = command == 'm' ? 'l' : 'L'
}

while (true) {
if (args.length == length[type]) {
args.unshift(command)
return data.push(args)
}
if (args.length < length[type]) throw new Error('malformed path data')
data.push([command].concat(args.splice(0, length[type])))
}
})
return data
}

var number = /-?[0-9]*\.?[0-9]+(?:e[-+]?\d+)?/ig

function parseValues(args) {
var numbers = args.match(number)
return numbers ? numbers.map(Number) : []
}

Path command to String

1
2
3
4
5
6
7
8
var pathCommandsToString = function (pathArray) {
let res = '';
for (let i = 0; i < pathArray.length - 1; i++) {
res += pathArray[i].join(' ') + ',';
}
res += pathArray[pathArray.length - 1].join(' ');
return res;
};

pathToAbsolute

来源alexk111/SVG-Morpheus

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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
var pathToString = function (pathArray) {
if (!pathArray || !pathArray.length) {
return [['M', 0, 0]];
}
var res = [],
x = 0,
y = 0,
mx = 0,
my = 0,
start = 0,
pa0;
if (pathArray[0][0] == 'M') {
x = +pathArray[0][1];
y = +pathArray[0][2];
mx = x;
my = y;
start++;
res[0] = ['M', x, y];
}
var crz =
pathArray.length == 3 &&
pathArray[0][0] == 'M' &&
pathArray[1][0].toUpperCase() == 'R' &&
pathArray[2][0].toUpperCase() == 'Z';
for (var r, pa, i = start, ii = pathArray.length; i < ii; i++) {
res.push((r = []));
pa = pathArray[i];
pa0 = pa[0];
if (pa0 != pa0.toUpperCase()) {
r[0] = pa0.toUpperCase();
switch (r[0]) {
case 'A':
r[1] = pa[1];
r[2] = pa[2];
r[3] = pa[3];
r[4] = pa[4];
r[5] = pa[5];
r[6] = +pa[6] + x;
r[7] = +pa[7] + y;
break;
case 'V':
r[1] = +pa[1] + y;
break;
case 'H':
r[1] = +pa[1] + x;
break;
case 'R':
var dots = [x, y].concat(pa.slice(1));
for (var j = 2, jj = dots.length; j < jj; j++) {
dots[j] = +dots[j] + x;
dots[++j] = +dots[j] + y;
}
res.pop();
res = res.concat(catmullRom2bezier(dots, crz));
break;
case 'O':
res.pop();
dots = ellipsePath(x, y, pa[1], pa[2]);
dots.push(dots[0]);
res = res.concat(dots);
break;
case 'U':
res.pop();
res = res.concat(ellipsePath(x, y, pa[1], pa[2], pa[3]));
r = ['U'].concat(res[res.length - 1].slice(-2));
break;
case 'M':
mx = +pa[1] + x;
my = +pa[2] + y;
default:
for (j = 1, jj = pa.length; j < jj; j++) {
r[j] = +pa[j] + (j % 2 ? x : y);
}
}
} else if (pa0 == 'R') {
dots = [x, y].concat(pa.slice(1));
res.pop();
res = res.concat(catmullRom2bezier(dots, crz));
r = ['R'].concat(pa.slice(-2));
} else if (pa0 == 'O') {
res.pop();
dots = ellipsePath(x, y, pa[1], pa[2]);
dots.push(dots[0]);
res = res.concat(dots);
} else if (pa0 == 'U') {
res.pop();
res = res.concat(ellipsePath(x, y, pa[1], pa[2], pa[3]));
r = ['U'].concat(res[res.length - 1].slice(-2));
} else {
for (var k = 0, kk = pa.length; k < kk; k++) {
r[k] = pa[k];
}
}
pa0 = pa0.toUpperCase();
if (pa0 != 'O') {
switch (r[0]) {
case 'Z':
x = +mx;
y = +my;
break;
case 'H':
x = r[1];
break;
case 'V':
y = r[1];
break;
case 'M':
mx = r[r.length - 2];
my = r[r.length - 1];
default:
x = r[r.length - 2];
y = r[r.length - 1];
}
}
}


return res;
};

参考 & 引用

https://codepen.io/chriscoyier/pen/wvBZyXX

https://github.com/alexk111/SVG-Morpheus

https://codepen.io/chriscoyier/pen/NRwANp