Skip to content

Loop with ‘setInterval’ and ‘setTimeout’ doesn’t work

I am trying to animate some cards that should enter the screen from the right, stop in the middle for a while, and then vanish to the left, in an infinite loop. This is what I tried:

function startAnimation(elem) {
  $('#' + elem).fadeIn(150).animate({
    left: '0'
  }, 1500);
}

function endAnimation(elem) {
  $('#' + elem).animate({
    left: '-200%'
  }, 1500);

  $('#' + elem).fadeOut(100).animate({
    left: '200%'
  }, 300);
}

function scrollCards(elem, n) {
  startAnimation(elem);

  setTimeout(function() {
    endAnimation(elem);
  }, 700);

  elem += 1;
  elem = elem == n ? 0 : elem;
  return elem;
}

n = 3;
var card = 0
var firstAnimationDone = false;
$('#0').fadeIn(150);

setInterval(function() {
  if (!firstAnimationDone) {
    endAnimation(card);
    card = 1;
  }
  card = scrollCards(card, n);
  firstAnimationDone = true;
}, 4500);
/* (boxArticle is here just to keep static the part of the page where the animation takes place) */

.boxArticle {
  overflow: hidden;
  height: 100px;
}

.boxAchievements {
  position: relative;
  height: 100px;
  width: 100%;
  left: 200%;
  top: 5px;
  display: none;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div class="boxArticle">
  <article class="boxAchievements" id="0">
    <h2>My achievements</h2>
    <p>Write 1</p>
  </article>
  <article class="boxAchievements" id="1">
    <h2>My achievements</h2>
    <p>Write 2</p>
  </article>
  <article class="boxAchievements" id="2">
    <h2>My achievements</h2>
    <p>Write 3</p>
  </article>
</div>

When I add setTimeout to the scrollCards function it stops in the middle for a very long time, no matter how long is the interval I put in the method, and it desync the loop, so I have 2 cards moving simultaneously.

Answer

You could use the CSS animation rule instead to achieve what you want with much less code. The solution below uses a trick that enables infinite animation to run with a delay between iterations (see, this Q&A, for example).

In short, animation duration is set with delay in mind, and @keyframes controls the delay by keeping the same animated property value from some point to 100% (i.e. if it takes 2s, and the delay is 8s, then set the duration to 8+2=10s and finish the property change by 100*2/10=20%).

Then you add the class with animation whenever you want. To align animations, add classes in sequence with a step equal to: duration + delay / number of elements.

Note that your CSS is changed to properly align <article> elements because of the removal of fadeIn / fadeOut method calls and display: none; rule.

(() => {

  $('#0').addClass("middle");
  
  setTimeout(() => $("#1").addClass("middle"), 5e3);
  
  setTimeout(() => $("#2").addClass("middle"), 9e3);
  
})();
body {
  margin: 0;
}

:root {
  --middle : calc(50% - 25vw / 2);
  --left   : calc(0% - 25vw);
  
  --duration : 12s;
}

.boxArticle {
  position: relative;
  overflow: hidden;
  height: 100vh;
  width: 100vw;
}

.boxAchievements {
  position: absolute;
  height: 100px;
  width: 25vw;
  left: 200%;
  top: 5px;
}

.middle {
  animation: 
    middle var(--duration) linear 0s normal infinite forwards running,
    left   var(--duration) linear 0s normal infinite forwards running;
}

@keyframes middle {
  8.3%, 100% { left: var(--middle); }
}

@keyframes left {
  8.3%, 24.9% { left: var(--middle); }
  33.2%, 100%  { left: var(--left); }
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div class="boxArticle">
  <article class="boxAchievements" id="0">
    <h2>My achievements</h2>
    <p>Write 1</p>
  </article>
  <article class="boxAchievements" id="1">
    <h2>My achievements</h2>
    <p>Write 2</p>
  </article>
  <article class="boxAchievements" id="2">
    <h2>My achievements</h2>
    <p>Write 3</p>
  </article>
</div>

There are also a couple of notes about the code in your snippet:

  1. Do not mix types of variables. Although JavaScript allows that, this is a source of nightmares for anyone who will read your code (including you a year from now). In particular, scrollCards has a parameter elem which is supposed to be an Element, not a number (or vice versa).

  2. Use a recursive setTimeout rather than setInterval – the latter queues up a function call regardless of whether the previous animation is finished or not (there are other reasons to use recursive setTimeout that are outside of the question scope).

  3. Declare n with var (better still – do not declare any global variables, but at least avoid creating implied globals by omitting a declaration keyword).

  4. setTimeout calls are not guaranteed to run after a specified amount of time as they are asynchronous – depending on a page load, the risk of completely desynchronized animations increases with time.

    One way to mitigate that is to use promises to wait until the timeout fires, but aligning item animations with that will likely be a difficult task. As an illustration, here is how you make scrollCards wait for endAnimation to happen:

(() => {
  const now = () => new Date().toISOString();
  const startAnimation = (elem) => console.log(`started animation at ${now()}`);
  const endAnimation = (elem) => console.log(`ended animation at ${now()}`);
  
  async function scrollCards(elem, n) {
    startAnimation(elem);

    //assuming endAnimation is synchronous
    await new Promise((resolve) => setTimeout((elem) => resolve(endAnimation(elem)), 700, elem));

    elem += 1; //see #1 - this is error-prone
    elem = elem == n ? 0 : elem;
    return elem;
  };
    
  scrollCards(0,1);
})();