How can move multiple circles along a <mpath>
(motion path) using SVG SMIL Animation <<animateMotion>
.
Problem: About the first 3 4 circles everything was fine.
Some circles went "out of orbit" – so they are not correctly aligned with the motion path.
.planePath {
stroke: red;
stroke-width: .1%;
stroke-width: .5%;
stroke-dasharray: 1% 2%;
stroke-linecap: round;
fill: none;
z-index: 99;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="index.css">
<!-- CSS only -->
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-gH2yIJqKdNHPEq0n4Mqa/HGKIhSkIHeL5AyhkYV8i59U5AR6csBvApHHNl/vI1Bx" crossorigin="anonymous">
<title>Document</title>
</head>
<body>
<div style="z-index: 99">
<svg viewBox="-300 -150 3387 1270" align="center" >
<path id="planePath"
d="M1.50024 430C58.2002 -111.6 853.699 -156.741 889.5 430C925.5 1020 1754 1007.5 1785 430C1816 -147.5 2665.5 -132 2665.5 430C2665.5 1010.27 1847 948 1785 453C1841.5 -83.5 930.282 -187.244 889.5 389C851 933 35 1017.5 1.50024 430Z"/>
/>
<path style="position:absolute" id="circle2"
d="M1.50024 430C58.2002 -111.6 853.699 -156.741 889.5 430C925.5 1020 1754 1007.5 1785 430C1816 -147.5 2665.5 -132 2665.5 430C2665.5 1010.27 1847 948 1785 453C1841.5 -83.5 930.282 -187.244 889.5 389C851 933 35 1017.5 1.50024 430Z"/>
/>
<defs>
<filter id="filter0_d_0_1" x="0" y="17" width="897" height="847" filterUnits="userSpaceOnUse"
color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha" />
<feOffset dy="4" />
<feGaussianBlur stdDeviation="2" />
<feComposite in2="hardAlpha" operator="out" />
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0" />
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_0_1" />
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_0_1" result="shape" />
</filter>
<filter id="filter1_d_0_1" x="926" y="33" width="897" height="847" filterUnits="userSpaceOnUse"
color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha" />
<feOffset dy="4" />
<feGaussianBlur stdDeviation="2" />
<feComposite in2="hardAlpha" operator="out" />
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0" />
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_0_1" />
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_0_1" result="shape" />
</filter>
<filter id="filter2_d_0_1" x="1884" y="33" width="897" height="847" filterUnits="userSpaceOnUse"
color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha" />
<feOffset dy="4" />
<feGaussianBlur stdDeviation="2" />
<feComposite in2="hardAlpha" operator="out" />
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0" />
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_0_1" />
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_0_1" result="shape" />
</filter>
</defs>
<g id="plane">
<circle cx="80" cy="0" r="20" fill="black" />
</g>
<g id="point">
<circle cx="-50" cy="0" r="20" fill="black"/>
</g>
<g id="point-2">
<circle cx="20" cy="0" r="20" fill="black" />
</g>
<g id="point-3">
<circle cx="-120" cy="0" r="20" fill="black" />
</g>
<g id="point-4">
<circle cx="140" cy="0" r="20" fill="black" />
</g>
<g id="point-5">
<circle cx="180" cy="20" r="20" fill="orange" />
</g>
<g id="point-6">
<circle cx="-180" cy="0" r="20" fill="black" />
</g>
<g id="point-7">
<circle cx="-200" cy="0" r="20" fill="black" />
</g>
<g id="point-8">
<circle cx="-220" cy="0" r="20" fill="black" />
</g>
<g id="point-9">
<circle cx="-240" cy="0" r="20" fill="black" />
</g>
<g id="point-10">
<circle cx="-260" cy="0" r="20" fill="black" />
</g>
<g id="point-11">
<circle cx="-280" cy="0" r="20" fill="black" />
</g>
<g id="point-12">
<circle cx="-300" cy="0" r="20" fill="black"/>
</g>
<g id="point-13">
<circle cx="320" cy="0" r="20" fill="black" />
</g>
<g id="point-14">
<circle cx="-340" cy="0" r="20" fill="black" />
</g>
<g id="point-15">
<circle cx="-360" cy="0" r="20" fill="black" />
</g>
<g id="point-16">
<circle cx="-380" cy="0" r="20" fill="black" />
</g>
<g id="point-17">
<circle cx="-400" cy="0" r="20" fill="black" />
</g>
<g id="point-18">
<circle cx="-420" cy="0" r="20" fill="black" />
</g>
<g id="point-19">
<circle cx="-430" cy="0" r="20" fill="black" />
</g>
<g id="point-20">
<circle cx="-440" cy="0" r="20" fill="black" />
</g>
<g id="point-21">
<circle cx="-460" cy="0" r="20" fill="black" />
</g>
<g id="point-22">
<circle cx="-480" cy="0" r="20" fill="black" />
</g>
<!-- Define the motion path animation -->
<animateMotion xlink:href="#plane" dur="20s" repeatCount="indefinite" rotate="auto">
<mpath xlink:href="#planePath" />
</animateMotion>
<animateMotion xlink:href="#point" dur="20s" repeatCount="indefinite" rotate="auto">
<mpath xlink:href="#planePath" />
</animateMotion>
<animateMotion xlink:href="#point-2" dur="20s" repeatCount="indefinite" rotate="auto">
<mpath xlink:href="#planePath" />
</animateMotion>
<animateMotion xlink:href="#point-3" dur="20s" repeatCount="indefinite" rotate="auto">
<mpath xlink:href="#planePath" />
</animateMotion>
<animateMotion xlink:href="#point-4" dur="20s" repeatCount="indefinite" rotate="auto">
<mpath xlink:href="#planePath" />
</animateMotion>
<animateMotion xlink:href="#point-5" dur="20s" repeatCount="indefinite" rotate="auto">
<mpath xlink:href="#planePath" />
</animateMotion>
<!-- <animateMotion xlink:href="#point-6" dur="20s" repeatCount="indefinite" rotate="auto">
<mpath xlink:href="#planePath" />
</animateMotion>
<animateMotion xlink:href="#point-7" dur="20s" repeatCount="indefinite" rotate="auto">
<mpath xlink:href="#planePath" />
</animateMotion>
<animateMotion xlink:href="#point-8" dur="20s" repeatCount="indefinite" rotate="auto">
<mpath xlink:href="#planePath" />
</animateMotion>
<animateMotion xlink:href="#point-9" dur="20s" repeatCount="indefinite" rotate="auto">
<mpath xlink:href="#planePath" />
</animateMotion> -->
</svg>
</div>
<!-- <div >
<div >
</div>
<div >
</div>
<div >
</div>
</div> -->
<script src="index.js"></script>
</body>
</html>
<!-- begin snippet: js hide: false console: true babel: false -->
CodePudding user response:
Your animated circles (moving along the motion path)
should be placed at cx/cy =0.
Explained here by @Paul LeBeau: Offset when following svg motion path
Otherwise their initial position will be added to the current motion path position.
That's why your circle are moving as in straight line around the path.
Path offset via animation delay
Actually all circles have the same center position of cx="0"
cy="0"
- so they would be overlapping without animation.
By adding an incremental begin
value we mimic a path offset like so:
<animateMotion xlink:href="#plane" dur="10s" begin="0s"repeatCount="indefinite" rotate="auto">
<mpath xlink:href="#planePath" />
</animateMotion>
<animateMotion xlink:href="#point-1" dur="10s" begin="0.1s" repeatCount="indefinite" rotate="auto">
<mpath xlink:href="#planePath" />
</animateMotion>
The higher the begin="0.1s"
value, the larger the distance between the circles.
Simplified example
<svg viewBox="-300 -150 3387 1270" align="center" >
<path id="planePath" fill="none" stroke="red" stroke-width="0.5%" stroke-dasharray="1% 2%"
stroke-linecap="round"
d="M1.50024 430C58.2002 -111.6 853.699 -156.741 889.5 430C925.5 1020 1754 1007.5 1785 430C1816 -147.5 2665.5 -132 2665.5 430C2665.5 1010.27 1847 948 1785 453C1841.5 -83.5 930.282 -187.244 889.5 389C851 933 35 1017.5 1.50024 430Z" />
/>
<g id="circles">
<circle id="plane" fill="green" cx="0" cy="0" r="20" fill="black" />
<circle id="point-1" fill="magenta" cx="0" cy="0" r="20" fill="black" />
<circle id="point-2" fill="purple" cx="0" cy="0" r="20" fill="black" />
<circle id="point-3" fill="orange" cx="0" cy="0" r="20" fill="black" />
</g>
<animateMotion xlink:href="#plane" dur="10s" begin="0s"repeatCount="indefinite" rotate="auto">
<mpath xlink:href="#planePath" />
</animateMotion>
<animateMotion xlink:href="#point-1" dur="10s" begin="0.1s" repeatCount="indefinite" rotate="auto">
<mpath xlink:href="#planePath" />
</animateMotion>
<animateMotion xlink:href="#point-2" dur="10s" begin="0.2s" repeatCount="indefinite" rotate="auto">
<mpath xlink:href="#planePath" />
</animateMotion>
<animateMotion xlink:href="#point-3" dur="10s" begin="0.3s" repeatCount="indefinite" rotate="auto">
<mpath xlink:href="#planePath" />
</animateMotion>
</svg>
Drawback: at the beginning you see circles at the position x/y=0 since they are not yet aligned to the motion path, due to the delay.
As a workaround we can hide all circles until all of them are animated/aligned:
.planePath {
stroke: red;
stroke-width: .1%;
stroke-width: .5%;
stroke-dasharray: 1% 2%;
stroke-linecap: round;
fill: none;
}
<svg viewBox="-300 -150 3387 1270" align="center" >
<path id="planePath"
d="M1.50024 430C58.2002 -111.6 853.699 -156.741 889.5 430C925.5 1020 1754 1007.5 1785 430C1816 -147.5 2665.5 -132 2665.5 430C2665.5 1010.27 1847 948 1785 453C1841.5 -83.5 930.282 -187.244 889.5 389C851 933 35 1017.5 1.50024 430Z" />
/>
<g id="circles" opacity="0">
<circle id="plane" fill="green" cx="0" cy="0" r="20" fill="black" />
<circle id="point-1" fill="magenta" cx="0" cy="0" r="20" fill="black" />
<circle id="point-2" fill="purple" cx="0" cy="0" r="20" fill="black" />
<circle id="point-3" fill="orange" cx="0" cy="0" r="20" fill="black" />
</g>
<animateMotion xlink:href="#plane" dur="10s" begin="0s"repeatCount="indefinite" rotate="auto">
<mpath xlink:href="#planePath" />
</animateMotion>
<animateMotion xlink:href="#point-1" dur="10s" begin="0.1s" repeatCount="indefinite" rotate="auto">
<mpath xlink:href="#planePath" />
</animateMotion>
<animateMotion xlink:href="#point-2" dur="10s" begin="0.2s" repeatCount="indefinite" rotate="auto">
<mpath xlink:href="#planePath" />
</animateMotion>
<animateMotion xlink:href="#point-3" dur="10s" begin="0.3s" repeatCount="indefinite" rotate="auto">
<mpath xlink:href="#planePath" />
</animateMotion>
<!-- hide circles on start: otherwise circles show up at x/y=0 due to animation delay-->
<animate attributeName="opacity"
xlink:href="#circles"
values="0; 1;" dur="0.1s" begin="0.4"
data-id="circles"
repeatCount="1"
fill="freeze"
/>
</svg>
Fade in animation
<animate attributeName="opacity"
xlink:href="#circles"
values="0; 1;" dur="0.1s" begin="0.4"
data-id="circles"
repeatCount="1"
fill="freeze"
/>
The begin
value is calculated like so:
delay-increment: 0.1 * total number of circles
Initial motion path offset – css offset-path
to the rescue
Disclaimer: browser support might still be spotty.
See also MDN Docs.
The main benefit of the offset-path
property is its ability to actually define a start offset – pretty neat for static element renderings as well.
(Quite similar to svg's textPath
related startOffset
property)
const svg = document.querySelector('svg');
let dotsCount = 15;
let steps = 100 / dotsCount;
let duration = 10;
let circleRadius = 20;
let startOffset = 50;
//create css rules for animations
let circleMarkup = '';
let css =
`.css-animate circle {
offset-path: path('M1.50024 430C58.2002 -111.6 853.699 -156.741 889.5 430C925.5 1020 1754 1007.5 1785 430C1816 -147.5 2665.5 -132 2665.5 430C2665.5 1010.27 1847 948 1785 453C1841.5 -83.5 930.282 -187.244 889.5 389C851 933 35 1017.5 1.50024 430Z');
offset-rotate: auto;
}`;
for (let i = 0; i < dotsCount; i ) {
circleMarkup =
`<circle id="point${i}" fill="green" cx="0" cy="0" r="${circleRadius}" />`;
css =
`.point${i} {
offset-distance: ${steps*i startOffset}%;
animation: followpath${i} ${duration}s linear infinite;
}
@keyframes followpath${i} {
to {
offset-distance: ${100 steps*i startOffset}%;
}
}`;
}
svg.insertAdjacentHTML('afterbegin', '<style>' css '</style>');
svg.insertAdjacentHTML('beforeend', circleMarkup);
<svg viewBox="-300 -150 3387 1270" >
<path id="planePath" stroke="#ccc" stroke-width="1%" stroke-dasharray="1% 2%"
stroke-linecap="round" fill="none"
d="M1.50024 430C58.2002 -111.6 853.699 -156.741 889.5 430C925.5 1020 1754 1007.5 1785 430C1816 -147.5 2665.5 -132 2665.5 430C2665.5 1010.27 1847 948 1785 453C1841.5 -83.5 930.282 -187.244 889.5 389C851 933 35 1017.5 1.50024 430Z" />
</svg>
In the above example the initial motion path offset is set by:
.point-1 {
offset-distance: 25%;
animation: followpath1 10s linear infinite;
}
Also referring to a @keyframe
animation rule:
@keyframes followpath1 {
to {
offset-distance: 125%;
}
}
Of course, you will need to adjust the values according to the desired offsets/timings.
Alternative: animated stroke-dashoffset
Probably the easiest approach – slightly jittery.
I use pathLength
to change the computed length for the dash-array to 100.
stroke-dasharray: 0 6.666;
stroke-linecap: round;
Setting the first dash-array value to 0 will result in a dotted line (so every dot is perfectly circular) when combined with stroke-linecap: round
.
The second value defines the the gap or the total number of dots:
100 (pathlength) / 15 (3 circles à 5 dots) = 0.666.
.planePath {
stroke: red;
stroke-width: 5%;
stroke-dasharray: 0 6.666;
stroke-linecap: round;
fill: none;
animation: animStroke 10s linear infinite;
stroke-dashoffset: 0;
}
.planePath2 {
stroke: red;
stroke-width: 5%;
stroke-dasharray: 0 3.333;
stroke-linecap: round;
fill: none;
animation: animStroke 10s linear infinite;
}
@keyframes animStroke {
to {
stroke-dashoffset: -100;
}
}
<div style="z-index: 99">
<svg viewBox="-300 -150 3387 1270" align="center" >
<path id="planePath" pathLength="100"
d="M1.50024 430C58.2002 -111.6 853.699 -156.741 889.5 430C925.5 1020 1754 1007.5 1785 430C1816 -147.5 2665.5 -132 2665.5 430C2665.5 1010.27 1847 948 1785 453C1841.5 -83.5 930.282 -187.244 889.5 389C851 933 35 1017.5 1.50024 430Z" />
/>
</svg>
</div>
<div style="z-index: 99">
<svg viewBox="-300 -150 3387 1270" align="center" >
<path id="planePath2" pathLength="100"
d="M1.50024 430C58.2002 -111.6 853.699 -156.741 889.5 430C925.5 1020 1754 1007.5 1785 430C1816 -147.5 2665.5 -132 2665.5 430C2665.5 1010.27 1847 948 1785 453C1841.5 -83.5 930.282 -187.244 889.5 389C851 933 35 1017.5 1.50024 430Z" />
/>
</svg>
</div>