Search code examples
svgsmilsvg-animationelements

How move/animate multiple circles along motion path by SVG Animation


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.

enter image description here

.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 class="container-fluid center" style="z-index: 99">
        <svg viewBox="-300 -150 3387 1270" align="center" class="svg-animation">
            <path id="planePath" class="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" class="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"/>
            />
            <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 class="box">
        <div class="circle">
        </div>
        <div class="circle">
        </div>
        <div class="circle">
        </div>
    </div> -->

    <script src="index.js"></script>
</body>

</html>

<!-- begin snippet: js hide: false console: true babel: false -->


Solution

  • 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.

    We can use also use negative begin values to set the distance between elements.

    The begin value is calculated like so:
    delay-increment: -0.1 * total number of circles.

    Simplified example

    <svg viewBox="-300 -150 3387 1270" align="center" class="svg-animation">
                <path id="planePath" fill="none" stroke="red" stroke-width="0.5%" stroke-dasharray="1% 2%"
                stroke-linecap="round"  class="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">
                    <circle id="plane" class="plane" fill="green" cx="0" cy="0" r="20" fill="black" />
                    <circle id="point-1" class="point-1" fill="magenta" cx="0" cy="0" r="20" fill="black" />
                    <circle id="point-2" class="point-2" fill="purple" cx="0" cy="0" r="20" fill="black" />
                    <circle id="point-3" class="point-3" fill="orange" cx="0" cy="0" r="20" fill="black" />
                </g>
                <animateMotion xlink:href="#plane" dur="10s" begin="-0.3s"repeatCount="indefinite" rotate="auto">
                    <mpath xlink:href="#planePath" />
                </animateMotion>
                <animateMotion xlink:href="#point-1" dur="10s" begin="-0.2s" repeatCount="indefinite" rotate="auto">
                    <mpath xlink:href="#planePath" />
                </animateMotion>
                <animateMotion xlink:href="#point-2" dur="10s" begin="-0.1s" repeatCount="indefinite" rotate="auto">
                    <mpath xlink:href="#planePath" />
                </animateMotion>
                <animateMotion xlink:href="#point-3" dur="10s" begin="0s" repeatCount="indefinite" rotate="auto">
                    <mpath xlink:href="#planePath" />
                </animateMotion>
    
            </svg>

    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}" class="point 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" class="css-animate">
            <path id="planePath" stroke="#ccc" stroke-width="1%" stroke-dasharray="1% 2%"
     stroke-linecap="round" fill="none" class="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" />
        </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 class="container-fluid center" style="z-index: 99">
            <svg viewBox="-300 -150 3387 1270" align="center" class="svg-animation">
                <path id="planePath" pathLength="100" class="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" />
                />
            </svg>
        </div>
    
    
        <div class="container-fluid center" style="z-index: 99">
            <svg viewBox="-300 -150 3387 1270" align="center" class="svg-animation">
                <path id="planePath2" pathLength="100" class="planePath planePath2"
                    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>