3

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);
}
Kaiido
  • 123,334
  • 13
  • 219
  • 285
RicardoAlvveroa
  • 226
  • 1
  • 8
  • I am not sure I understand this question `how do I get them to be in sync on the same timeline?`. Could you elaborate? – codemonkey Dec 30 '20 at 03:29
  • @codemonkey sure. I saw in this video http://www.youtube.com/watch?v=AGiTmjFHs8M&t=9m23s at 9:23 that shows how JS Animations , as opposed to CSS animations, can be sync'd up on the same timeline since they all inherit the same timeline – RicardoAlvveroa Dec 30 '20 at 03:32
  • `.animate()` is a wrapper that creates and starts the animation automatically. Instead create and start the animations manually. See [Web Animations API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API) and [Animation](https://developer.mozilla.org/en-US/docs/Web/API/Animation). – Ouroborus Dec 30 '20 at 03:39
  • I see so tell me if this is the desired behavior: Every time you click the "Add" button, you want a new "I am a new Item" to be synced up with all the previous ones which means you want them superimposed on each other? – codemonkey Dec 30 '20 at 03:39
  • @codemonkey yep, correct – RicardoAlvveroa Dec 30 '20 at 03:42
  • 1
    When you start the first animation, keep track of the time in a variable. Whenever somebody clicks "Add", calculate when the next "tick" boundary will be (hint: every multiple of 2000ms after the initial start time you kept track of), determine how many ms that will is in the future (it will be guaranteed to be <= 2000ms), and use `setTimeout()` to actually add that element in that many seconds from now. Also, have you seen: https://stackoverflow.com/a/57776076/378779 – kmoser Dec 30 '20 at 04:04

1 Answers1

1

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>
Kaiido
  • 123,334
  • 13
  • 219
  • 285
  • 1
    Also note that you can set the `startTime` of the new animation. This allows you to synchronize the new animation with an existing one _and_ force the `ready` promise to resolve immediately. In fact, Firefox uses this technique to synchronize the throbber animation in loading tabs: https://searchfox.org/mozilla-central/rev/bfbacfb6a4efd98247e83d3305e912ca4f7e106a/browser/base/content/tabbrowser.js#709-746 – brianskold Dec 31 '20 at 05:08
  • @brianskold Oh I didn't got correctly what setting a past startTime does! So if I understand better now, it acts as if it was running since this time? And given it's constant as a getter there is no need to worry when it actually enters the graph, right? (Now wondering how I missed that when [MDN's example](https://developer.mozilla.org/en-US/docs/Web/API/Animation/startTime) is exatly OP's case...). Thanks for the note, you could maybe write your own answer in order to gain deserved imaginary points here, or I'll edit mine to include it when I'll have time. – Kaiido Dec 31 '20 at 05:34
  • Feel free to edit your answer to add details of `startTime`. The way `animate()` or `play()` works is that they are asynchronous so that they can resolve the `startTime` only when the animation is ready to run, to ensure the animation starts smoothly. However, when you're trying to synchronize animations, you don't care about a smooth start, so setting `startTime` is fine. (The reason the Firefox code I linked to uses `requestAnimationFrame` is simply to batch style updates.) – brianskold Jan 01 '21 at 10:23