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!