如何使用 Anime.js 将 svg 正确地变形为另一个 svg?
How to morph svg into another svg correctly with Anime.js?
我有一个问题,我的两个 svg 具有 相同数量的点,但是当我播放动画时有些不对劲,两个 svg 靠得太近了但是动画不知从哪里跳出来,这是不对的,在第一个 svg 变为第二个之前出现了一个奇怪的形状。
我正在 Adobe XD 中制作 svg。这是代码:
<svg id="morph" viewBox="0 0 1920 540">
<path class="morph" d="m864.216 135.95 36.39 41.917S780.519 307.11 1078.914 373.479s221.979-87.327 221.979-87.327l32.75-34.931s25.473 101.3 207.422 34.931 440.314 150.2 411.2 380.744S34.528 576.079 34.528 576.079s-3.64-429.647 342.063-509.987 272.923 174.653 487.623 69.861"/>
</svg>
<script>
var overlay = document.getElementById('morph');
var morphing = anime({
targets: '.morph',
d: [
{value : "m864.216 135.95 36.39 41.917S780.519 307.11 1078.914 373.479s221.979-87.327 221.979-87.327l32.75-34.931s25.473 101.3 207.422 34.931 440.314 150.2 411.2 380.744S34.528 576.079 34.528 576.079s-3.64-429.647 342.063-509.987 272.923 174.653 487.623 69.861"},
{value: "M2.49 576.483S20.535 398.736 122.472 239.61s236.674-199.127 302-217.883c176.407-41.244 334 45.685 334 45.685l340 233.7s172 105.427 280 119.484 322 12.3 322 12.3 118 5.271 160 61.5 56 89.613 62 117.727S2.49 576.483 2.49 576.483Z"},
],
duration: 1200,
loop: false,
easing: 'easeInOutQuint'
})
</script>
您的路径仍需要一些优化才能完全兼容插值。
大多数动画库都试图使路径在某种程度上兼容(例如通过将它们转换为多边形,如 flubber.js)。
但通常手动清理路径会获得最佳结果。
step 1
step 2
m 864.216 135.95
M 2.49 576.483
l 36.39 41.917
S 20.535 398.736 122.472 239.61
S 780.519 307.11 1078.914 373.479
s 236.674 -199.127 302-217.883
s 221.979 -87.327 221.979 -87.327
c 176.407 -41.244 334 45.685 334 45.685
l 32.75 -34.931
l 340 233.7
s 25.473 101.3 207.422 34.931
s 172 105.427 280 119.484
s 440.314 150.2 411.2 380.744
s 322 12.3 322 12.3
S 34.528 576.079 34.528 576.079
s 118 5.271 160 61.5
s -3.64 -429.647 342.063-509.987
s 56 89.613 62 117.727
s 272.923 174.653 487.623 69.861
S 2.49 576.483 2.49 576.483
Z
您的命令类型也需要兼容。
例如第二个动画步骤只有一个 l
(lineTo) 命令和第一个路径中缺少的 Z
(closePath)。
不幸的是,您无法确定您的 editor/graphic 应用会输出与它可能决定使用 shorthand 命令(如 h 表示水平 lineTos)来缩小标记相同的命令。
将 d
标准化为一组减少的命令
这将通过将路径数据转换为 M、C、L 和 Z 命令来简化进一步的调整。
我正在使用 Jarek Foksa's getPathData polyfill.
path.getPathData({normalize: true});
{normalize: true}
参数也会将所有命令转换为绝对坐标。
将 L 命令转换为 C
您可以通过像这样重复 x/y 坐标轻松地将 L
命令转换为 C
曲线。
L 901 178
至:
C 901 178 901 178 901 178
调整M
个起点
将起点设置为类似于最左边的东西 corner/point。因此,您的路径将使用视觉参考点进行插值。
否则你可能会得到奇怪的翻转过渡。
使用绝对和规范化命令更改路径的 M
也更容易。您还会在代码段中找到辅助函数
(1.路径数据块
=> 将附加在第二个块之后)
M 864 136 (旧起点=>将被删除)
C 901 178 901 178 901 178
C 901 178 781 307 1079 373
C 1377 440 1301 286 1301 286
C 1334 251 1334 251 1334 251
C 1334 251 1359 353 1541 286
C 1723 220 1981 436 1952 667
C 1923 897 35 576 35 576 => 将成为新的 M xy 坐标
(2.路径数据块)
C 35 576 31 146 377 66
C 722 -14 650 241 864 136
(3.路径数据块:closePath)
Z
结果:
M 35 576
C 35 576 31 146 377 66
C 722 -14 650 241 864 136
C 901 178 901 178 901 178
C 901 178 781 307 1079 373
C 1377 440 1301 286 1301 286
C 1334 251 1334 251 1334 251
C 1334 251 1359 353 1541 286
C 1723 220 1981 436 1952 667
C 1923 897 35 576 35 576
Z
路径方向
在你的例子中,两条路径都是顺时针方向。
如果您遇到奇怪的翻转过渡,您可以尝试反转路径方向。
您可以使用 @enxaneta's great codepen example 或
Svg Path commander Library.
示例 1:规范化和更改起点(包括助手)
let svgNorm = document.querySelectorAll('.svgNorm');
svgNorm.forEach(function(svg) {
let svgPaths = svg.querySelectorAll('path');
normalizePaths(svgPaths, 0, true);
})
let orig1 = document.querySelector('.orig1');
let orig2 = document.querySelector('.orig2');
let path1 = document.querySelector('.morph1');
let path2 = document.querySelector('.morph2');
//shift starting point
shiftSvgStartingPoint(path1, 7);
//show starting points
addMarkers(orig1);
addMarkers(orig2);
addMarkers(path1);
addMarkers(path2);
function normalizePaths(paths, decimals = 1, convertLineto = false) {
paths.forEach(function(path, i) {
let pathData = path.getPathData({
normalize: true
});
pathData.forEach(function(com) {
let [type, values] = [com['type'], com['values']];
values.forEach(function(coord, c) {
com['values'][c] = +(com['values'][c]).toFixed(decimals)
})
let [x, y] = [com['values'][0], com['values'][1]];
if (type == 'L' && convertLineto) {
com['type'] = 'C';
com['values'] = [x, y, x, y, x, y];
}
})
path.setPathData(pathData)
})
}
function shiftSvgStartingPoint(path, offset) {
let pathData = path.getPathData({
normalize: true
});
let pathDataL = pathData.length;
//exclude Z/z (closepath) command if present
let lastCommand = (pathData[pathDataL - 1]['type']);
let trimR = 0;
if (lastCommand == 'Z') {
trimR = 1;
}
let newStartIndex = offset + 1 < pathData.length - 1 ? offset + 1 : pathData.length - 1 - trimR;
let newPathData = pathData;
let newPathDataL = newPathData.length;
// slice array to reorder
let newPathDataStart = newPathData.slice(newStartIndex);
let newPathDataEnd = newPathData.slice(0, newStartIndex);
// remove original M
newPathDataEnd.shift();
let newPathDataEndL = newPathDataEnd.length;
let newPathDataEndLastValues = newPathDataEnd[newPathDataEndL - 1]['values'];
let newPathDataEndLastXY = [newPathDataEndLastValues[newPathDataEndLastValues.length - 2],
newPathDataEndLastValues[newPathDataEndLastValues.length - 1]
];
//remove z(close path) from original pathdata array
if (trimR) {
newPathDataStart.pop();
newPathDataEnd.push({
'type': 'Z',
'values': []
});
}
// prepend new M command and concatenate array chunks
newPathData = [{
'type': 'M',
'values': newPathDataEndLastXY
}].concat(newPathDataStart).concat(newPathDataEnd);
// update path's d property
path.setPathData(newPathData);
return path;
}
testInterpolation(path1, path2);
function testInterpolation(path1, path2) {
path1.addEventListener('click', function(e) {
if (!path1.getAttribute('style')) {
path1.setAttribute('style', `d:path("${path2.getAttribute('d')}")`)
} else {
path1.removeAttribute('style');
}
})
}
function addMarkers(path) {
let svg = path.closest('svg');
let markerDef = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
let marker =
`<marker id="circle" viewBox="0 0 10 10" refX="5" refY="5" markerWidth="10%" markerHeight="10%"
orient="auto-start-reverse">
<circle cx="5" cy="5" r="5" fill="green" />
</marker>`;
markerDef.innerHTML = marker;
svg.insertBefore(markerDef, svg.childNodes[0]);
path.setAttribute('marker-start', 'url(#circle)');
}
svg {
display: inline-block;
width: 30%;
overflow: visible;
border: 1px solid #ccc;
margin-right: 5%;
}
.row {
margin-top: 4em;
}
path {
opacity: 0.5;
transition: 0.5s;
}
<script src="https://cdn.jsdelivr.net/npm/path-data-polyfill@1.0.3/path-data-polyfill.min.js"></script>
<div>
<p>Green points illustrate starting points</p>
<svg viewBox="0 0 1920 540">
<path class="orig1" d="
m 864.216 135.95
l 36.39 41.917
S 780.519 307.11 1078.914 373.479
s 221.979 -87.327 221.979 -87.327
l 32.75 -34.931
s 25.473 101.3 207.422 34.931
s 440.314 150.2 411.2 380.744
S 34.528 576.079 34.528 576.079
s -3.64 -429.647 342.063-509.987
s 272.923 174.653 487.623 69.861
z" />
</svg>
<svg viewBox="0 0 1920 540">
<path class="orig2" d="
M 2.49 576.483
S 20.535 398.736 122.472 239.61
s 236.674 -199.127 302-217.883
c 176.407 -41.244 334 45.685 334 45.685
l 340 233.7
s 172 105.427 280 119.484
s 322 12.3 322 12.3
s 118 5.271 160 61.5
s 56 89.613 62 117.727
S 2.49 576.483 2.49 576.483
Z" />
</svg>
</div>
<div class="row">
<svg class="svgNorm" viewBox="0 0 1920 540">
<path class="morph1"
d="
m 864.216 135.95
l 36.39 41.917
S 780.519 307.11 1078.914 373.479
s 221.979 -87.327 221.979 -87.327
l 32.75 -34.931
s 25.473 101.3 207.422 34.931
s 440.314 150.2 411.2 380.744
S 34.528 576.079 34.528 576.079
s -3.64 -429.647 342.063-509.987
s 272.923 174.653 487.623 69.861
z" />
</svg>
<svg class="svgNorm" viewBox="0 0 1920 540">
<path class="morph2"
d="
M 2.49 576.483
S 20.535 398.736 122.472 239.61
s 236.674 -199.127 302-217.883
c 176.407 -41.244 334 45.685 334 45.685
l 340 233.7
s 172 105.427 280 119.484
s 322 12.3 322 12.3
s 118 5.271 160 61.5
s 56 89.613 62 117.727
S 2.49 576.483 2.49 576.483
Z" />
</svg>
<p>Click on the left path to see morphing animation. <br />Inspect this path in DevTools to get new compatible path data.</p>
</div>
示例 2:使用 anime.js
变形优化路径
var morphing = anime({
targets: ".morph",
d: [
{
value:
"M 2 576 C 2 576 21 399 122 240 C 224 80 359 40 424 22 C 601 -20 758 67 758 67 C 1098 301 1098 301 1098 301 C 1098 301 1270 407 1378 421 C 1486 435 1700 433 1700 433 C 1700 433 1818 438 1860 494 C 1902 551 1916 584 1922 612 C 1928 640 2 576 2 576 Z"
}
],
duration: 1200,
loop: false,
easing: "easeInOutQuint"
});
svg{
display:inline-block;
width:20em;
overflow:visible;
}
.morph{
transition:0.5s;
}
.morphPoly{
transition:0.5s;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/animejs/3.2.1/anime.min.js"></script>
<svg id="morph" viewBox="0 0 1920 540">
<path class="morph" d="M 35 576 C 35 576 31 146 377 66 C 722 -14 650 241 864 136 C 901 178 901 178 901 178 C 901 178 781 307 1079 373 C 1377 440 1301 286 1301 286 C 1334 251 1334 251 1334 251 C 1334 251 1359 353 1541 286 C 1723 220 1981 436 1952 667 C 1923 897 35 576 35 576 Z" />
</svg>
...相当多的工作。
但是一旦你的路径超级兼容,你也可以通过普通 css 在形状之间变形。 (例如,通过 animating/transitioning d:path()
属性)。
我有一个问题,我的两个 svg 具有 相同数量的点,但是当我播放动画时有些不对劲,两个 svg 靠得太近了但是动画不知从哪里跳出来,这是不对的,在第一个 svg 变为第二个之前出现了一个奇怪的形状。
我正在 Adobe XD 中制作 svg。这是代码:
<svg id="morph" viewBox="0 0 1920 540">
<path class="morph" d="m864.216 135.95 36.39 41.917S780.519 307.11 1078.914 373.479s221.979-87.327 221.979-87.327l32.75-34.931s25.473 101.3 207.422 34.931 440.314 150.2 411.2 380.744S34.528 576.079 34.528 576.079s-3.64-429.647 342.063-509.987 272.923 174.653 487.623 69.861"/>
</svg>
<script>
var overlay = document.getElementById('morph');
var morphing = anime({
targets: '.morph',
d: [
{value : "m864.216 135.95 36.39 41.917S780.519 307.11 1078.914 373.479s221.979-87.327 221.979-87.327l32.75-34.931s25.473 101.3 207.422 34.931 440.314 150.2 411.2 380.744S34.528 576.079 34.528 576.079s-3.64-429.647 342.063-509.987 272.923 174.653 487.623 69.861"},
{value: "M2.49 576.483S20.535 398.736 122.472 239.61s236.674-199.127 302-217.883c176.407-41.244 334 45.685 334 45.685l340 233.7s172 105.427 280 119.484 322 12.3 322 12.3 118 5.271 160 61.5 56 89.613 62 117.727S2.49 576.483 2.49 576.483Z"},
],
duration: 1200,
loop: false,
easing: 'easeInOutQuint'
})
</script>
您的路径仍需要一些优化才能完全兼容插值。
大多数动画库都试图使路径在某种程度上兼容(例如通过将它们转换为多边形,如 flubber.js)。
但通常手动清理路径会获得最佳结果。
step 1 | step 2 |
---|---|
m 864.216 135.95 | M 2.49 576.483 |
l 36.39 41.917 | S 20.535 398.736 122.472 239.61 |
S 780.519 307.11 1078.914 373.479 | s 236.674 -199.127 302-217.883 |
s 221.979 -87.327 221.979 -87.327 | c 176.407 -41.244 334 45.685 334 45.685 |
l 32.75 -34.931 | l 340 233.7 |
s 25.473 101.3 207.422 34.931 | s 172 105.427 280 119.484 |
s 440.314 150.2 411.2 380.744 | s 322 12.3 322 12.3 |
S 34.528 576.079 34.528 576.079 | s 118 5.271 160 61.5 |
s -3.64 -429.647 342.063-509.987 | s 56 89.613 62 117.727 |
s 272.923 174.653 487.623 69.861 | S 2.49 576.483 2.49 576.483 |
Z |
您的命令类型也需要兼容。
例如第二个动画步骤只有一个 l
(lineTo) 命令和第一个路径中缺少的 Z
(closePath)。
不幸的是,您无法确定您的 editor/graphic 应用会输出与它可能决定使用 shorthand 命令(如 h 表示水平 lineTos)来缩小标记相同的命令。
将 d
标准化为一组减少的命令
这将通过将路径数据转换为 M、C、L 和 Z 命令来简化进一步的调整。
我正在使用 Jarek Foksa's getPathData polyfill.
path.getPathData({normalize: true});
{normalize: true}
参数也会将所有命令转换为绝对坐标。
将 L 命令转换为 C
您可以通过像这样重复 x/y 坐标轻松地将 L
命令转换为 C
曲线。
L 901 178
至:
C 901 178 901 178 901 178
调整M
个起点
将起点设置为类似于最左边的东西 corner/point。因此,您的路径将使用视觉参考点进行插值。 否则你可能会得到奇怪的翻转过渡。
使用绝对和规范化命令更改路径的 M
也更容易。您还会在代码段中找到辅助函数
(1.路径数据块
=> 将附加在第二个块之后)
M 864 136 (旧起点=>将被删除)
C 901 178 901 178 901 178
C 901 178 781 307 1079 373
C 1377 440 1301 286 1301 286
C 1334 251 1334 251 1334 251
C 1334 251 1359 353 1541 286
C 1723 220 1981 436 1952 667
C 1923 897 35 576 35 576 => 将成为新的 M xy 坐标
(2.路径数据块)
C 35 576 31 146 377 66
C 722 -14 650 241 864 136
(3.路径数据块:closePath)
Z
结果:
M 35 576
C 35 576 31 146 377 66
C 722 -14 650 241 864 136
C 901 178 901 178 901 178
C 901 178 781 307 1079 373
C 1377 440 1301 286 1301 286
C 1334 251 1334 251 1334 251
C 1334 251 1359 353 1541 286
C 1723 220 1981 436 1952 667
C 1923 897 35 576 35 576
Z
路径方向
在你的例子中,两条路径都是顺时针方向。
如果您遇到奇怪的翻转过渡,您可以尝试反转路径方向。
您可以使用 @enxaneta's great codepen example 或
Svg Path commander Library.
示例 1:规范化和更改起点(包括助手)
let svgNorm = document.querySelectorAll('.svgNorm');
svgNorm.forEach(function(svg) {
let svgPaths = svg.querySelectorAll('path');
normalizePaths(svgPaths, 0, true);
})
let orig1 = document.querySelector('.orig1');
let orig2 = document.querySelector('.orig2');
let path1 = document.querySelector('.morph1');
let path2 = document.querySelector('.morph2');
//shift starting point
shiftSvgStartingPoint(path1, 7);
//show starting points
addMarkers(orig1);
addMarkers(orig2);
addMarkers(path1);
addMarkers(path2);
function normalizePaths(paths, decimals = 1, convertLineto = false) {
paths.forEach(function(path, i) {
let pathData = path.getPathData({
normalize: true
});
pathData.forEach(function(com) {
let [type, values] = [com['type'], com['values']];
values.forEach(function(coord, c) {
com['values'][c] = +(com['values'][c]).toFixed(decimals)
})
let [x, y] = [com['values'][0], com['values'][1]];
if (type == 'L' && convertLineto) {
com['type'] = 'C';
com['values'] = [x, y, x, y, x, y];
}
})
path.setPathData(pathData)
})
}
function shiftSvgStartingPoint(path, offset) {
let pathData = path.getPathData({
normalize: true
});
let pathDataL = pathData.length;
//exclude Z/z (closepath) command if present
let lastCommand = (pathData[pathDataL - 1]['type']);
let trimR = 0;
if (lastCommand == 'Z') {
trimR = 1;
}
let newStartIndex = offset + 1 < pathData.length - 1 ? offset + 1 : pathData.length - 1 - trimR;
let newPathData = pathData;
let newPathDataL = newPathData.length;
// slice array to reorder
let newPathDataStart = newPathData.slice(newStartIndex);
let newPathDataEnd = newPathData.slice(0, newStartIndex);
// remove original M
newPathDataEnd.shift();
let newPathDataEndL = newPathDataEnd.length;
let newPathDataEndLastValues = newPathDataEnd[newPathDataEndL - 1]['values'];
let newPathDataEndLastXY = [newPathDataEndLastValues[newPathDataEndLastValues.length - 2],
newPathDataEndLastValues[newPathDataEndLastValues.length - 1]
];
//remove z(close path) from original pathdata array
if (trimR) {
newPathDataStart.pop();
newPathDataEnd.push({
'type': 'Z',
'values': []
});
}
// prepend new M command and concatenate array chunks
newPathData = [{
'type': 'M',
'values': newPathDataEndLastXY
}].concat(newPathDataStart).concat(newPathDataEnd);
// update path's d property
path.setPathData(newPathData);
return path;
}
testInterpolation(path1, path2);
function testInterpolation(path1, path2) {
path1.addEventListener('click', function(e) {
if (!path1.getAttribute('style')) {
path1.setAttribute('style', `d:path("${path2.getAttribute('d')}")`)
} else {
path1.removeAttribute('style');
}
})
}
function addMarkers(path) {
let svg = path.closest('svg');
let markerDef = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
let marker =
`<marker id="circle" viewBox="0 0 10 10" refX="5" refY="5" markerWidth="10%" markerHeight="10%"
orient="auto-start-reverse">
<circle cx="5" cy="5" r="5" fill="green" />
</marker>`;
markerDef.innerHTML = marker;
svg.insertBefore(markerDef, svg.childNodes[0]);
path.setAttribute('marker-start', 'url(#circle)');
}
svg {
display: inline-block;
width: 30%;
overflow: visible;
border: 1px solid #ccc;
margin-right: 5%;
}
.row {
margin-top: 4em;
}
path {
opacity: 0.5;
transition: 0.5s;
}
<script src="https://cdn.jsdelivr.net/npm/path-data-polyfill@1.0.3/path-data-polyfill.min.js"></script>
<div>
<p>Green points illustrate starting points</p>
<svg viewBox="0 0 1920 540">
<path class="orig1" d="
m 864.216 135.95
l 36.39 41.917
S 780.519 307.11 1078.914 373.479
s 221.979 -87.327 221.979 -87.327
l 32.75 -34.931
s 25.473 101.3 207.422 34.931
s 440.314 150.2 411.2 380.744
S 34.528 576.079 34.528 576.079
s -3.64 -429.647 342.063-509.987
s 272.923 174.653 487.623 69.861
z" />
</svg>
<svg viewBox="0 0 1920 540">
<path class="orig2" d="
M 2.49 576.483
S 20.535 398.736 122.472 239.61
s 236.674 -199.127 302-217.883
c 176.407 -41.244 334 45.685 334 45.685
l 340 233.7
s 172 105.427 280 119.484
s 322 12.3 322 12.3
s 118 5.271 160 61.5
s 56 89.613 62 117.727
S 2.49 576.483 2.49 576.483
Z" />
</svg>
</div>
<div class="row">
<svg class="svgNorm" viewBox="0 0 1920 540">
<path class="morph1"
d="
m 864.216 135.95
l 36.39 41.917
S 780.519 307.11 1078.914 373.479
s 221.979 -87.327 221.979 -87.327
l 32.75 -34.931
s 25.473 101.3 207.422 34.931
s 440.314 150.2 411.2 380.744
S 34.528 576.079 34.528 576.079
s -3.64 -429.647 342.063-509.987
s 272.923 174.653 487.623 69.861
z" />
</svg>
<svg class="svgNorm" viewBox="0 0 1920 540">
<path class="morph2"
d="
M 2.49 576.483
S 20.535 398.736 122.472 239.61
s 236.674 -199.127 302-217.883
c 176.407 -41.244 334 45.685 334 45.685
l 340 233.7
s 172 105.427 280 119.484
s 322 12.3 322 12.3
s 118 5.271 160 61.5
s 56 89.613 62 117.727
S 2.49 576.483 2.49 576.483
Z" />
</svg>
<p>Click on the left path to see morphing animation. <br />Inspect this path in DevTools to get new compatible path data.</p>
</div>
示例 2:使用 anime.js
变形优化路径var morphing = anime({
targets: ".morph",
d: [
{
value:
"M 2 576 C 2 576 21 399 122 240 C 224 80 359 40 424 22 C 601 -20 758 67 758 67 C 1098 301 1098 301 1098 301 C 1098 301 1270 407 1378 421 C 1486 435 1700 433 1700 433 C 1700 433 1818 438 1860 494 C 1902 551 1916 584 1922 612 C 1928 640 2 576 2 576 Z"
}
],
duration: 1200,
loop: false,
easing: "easeInOutQuint"
});
svg{
display:inline-block;
width:20em;
overflow:visible;
}
.morph{
transition:0.5s;
}
.morphPoly{
transition:0.5s;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/animejs/3.2.1/anime.min.js"></script>
<svg id="morph" viewBox="0 0 1920 540">
<path class="morph" d="M 35 576 C 35 576 31 146 377 66 C 722 -14 650 241 864 136 C 901 178 901 178 901 178 C 901 178 781 307 1079 373 C 1377 440 1301 286 1301 286 C 1334 251 1334 251 1334 251 C 1334 251 1359 353 1541 286 C 1723 220 1981 436 1952 667 C 1923 897 35 576 35 576 Z" />
</svg>
...相当多的工作。
但是一旦你的路径超级兼容,你也可以通过普通 css 在形状之间变形。 (例如,通过 animating/transitioning d:path()
属性)。