Home > Back-end >  How to synchronize CSS animations that were started/restarted at different times
How to synchronize CSS animations that were started/restarted at different times

Time:01-03

I'm trying to synchronize CSS animations for multiple page elements receiving the same animation class.

There seems to be a lot of posts about this subject but no unified solution, and none of the solutions I've found seem to apply to this case.

I've prepared a jsfiddle here to demonstrate the issue:

https://jsfiddle.net/gdf7szo5/1/

If you click on any of the letters it will start the animation for that letter. If you click again it will switch to a different animation, and if you click a third time it will set the letter to have no animation at all.

The desired effect is for all letters blinking in one animation to be synchronized with each other, and any letters in the other animation to be synchronized with each other. To be clear, I'm not trying to synchronize the two animations — I just want all the elements with a given animation to be synchronized with each other.

But currently if one letter shows an animation and you set another letter to the same animation, unless you have absolutely perfect timing the animations for the two letters will be out of sync even though they're the same animation.

Here's the code in play:

HTML:

<div>
  <span id="spA" onclick="toggleFx('spA')">A</span>
  <span id="spB" onclick="toggleFx('spB')">B</span>
  <span id="spC" onclick="toggleFx('spC')">C</span>
  <span id="spD" onclick="toggleFx('spD')">D</span>
</div>

CSS:

div {
  display: flex;
  flex-flow: column;
}

span {
  animation-name: pulse_active;
}

span.pulse {
    color: #f00;
    animation: pulse_active 1.5s ease-in infinite;
}

span.pulse_invert {
  color: #00f;
  animation: pulse_inverted 3s ease-in infinite;
}

@keyframes pulse_active {
    0% {
        opacity: 0;
    }
    50% {
        opacity: 0.66;
    }
    100% {
        opacity: 0;
    }
}

@keyframes pulse_inverted {
    0% {
        opacity: 1;
    }
    50% {
        opacity: 0.33;
    }
    100% {
        opacity: 1;
    }
}

JS:

let pulseNormal = {};
let pulseInvert = {};

function toggleFx(spanID) {

    let spanTarget = document.getElementById(spanID);
  
  // shift setting
  if (spanID in pulseInvert) {
  console.log('y');
  console.log(Object.keys(pulseInvert));
    delete pulseInvert[spanID]
  console.log(Object.keys(pulseInvert));
  } else if (spanID in pulseNormal) {
    pulseInvert[spanID] = true;
    delete pulseNormal[spanID];
  } else {
    pulseNormal[spanID] = true
  }
  
  // update display
  for (let sp of 'ABCD') {
    let span = document.getElementById(`sp${sp}`);
    
    // clear any prev animation
        span.classList.remove('pulse');     
    span.classList.remove('pulse_invert');
    
    // add/re-add animation as needed
    if (`sp${sp}` in pulseNormal) {             
        span.classList.add('pulse');
    } else if (`sp${sp}` in pulseInvert) {
        span.classList.add('pulse_invert');
    }
  }

}

CodePudding user response:

In order to synchronize them, the animation should be initialized at the same time.

Here I specify the animation-name for span, the animation won't run until the other props are defined (actually I'm curious to see if different rendering engines share the same behavior).

let pulseList = {};

function toggleFx(spanID) {
  let spanTarget = document.getElementById(spanID);

  // toggle setting
  if (spanID in pulseList) {
    delete pulseList(spanID)
  } else {
    pulseList[spanID] = true;
  }

  // update display
  for (let sp of 'ABCD') {
    let span = document.getElementById(`sp${sp}`);
    span.classList.remove('pulse'); // clear any prev animation
    if (`sp${sp}` in pulseList) { // add/re-add animation as needed
      span.classList.add('pulse');
    }
  }
}
div {
  display: flex;
  flex-flow: column;
}

span {
  animation-name: pulse_active;
}

span.pulse {
  color: #f00;
  animation: pulse_active 1.5s ease-in infinite;
}

@keyframes pulse_active {
  0% {
    opacity: 0
  }
  50% {
    opacity: 0.66
  }
  100% {
    opacity: 0
  }
}
<div>
  <span id="spA" onclick="toggleFx('spA')">A</span>
  <span id="spB" onclick="toggleFx('spB')">B</span>
  <span id="spC" onclick="toggleFx('spC')">C</span>
  <span id="spD" onclick="toggleFx('spD')">D</span>
</div>

Edit

At least Blink and Gecko do share the same behavior, I didn't test on WebKit Safari.

CodePudding user response:

For this purpose document.getAnimations() is a useful method. It returns an array of all animation objects currently in effect whose target elements are descendants of the document. More information: https://developer.mozilla.org/en-US/docs/Web/API/Document/getAnimations

Items of document.getAnimations() include currentTime property, that can use for syncing animations. More information: https://drafts.csswg.org/web-animations/#dom-documentorshadowroot-getanimations

toggleFx function was modified and now it has three parts.

In the first part, search document.getAnimations() and find currentTime attribute where animationName is one of pulse_active or pulse_inverted; then store them in the realted variables (i.e. pulseActiveStart and pulseInvertedStart).

In the 2nd part, className of spanTarget changed as you told.

In the 3rd part, pulseActiveStart and pulseInvertedStart applied to currentTime attribute of related animation (reverse 1st part).

function toggleFx(spanID) {

    let spanTarget = document.getElementById(spanID);
    
    let  pulseActiveStart;
    let  pulseInvertedStart;
    let anims = document.getAnimations()    
    for(let i = 0; i < anims.length; i  ) { 
        if(anims[i].animationName == "pulse_active") pulseActiveStart = anims[i].currentTime;
        else if(anims[i].animationName == "pulse_inverted") pulseInvertedStart = anims[i].currentTime;      
    }
    
    
    
    if(spanTarget.classList.contains('pulse_invert')) {
        spanTarget.classList.remove('pulse_invert');    
        spanTarget.classList.remove('pulse');   
    } else if(spanTarget.classList.contains('pulse')) {
        spanTarget.classList.add('pulse_invert');   
        spanTarget.classList.remove('pulse');   
    } else {
        spanTarget.classList.add('pulse');  
    }
    
    anims = document.getAnimations()    
    for(let i = 0; i < anims.length; i  ) { 
        if(anims[i].animationName == "pulse_active") {
            if(pulseActiveStart) anims[i].currentTime = pulseActiveStart;           
        } else if(anims[i].animationName == "pulse_inverted") {
            if(pulseInvertedStart) anims[i].currentTime = pulseInvertedStart;       
        }
    }
}
div {
  display: flex;
  flex-flow: column;
}

span.pulse {
    color: #f00;
    animation: pulse_active 1.5s ease-in infinite;
}

span.pulse_invert {
  color: #00f;
  animation: pulse_inverted 3s ease-in infinite;
}

@keyframes pulse_active {
    0% { opacity: 0; }
    50% { opacity: 0.66; }
    100% {  opacity: 0; }
}

@keyframes pulse_inverted {
    0% { opacity: 1; }
    50% { opacity: 0.33; }
    100% {  opacity: 1; }
}
<div>
  <span id="spA" onclick="toggleFx('spA')">A</span>
  <span id="spB" onclick="toggleFx('spB')">B</span>
  <span id="spC" onclick="toggleFx('spC')">C</span>
  <span id="spD" onclick="toggleFx('spD')">D</span>
</div>

CodePudding user response:

I think that synchronizing CSS animations is quite a wide topic that cannot be easily addressed with a catch-all solution.

In this specific case, the most straightfoward approach may be to track time since pulse class was added for the first time and then delay all the subsequent pulse class additions to await beginning of the next animation iteration.

See modified jsfiddle with suggested solution. It also tweaks the styles a bit to achieve a smoother transition when awaiting next iteration. Hopefully, it is close to what you want to achieve.

You can theoretically use the base time offset to 'compute' and set the first round animation before the pulse class is added with a delay more precisely, to achieve even smoother transition. But it would also require a bit more math and it would definitely clutter the code to some extent. Depends on your goals and preference.

If somebody knew a cleaner solution, perhaps with pure CSS approach (?), it would be great.

CodePudding user response:

Building on the info from @Amirreza Noori's answer I managed to get this done with a concise little one-liner:

for (let a of document.getAnimations()) { a.startTime = 0 }

No need to find the position for existing animations or distinguish specific animations — this just restarts all the animations on the page.

The other info in that answer (as well as the others) will be helpful if, in the future, I introduce animations that can't be interrupted when the rest of the page's animations get reset. But if I can avoid having to do that then this one-liner seems to cut the mustard nicely.

Thanks to everyone for the answers and insight!

  • Related