javascriptweb-animations

JS: how to sync timing of multiple animations?


When I dynamically add an item that animates with JS, how do I get them to be in sync on the same timeline as shown here: http://youtube.com/watch?v=AGiTmjFHs8M&t=9m23s ? I saw a tutorial that showed a benefit of using JS animation vs. CSS is they inherit the same timeline.

<div class="body"></div>
<button onclick="addItem()">Add</button>

function addItem() {
   let body = document.querySelector('.body');
   let newEl = document.createElement('div');
   newEl.innerText = "I am a new Item";
   newEl.animate([
      { 
          transform: 'translate(0px, 0px)',
          transform: 'translate(500px, 500px)',
      }
      ], 
      {
         duration: 2000,
         iterations: Infinity,
      });

   body.appendChild(newEl);
}

Solution

  • If all your Animation objects do share the same duration and you want them to start and end at the same time, you can simply set the iterationStart EffectTiming of your new Animation object to the ComputedTiming .progress value of the already running one.
    However beware you must wait for the new Animation object is ready before getting the computed value of the previous one or you'll be one frame late. For this, you can simply await the animation.ready Promise.

    To get the previous Animation object, you can either store it when you create it through Element.animate(), or you can access the set of currently running Animations on the document through document.getAnimations(), and pick it from there.

    let i = 0;
    async function addItem() {
    
      i++;
      const body = document.querySelector(".body");
      const newEl = document.createElement("div");
      newEl.classList.add("item");
      newEl.textContent = "I am a new Item " + i;
    
      // get a previous Animation if any
      const previous_animation = document.getAnimations()[0];
    
      // create the new Animation object
      // slightly offset on the x axis only
      // to check if they are indeed in sync
      const anim = newEl.animate([{
        transform: "translate(" + (i * 10) + "px, 0px)",
        transform: "translate(" + (i * 10 + 250) + "px, 250px)",
      }], {
        duration: 2000,
        iterations: Infinity
      });
      // when it's ready to start
      await anim.ready;
    
      // get the current progress of the first Animation object
      const computed_timing = previous_animation?.effect.getComputedTiming();
      if( computed_timing ) {
        // update our new Animation object to the same progress value
        anim.effect.updateTiming( {
          iterationStart: computed_timing.progress
        });
      }
      
      body.appendChild(newEl);
    
    }
    .item {
      position: absolute;
    }
    <div class="body"></div>
    <button onclick="addItem()">Add</button>


    Note that as pointed out by user brianskold in a comment below, the startTime property of the Animation can be set to an earlier time. Doing so will make it like the animation did start at this time.

    So for synchronizing two animations, this is probably the best way:

    let i = 0;
    function addItem() {
    
      i++;
      const body = document.querySelector(".body");
      const newEl = document.createElement("div");
      newEl.classList.add("item");
      newEl.textContent = "I am a new Item " + i;
    
      // get a previous Animation if any
      const previous_animation = document.getAnimations()[0];
    
      // create the new Animation object
      // slightly offset on the x axis only
      // to check if they are indeed in sync
      const anim = newEl.animate([{
        transform: "translate(" + (i * 10) + "px, 0px)",
        transform: "translate(" + (i * 10 + 250) + "px, 250px)",
      }], {
        duration: 2000,
        iterations: Infinity
      });
    
      if( previous_animation ) {
        // set the startTime to the same
        // as the previously running's one
        // note this also forces anim.ready to resolve directly
        anim.startTime = previous_animation.startTime;
      }
    
      body.appendChild(newEl);
    
    }
    .item {
      position: absolute;
    }
    <div class="body"></div>
    <button onclick="addItem()">Add</button>