Get/set current @keyframes percentage/change keyframes

Is it possible to get/set the current animation percentage of a CSS3 @keyframes animation using javascript, jQuery, or some other means?

Say for example there is a div with class called .spin that simply spins around in a circle using a keyframe also called spin.

  1. I have tried to get the current percentage value of the animation using $('.spin').css('animation'), $('.spin').css('animation: spin'), and a couple other ways, but they all alert empty

  2. I'm also interested in changing the original animation at each predefined keyframe % and I have tried things like $('.spin').css('animation', '..new definition here...') and $('.spin').css('spin', '50%', '...new definition here...) to no avail.

Any ideas?


Solution 1:

I achieved roughly what I wanted using pure javascript with my CSS3.

For my experiment to come up with a way to do these objectives, I created a basic CSS3 animation rotating a circle around a small circular path. My goal was to, when a button was clicked, change the origin of the animation to the new location

To achieve the first goal of getting the percentage value of the animation I simply approximated the current percentage using the following setInterval, which displays the approximated current percent. This runs every 40 milliseconds to correspond with the duration of the animation (milliseconds / 100)

var result = document.getElementById('result'), currentPercent = 0;
var showPercent = window.setInterval(function() {
  if(currentPercent < 100)
  {
    currentPercent += 1;
  }
  else {
    currentPercent = 0;
  }
  result.innerHTML = currentPercent;
}, 40);

Note on this solution:

  • It is not perfect due because the counter keeps running when another tab is clicked but the animation stops, so they become un-synchronized
  • It is also faulty when the button is clicked long after the last click. Evidently the setInterval runs a little bit longer than the animation, so they become less and less synced each iteration
  • I looked everywhere for a more perfect solution but have been unable to come up with one as of yet

To achieve the second goal of setting a new definition for an animation's % value it took a bit of a more complex solution. After pouring through dozens of articles and web pages (the important ones listed below), I managed to come up with the following solution:

var circle = document.getElementById('circle'), 
    button = document.getElementById('button');
var result = document.getElementById('result'), 
    totalCurrentPercent = 0,
    currentPercent = 0;
var showPercent = window.setInterval(function() {
  if(currentPercent < 100)
  {
    currentPercent += 1;
  }
  else {
    currentPercent = 0;
  }
  result.innerHTML = currentPercent;
}, 40);
function findKeyframesRule(rule) {
    var ss = document.styleSheets;
    for (var i = 0; i < ss.length; ++i) {
        for (var j = 0; j < ss[i].cssRules.length; ++j) {
            if (ss[i].cssRules[j].type == window.CSSRule.WEBKIT_KEYFRAMES_RULE && ss[i].cssRules[j].name == rule) { return ss[i].cssRules[j]; }
        }
    }
    return null;
}
function change(anim) {
  var keyframes = findKeyframesRule(anim),
      length = keyframes.cssRules.length;
  var keyframeString = [];  
  for(var i = 0; i < length; i ++)
  {
    keyframeString.push(keyframes[i].keyText);
  }
  var keys = keyframeString.map(function(str) {
    return str.replace('%', '');
  });
  totalCurrentPercent += currentPercent;
  if(totalCurrentPercent > 100)
  {
    totalCurrentPercent -= 100;
  }
  var closest = getClosest(keys);  
  var position = keys.indexOf(closest), 
      firstPercent = keys[position];
  for(var i = 0, j = keyframeString.length; i < j; i ++)
  {
    keyframes.deleteRule(keyframeString[i]);
  }
  var multiplier = firstPercent * 3.6;
  keyframes.insertRule("0% { -webkit-transform: translate(100px,100px) rotate(" + (multiplier + 0) + "deg) translate(-100px,-100px) rotate(" + (multiplier + 0) + "deg); background-color:red; }");
  keyframes.insertRule("13% { -webkit-transform: translate(100px,100px) rotate(" + (multiplier + 45) + "deg) translate(-100px,-100px) rotate(" + (multiplier + 45) + "deg); }");
  keyframes.insertRule("25% { -webkit-transform: translate(100px,100px) rotate(" + (multiplier + 90) + "deg) translate(-100px,-100px) rotate(" + (multiplier + 90) + "deg); }");
  keyframes.insertRule("38% { -webkit-transform: translate(100px,100px) rotate(" + (multiplier + 135) + "deg) translate(-100px,-100px) rotate(" + (multiplier + 135) + "deg); }");
  keyframes.insertRule("50% { -webkit-transform: translate(100px,100px) rotate(" + (multiplier + 180) + "deg) translate(-100px,-100px) rotate(" + (multiplier + 180) + "deg); }");
  keyframes.insertRule("63% { -webkit-transform: translate(100px,100px) rotate(" + (multiplier + 225) + "deg) translate(-100px,-100px) rotate(" + (multiplier + 225) + "deg); }");
  keyframes.insertRule("75% { -webkit-transform: translate(100px,100px) rotate(" + (multiplier + 270) + "deg) translate(-100px,-100px) rotate(" + (multiplier + 270) + "deg); }");
  keyframes.insertRule("88% { -webkit-transform: translate(100px,100px) rotate(" + (multiplier + 315) + "deg) translate(-100px,-100px) rotate(" + (multiplier + 315) + "deg); }");
  keyframes.insertRule("100% { -webkit-transform: translate(100px,100px) rotate(" + (multiplier + 360) + "deg) translate(-100px,-100px) rotate(" + (multiplier + 360) + "deg); }");
  circle.style.display = "inherit";
  circle.style.webkitAnimationName = anim; 
  window.clearInterval(showPercent);
  currentPercent = 0;
  showPercent = self.setInterval(function() {
    if(currentPercent < 100)
    {
      currentPercent += 1;
    }
    else {
      currentPercent = 0;
    }
    result.innerHTML = currentPercent;
  }, 40); 
}
button.onclick = function() {
    circle.style.webkitAnimationName = "none";
    circle.style.display = "none";
    setTimeout(function () { 
        change("rotate"); 
    }, 0);
}
function getClosest(keyframe) {
  var curr = keyframe[0];
  var diff = Math.abs (totalCurrentPercent - curr);
  for (var val = 0; val < keyframe.length; val++) {
    var newdiff = Math.abs(totalCurrentPercent - keyframe[val]);
    if (newdiff < diff) {
      diff = newdiff;
      curr = keyframe[val];
     }
  }
  return curr;
}

Check out the experiment itself here including comments describing what each part of the javascript is doing

Notes on this solution:

  • I tried to make the change function as non-hard-coded as possible
  • It works alright for approximating the current @keyvalue percentage
  • The transition from one animation to the other is only as smooth as however close the nearest % value of the animation is to the current % of the animation, so adding more % definitions to the animation would make it even more smooth

In the process of trying to come up with a solution for the problem, I found these useful resources:

  • RussellUresti's answer in this SO post and corresponding example was quite influential and greatly aided my final solution
  • To get the closest value based on an input and array values, I used paxdiablo's method in this SO post (thank you)
  • This plugin, while I didn't use it myself, seemed to achieve a very similar (though seemingly not quite as customizable) effect in jQuery

---EDIT---

If you are just using CSS transitions or you can change your animation to just use transitions instead, you can use this simpler approach. You can pause the transition by copying the attributes changed by the transition, setting the attributes to those changed attributes, and then removing the class that animates it. Of course if you are using the same object to animate/pause you will need to animate the first click then pause it the next click. You can easily do the pure javascript equivalent as well

Note: the !important in the CSS changed attribute is necessary unless you have a more leveled selector for the animation class than the jQuery selector, aka something like div #defaultID.animationClass { as opposed to just #defaultID.animationClass {. Since #defaultID and #defaultID.animationClass are both one level, this example requires the !important

--Another Edit--

For more information on this topic, check out my post on CSS-Tricks