Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to synchronize CSS animations that were started/restarted at different times

Tags:

javascript

css

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');
    }
  }

}
like image 971
1337ingDisorder Avatar asked Jan 02 '22 22:01

1337ingDisorder


People also ask

How do you add an animation delay in CSS?

CSS Animation Delay Syntax The CSS animation-delay property has the following syntax: animation-delay: [time] | initial | inherit; As you can see, there are three possible values: time, initial, and inherit. The first option is [time], which is the number of seconds or milliseconds before the animation starts.

Can you have 2 animations in CSS?

While it is true that you cannot play two transform animations at the same time (because one transform would overwrite the other), it is not correct to say "You cannot play two animations since the attribute can be defined only once." See the accepted answer about using comma-separated values with animations.

Which CSS property is used to play animation again and again?

The animation-play-state property specifies whether the animation is running or paused.


Video Answer


3 Answers

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.

like image 92
Leo Avatar answered Oct 13 '22 15:10

Leo


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>
like image 2
Amirreza Noori Avatar answered Oct 13 '22 17:10

Amirreza Noori


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.

like image 1
mika.koshonson Avatar answered Oct 13 '22 17:10

mika.koshonson