skip to Main Content

I’m trying to animate motions of a circle moving along edges (SVG paths) in a graph.

It looks like I don’t understand the dur attribute of motions: even if I create and append a second motion after the first one ended, with its duration set to 2 seconds, then nothing happens. If I set its duration to 4 seconds instead, then the animation starts in the middle of the path, as if 2 seconds had already elapsed.

It feels like dur is a duration since the page was loaded?

<svg width="300" height="200">
    <defs>
        <marker id="arrow" markerWidth="10" markerHeight="10" refX="0" refY="3" orient="auto">
            <path d="M0,0 L0,6 L9,3 z" fill="#f00" />
        </marker>
    </defs>
    <circle cx="50" cy="50" r="20" stroke="black" stroke-width="3" fill="red" />
    <circle cx="200" cy="100" r="20" stroke="black" stroke-width="3" fill="red" />
    <circle cx="200" cy="175" r="20" stroke="black" stroke-width="3" fill="red" />
    <circle cx="50" cy="175" r="20" stroke="black" stroke-width="3" fill="red" />
    <path id="edge1" d="M70,50 Q125,0 180,90" stroke="black" stroke-width="2" fill="none" marker-end="url(#arrow)" />
    <path id="edge2" d="M70,175 Q125,125 180,100" stroke="black" stroke-width="2" fill="none" marker-end="url(#arrow)" />
    <circle id="animCircle" r="10" fill="blue">
        <animateMotion repeatCount="1">
            <mpath href="#edge1" />
        </animateMotion>
    </circle>
</svg>
<script>
  document.addEventListener("DOMContentLoaded", function() {
    const animCircle = document.getElementById('animCircle');

    // Function to create animateMotion elements dynamically
    function createAnimateMotion(pathId, dur) {
      const animElement = document.createElementNS('http://www.w3.org/2000/svg', 'animateMotion');
      animElement.setAttribute('dur', dur);
      animElement.setAttribute('repeatCount', '1');

      const mpath = document.createElementNS('http://www.w3.org/2000/svg', 'mpath');
      mpath.setAttribute('href', pathId);

      animElement.appendChild(mpath);

      return animElement;
    }

    // Initially set the animation along the first edge
    const firstAnim = createAnimateMotion('#edge1', '2s');
    animCircle.appendChild(firstAnim);

    firstAnim.addEventListener('endEvent', function() {
      // Remove the first animation
      animCircle.removeChild(firstAnim);

      // Set the animation along the second edge
      const secondAnim = createAnimateMotion('#edge2', '4s');
      animCircle.appendChild(secondAnim);
    });
  });
</script>

How do I create my animateMotion elements so that they play one after the other, without depending on the time already elapsed?

2

Answers


  1. Chosen as BEST ANSWER

    For now I'm computing a time delta to use as value for begin:

    const started = new Date().getTime();
    const animCircle = document.getElementById('animCircle');
    
    // Function to create animateMotion elements dynamically
    function createAnimateMotion(pathId, dur) {
        var animElement = document.createElementNS('http://www.w3.org/2000/svg', 'animateMotion');
        animElement.setAttribute('dur', dur);
        animElement.setAttribute('repeatCount', '1');
        const delta = (new Date().getTime() - started) / 1000;
        animElement.setAttribute('begin', delta);
    
        var mpath = document.createElementNS('http://www.w3.org/2000/svg', 'mpath');
        mpath.setAttribute('href', pathId);
    
        animElement.appendChild(mpath);
    
        return animElement;
    }
    

    This should ensure that an animation starts almost exactly when it is created with createAnimateMotion.


  2. You can queue up all animations inside the <animateMotion> element, and remove them one by one using the repeat event

    const animateMotion = animCircle.firstElementChild;
    
    animateMotion.onrepeat = () => {
      animateMotion.removeChild(animateMotion.firstElementChild);
    };
    <svg width="300" height="200">
      <defs>
        <marker id="arrow" markerWidth="9" markerHeight="6" refX="9" refY="3" orient="auto">
          <path d="M0 0 0 6 9 3" />
        </marker>
      </defs>
      <g stroke="black" stroke-width="3" fill="red">
        <circle cx="50" cy="50" r="20" />
        <circle cx="200" cy="100" r="20" />
        <circle cx="200" cy="175" r="20" />
        <circle cx="50" cy="175" r="20" />
      </g>
      <g stroke="black" stroke-width="2" fill="none" marker-end="url(#arrow)">
        <path id="edge1" d="M70 50Q125 0 180 90" />
        <path id="edge2" d="M70 175q55-50 110-75" />
      </g>
      <circle id="animCircle" r="10" fill="blue">
        <animateMotion repeatCount="2" dur="2" fill="freeze">
          <mpath href="#edge1" />
          <mpath href="#edge2" />
        </animateMotion>
      </circle>
    </svg>

    This approach requires you to set the repeatCount to the number of animations you want to play, and won’t work for indefinite (infinitely looping) animations.

    You can also inline this function if this is all you need

    <animateMotion repeatCount="2" dur="2" fill="freeze" onrepeat="this.removeChild(this.firstElementChild)">
    

    Here’s a hacky CSS solution with different trade-offs:

    #animCircle {
      animation: move 4s 1 linear forwards;
    }
    
    @keyframes move {
      0%, 50.001% { offset-distance: 0% }
      50%, 100% { offset-distance: 100% }
      0%, 50% { offset-path: url(#edge1) }
      50.001%, 100% { offset-path: url(#edge2) }
    }
    <svg width="300" height="200">
      <defs>
        <marker id="arrow" markerWidth="9" markerHeight="6" refX="9" refY="3" orient="auto">
          <path d="M0 0 0 6 9 3" />
        </marker>
      </defs>
      <g stroke="black" stroke-width="3" fill="red">
        <circle cx="50" cy="50" r="20" />
        <circle cx="200" cy="100" r="20" />
        <circle cx="200" cy="175" r="20" />
        <circle cx="50" cy="175" r="20" />
      </g>
      <g stroke="black" stroke-width="2" fill="none" marker-end="url(#arrow)">
        <path id="edge1" d="M70 50Q125 0 180 90" />
        <path id="edge2" d="M70 175q55-50 110-75" />
      </g>
      <circle id="animCircle" r="10" fill="blue" />
    </svg>
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search