Coupling JavaScript with CSS Animations and Transitions

One of the grievances with web developers and designers has been the layers of intricacy associated with animating in CSS. Some go on to believe that the CSS animations are not as effective as they would like them to be. JavaScript meanwhile happens to be the go to platform for most folks aiming for creating powerful animations without a sweat-soaked exercise. We however, can branch out a little and use JavaScript as input to creating animations and transitions through CSS. This combination gives us the wherewithal to shape the animations that are functionally boosted by hardware and end up being more synergistic than the typical JavaScript-created animations. To begin with, it is worth knowing that animations and transitions are not the same thing. Transitions in CSS are time-centric and largely depend on some variable properties in terms of the specific time period over which they change. They are only associated with a single element. Animations on the other hand have little to do with a cyclic period of time. As soon as we apply them, they start running.

Engineering the CSS Transitions

If you have been bothered by doubts over how you can bring about the pause and movement of a specific element’s transition, don’t be anymore. JavaScript has answers seamlessly executable for you. A class name has to be toggled on an element when its transition has to be initialized. We need to make use of getComputedStyle and getPropertyValue right at the instant we want a pause. Then the CSS values of the properties will be assigned as per the values we have just fetched. Let us inject some more clarity with this example :
HTML
123456789101112131415
<!DOCTYPE HTML>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<script src="http://code.jquery.com/jquery-2.0.0.js"></script>
</head>
<body>
<h3>Pure Javascript</h3>
<div class='box'></div> 
<button class='toggleButton' value='play'>Play</button>
<h3>jQuery</h3>
<div class='box'></div> 
<button class='toggleButton' value='play'>Play</button>
</body>
</html>
CSS
1234567891011121314
.box {
  margin: 30px;
  height: 50px;
  width: 50px;
  background-color: blue;
}
.box.horizTranslate {
  -webkit-transition: 3s;
  -moz-transition: 3s;
  -ms-transition: 3s;
  -o-transition: 3s;
  transition: 3s;
  margin-left: 50% !important;
}
JS
12345678910111213141516
var boxOne = document.getElementsByClassName('box')[0],
    $boxTwo = $('.box:eq(1)');

document.getElementsByClassName('toggleButton')[0].onclick = function() {
  if(this.innerHTML === 'Play') 
  { 
    this.innerHTML = 'Pause';
    boxOne.classList.add('horizTranslate');
  } else {
    this.innerHTML = 'Play';
    var computedStyle = window.getComputedStyle(boxOne),
        marginLeft = computedStyle.getPropertyValue('margin-left');
    boxOne.style.marginLeft = marginLeft;
    boxOne.classList.remove('horizTranslate');    
  }  
}
JS
123456789101112
$('.toggleButton:eq(1)').on('click', function() { 
  if($(this).html() === 'Play') 
  {
    $(this).html('Pause');
    $boxTwo.addClass('horizTranslate');
  } else {
    $(this).html('Play');
    var computedStyle = $boxTwo.css('margin-left');
    $boxTwo.removeClass('horizTranslate');
    $boxTwo.css('margin-left', computedStyle);
  }  
});

Pure Javascript

jQuery

Now, we will reproduce the above technique even when there are more complex transitions to bring about.

Making use of the Callback Functions

The DOM events of JS can prove to be extremely handy when we are playing around with CSS transitions and animations. These may include events of the likes of animationEnd, animationIteration and so on. For transitions, we have transitionEnd, etc. the names make it clear what action they trigger. Let us create an animation and transition effect by creating a heart shape and then interrupt its animation when it is hovered over:
HTML
12345678910111213
<!DOCTYPE HTML>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<script src="http://code.jquery.com/jquery-2.0.0.js"></script>
</head>
<body>
<h3>Pure CSS</h3>
<div class='heart animated css'></div> 
<h3>With Javascript</h3>
<div class='heart animated'></div> 
</body>
</html>
CSS
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104
.heart {
  position: relative;
  width: 90px;
  height: 85px;
  margin: 27px;
  
  -webkit-transform: scale(1);
  -ms-transform: scale(1);
  -o-transform: scale(1);
  -moz-transform: scale(1);
  transform: scale(1);
  -webkit-transform-origin: center center;
  -moz-transform-origin: center center;
  -ms-transform-origin: center center;
  -o-transform-origin: center center;
  transition: all 1s; 
}
.heart.css {  
  -webkit-animation-delay:1s;
  -moz-animation-delay:1s;
  -ms-animation-delay:1s;
  -o-animation-delay:1s;
  animation-dely:1s;
}
.heart.animated {
  -webkit-animation: 1500ms pulsate infinite alternate ease-in-out;
  -moz-animation: 1500ms pulsate infinite alternate ease-in-out;
  -ms-animation: 1500ms pulsate infinite alternate ease-in-out;
  -o-animation: 1500ms pulsate infinite alternate ease-in-out;
  animation: 1500ms pulsate infinite alternate ease-in-out;
}
.heart:before,
.heart:after { 
  position: absolute; 
  content: "";
  left: 48px;
  top: 0;
  width: 48px;
  height: 77px;
  background: red;
  -moz-border-radius: 48px 48px 0 0;
  border-radius: 48px 48px 0 0;
  -webkit-transform: rotate(-43px);
     -moz-transform: rotate(-43px);
      -ms-transform: rotate(-43px);
       -o-transform: rotate(-43px);
          transform: rotate(-43px);
  -webkit-transform-origin: 0 100%;
     -moz-transform-origin: 0 100%;
      -ms-transform-origin: 0 100%;
       -o-transform-origin: 0 100%;
          transform-origin: 0 100%;
}
.heart:after {
  left: 0;
  -webkit-transform: rotate(43deg);
     -moz-transform: rotate(43deg);
      -ms-transform: rotate(43deg);
       -o-transform: rotate(43deg);
          transform: rotate(43deg);
  -webkit-transform-origin: 100% 100%;
     -moz-transform-origin: 100% 100%;
      -ms-transform-origin: 100% 100%;
       -o-transform-origin: 100% 100%;
          transform-origin :100% 100%;
}
.heart.css:hover {
  -webkit-transform: scale(2);
  -moz-transform: scale(2);
  -ms-transform: scale(2);
  -o-transform: scale(2);
  transform: scale(2);
  -webkit-animation:'';
  -moz-animation:none;
  -ms-animation:'';
  -o-animation:'';
  animation:'';
}

@keyframes pulsate {
  0% { transform: scale(1); }
  48% { transform: scale(1.3); }
  95% { transform: scale(1); }
}
@-webkit-keyframes pulsate {
  0% { -webkit-transform: scale(1); }
  48% { -webkit-transform: scale(1.3); }
  95% { -webkit-transform: scale(1); }
}
@-moz-keyframes pulsate {
  0% { -moz-transform: scale(1); }
  48% { -moz-transform: scale(1.3); }
  95% { -moz-transform: scale(1); }  
}
@-ms-keyframes pulsate {
  0% { -ms-transform: scale(1); }
  48% { -ms-transform: scale(1.3); }
  95% { -ms-transform: scale(1); }  
}
@-o-keyframes pulsate {
  0% { -o-transform: scale(1); }
  48% { -o-transform: scale(1.3); }
  95% { -o-transform: scale(1); }  
}
JS
1234567891011121314151617181920212223242526272829303132333435363738394041424344
var heart = document.getElementsByClassName('heart')[1],
    pfx = ["webkit", "moz", "MS", "o", ""],
    hovered = false;

function AnimationListener() {
    if(hovered)
    { 
      heart.classList.remove('animated'); 
      heart.style.webkitTransform = 'scale(2)';
      heart.style.MozTransform = 'scale(2)';
      heart.style.msTransform = 'scale(2)';
      heart.style.OTransform = 'scale(2)';
      heart.style.transform = 'scale(2)';
    }
}

function TransitionListener() {
  if(!hovered)
  {
    heart.classList.add('animated');
  }
}

function PrefixedEvent(element, type, callback) {
    for (var p = 0; p < pfx.length; p++) {
        if (!pfx[p]) type = type.toLowerCase();
        element.addEventListener(pfx[p]+type, callback, false);
    }
}

PrefixedEvent(heart, "AnimationIteration", AnimationListener);

heart.onmouseover = function() {
  hovered = true;
}
heart.onmouseout = function() {
  setTimeout(function() { hovered = false; }, 500);
  PrefixedEvent(heart, "TransitionEnd", TransitionListener);
  heart.style.webkitTransform = 'scale(1)';
  heart.style.MozTransform = 'scale(1)';
  heart.style.msTransform = 'scale(1)';
  heart.style.OTransform = 'scale(1)';
  heart.style.transform = 'scale(1)';  
}

Pure CSS

With Javascript

Here, JavaScript makes sure that the final animation is much more stable than how it is in pure CSS.

Playing Around with CSS Animations

Now, we saw some events for animations like animationStart, animationEnd and so on. But you would need to branch out of your comfort zone to alter the animation right in the middle of the animation taking place. For the same, the animation-play-state property proves to be handy and helps you to pause the animation as and when you want. Make the following change in your JS:
JS
12
element.style.webkitAnimationPlayState = "paused";
element.style.webkitAnimationPlayState = "running";
It would serve us well if we can fetch the current keyvalue percentage so that we have a better idea of the animation's progress. The setInterval proves to be highly useful for the same. This is how we use it in the JavaScript:
JS
12345678
var showPercent = window.setInterval(function() {
  if (currentPercent < 100) {
    currentPercent += 1;
  } else {
    currentPercent = 0;
  }
  result.innerHTML = currentPercent;
}, 40);

What are the Current Values of CSS Animation

Finding out the same is highly consequential on the overall exercise. Here are some code snippets that can be made use of when obtaining and altering CSS animation midway is on the agenda:
HTML
123
<div id="circle"></div>
        <div id='button'>ChangeAnimation</div>
        <div id='result'></div>
CSS
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647
@-webkit-keyframes rotate {
    0% {
        -webkit-transform:translate(98px, 98px) rotate(0deg) translate(-98px, -98px) rotate(0deg);
        background-color:red;
    }  
  13% {
        -webkit-transform:translate(98px, 98px) rotate(39deg) translate(-98px, -98px) rotate(-39deg);
  }
  25% {
      -webkit-transform:translate(98px, 98px) rotate(85deg) translate(-98px, -98px) rotate(-85deg);
  }
  38% {
      -webkit-transform:translate(98px, 98px) rotate(129deg) translate(-98px, -98px) rotate(0deg);
  }
  50% {
      -webkit-transform:translate(98px, 98px) rotate(180deg) translate(-98px, -98px) rotate(-180deg);
  }
  63% {
      -webkit-transform:translate(98px, 98px) rotate(225deg) translate(-98px, -98px) rotate(225deg);
  }
  75% {
      -webkit-transform:translate(98px, 98px) rotate(270deg) translate(-98px, -98px) rotate(-270deg);
  }
  88% {
      -webkit-transform:translate(98px, 98px) rotate(315deg) translate(-98px, -98px) rotate(315deg);
  }
    100% {
        -webkit-transform:translate(98px, 98px) rotate(360deg) translate(-98px, -98px) rotate(-360deg);
    }
}
#circle {
    height: 50px;
    width: 50px;
    border-radius:25px;
    background-color: teal;
    -webkit-animation-duration: 4s;
    -webkit-animation-timing-function: linear;
    -webkit-animation-name:"rotate";
    -webkit-animation-iteration-count: infinite;
    position:absolute;
    left:30%;
    top:20%; 
}
#button {
  width:130px;
  background:teal;
}
JS
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
// NOTE: The change to red signifies the start of 
// the animation


// Allows elements to be accessed in a clean way
var circle = document.getElementById('circle'), 
    button = document.getElementById('button');

    // Gets element to show current percentage
var result = document.getElementById('result'),
    // Current position of circle around its path
    // in percent in reference to the original
    totalCurrentPercent = 0,
    // Percent of circle around its path in
    // percent in reference to the latest origin
    currentPercent = 0;

// Updates the percent change from the latest origin
var showPercent = window.setInterval(function() {
  if(currentPercent < 100)
  {
    currentPercent += 1;
  }
  else {
    currentPercent = 0;
  }
  result.innerHTML = currentPercent;
}, 39); // Runs at a rate based on the animation's
        // duration (milliseconds / 100)



// Checks to see if the specified rule is within 
// any of the stylesheets found in the document;
// returns the animation object if so
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 &#038;&#038; ss[i].cssRules[j].name == rule) { return ss[i].cssRules[j]; }
        }
    }
    return null;
}

// Replaces the animation based on the percent
// when activated and other hard coded 
// specifications
function change(anim) {
  // Obtains the animation object of the specified
  // animation
  var keyframes = findKeyframesRule(anim),
      length = keyframes.cssRules.length;
  
  // Makes an array of the current percent values
  // in the animation
  var keyframeString = [];  
  for(var i = 0; i < length; i ++)
  {
    keyframeString.push(keyframes[i].keyText);
  }
  
    
  // Removes all the % values from the array so
  // the getClosest function can perform calculations
  var keys = keyframeString.map(function(str) {
    return str.replace('%', '');
  });
  
  // Updates the current position of the circle to
  // be used in the calculations
  totalCurrentPercent += currentPercent;
  if(totalCurrentPercent > 100)
  {
    totalCurrentPercent -= 100;
  }
  // Self explanatory variables if you read the
  // description of getClosest
  var closest = getClosest(keys);
  
  var position = keys.indexOf(closest), 
      firstPercent = keys[position];
  
  // Removes the current rules of the specified 
  // animation
  for(var i = 0, j = keyframeString.length; i < j; i ++)
  {
    keyframes.deleteRule(keyframeString[i]);
  }
  
  // Turns the percent when activated into the
  // corresponding degree of a circle
  var multiplier = firstPercent * 3.6;
  
  // Essentially this creates the rules to set a new 
  // origin for the path based on the approximated
  // percent of the animation when activated and
  // increases the diameter of the new circular path  
  keyframes.insertRule("0% { -webkit-transform: translate(98px,98px) rotate(" + (multiplier + 0) + "deg) translate(-98px,-98px) rotate(" + (multiplier + 0) + "deg); background-color:red; }");
  keyframes.insertRule("13% { -webkit-transform: translate(98px,98px) rotate(" + (multiplier + 45) + "deg) translate(-98px,-98px) rotate(" + (multiplier + 45) + "deg); }");
  keyframes.insertRule("25% { -webkit-transform: translate(98px,98px) rotate(" + (multiplier + 90) + "deg) translate(-98px,-98px) rotate(" + (multiplier + 90) + "deg); }");
  keyframes.insertRule("38% { -webkit-transform: translate(98px,98px) rotate(" + (multiplier + 135) + "deg) translate(-98px,-98px) rotate(" + (multiplier + 135) + "deg); }");
  keyframes.insertRule("50% { -webkit-transform: translate(98px,98px) rotate(" + (multiplier + 180) + "deg) translate(-98px,-98px) rotate(" + (multiplier + 180) + "deg); }");
  keyframes.insertRule("63% { -webkit-transform: translate(98px,98px) rotate(" + (multiplier + 225) + "deg) translate(-98px,-98px) rotate(" + (multiplier + 225) + "deg); }");
  keyframes.insertRule("75% { -webkit-transform: translate(98px,98px) rotate(" + (multiplier + 270) + "deg) translate(-98px,-98px) rotate(" + (multiplier + 270) + "deg); }");
  keyframes.insertRule("88% { -webkit-transform: translate(98px,98px) rotate(" + (multiplier + 315) + "deg) translate(-98px,-98px) rotate(" + (multiplier + 315) + "deg); }");
  keyframes.insertRule("100% { -webkit-transform: translate(98px,98px) rotate(" + (multiplier + 360) + "deg) translate(-98px,-98px) rotate(" + (multiplier + 360) + "deg); }");
  
  // Shows the circle again
  circle.style.display = "inherit";
  // Sets the animation to the newly specified rules 
  circle.style.webkitAnimationName = anim; 
  
  // Resets the approximate animation percent counter
  window.clearInterval(showPercent);
  currentPercent = 0;
  showPercent = self.setInterval(function() {
    if(currentPercent < 100)
    {
      currentPercent += 1;
    }
    else {
      currentPercent = 0;
    }
    result.innerHTML = currentPercent;
  }, 39); 
}

// Attatches the change function to the button's
// onclick function
button.onclick = function() {
  // Removes the animation so a new one can be set
  circle.style.webkitAnimationName = "none";
  // Temporarily hides the circle
  circle.style.display = "none";
  // Initializes change function
  setTimeout(function () { 
      change("rotate"); 
  }, 0);
}

// Gets the animation's closest % value based on
// the approximated % found below
function getClosest(keyframe) {
  var curr = keyframe[0];
  var diff = Math.abs (totalCurrentPercent - curr);
  for (var val = 0, j = keyframe.length; val < j; val++) {
    var newdiff = Math.abs(totalCurrentPercent - keyframe[val]);
    if (newdiff < diff) {
      diff = newdiff;
      curr = keyframe[val];
     }
  }
  return curr;
}
ChangeAnimation

Practically Altering the Animation

To begin with, we need to know how many circles have been completed by the animation from its starting point and also how many circles it has completed from the latest recorded starting point. For both, we need different variables. The second variable can be changed using the setInterval function. As soon as the button is clicked, the following JavaScript code will come into play to update it:
JS
1234
totalCurrentPercent += currentPercent;
if (totalCurrentPercent > 100) {
  totalCurrentPercent -= 100;
}
Now, we will determine the closest keyframe to the percentage we have at present:
JS
12345678910111213141516171819
function getClosest(keyframe) {
  // curr stands for current keyframe
  var curr = keyframe[0];
  var diff = Math.abs (totalCurrentPercent - curr);
  for (var val = 0, j = keyframe.length; val < j; val++) {
    var newdiff = Math.abs(totalCurrentPercent - keyframe[val]);
    // If the difference between the current percent and the iterated 
    // keyframe is smaller, take the new difference and keyframe
    if (newdiff < diff) {
      diff = newdiff;
      curr = keyframe[val];
     }
  }
  return curr;
}
The .indexOf method will be used the first keyframe value of fresh animation:
for (var i = 0, j = keyframeString.length; i < j; i ++) {
  keyframes.deleteRule(keyframeString[i]);
}
And now comes changing the percentage into the circle's degree. Following that, the new rles have to be charted out, based on which variables we have fetched so far.
JS
123456789
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); 
}");
The current percent setInterval will then be reset. Cross browser compatibility has also be taken into account meanwhile:
JS
123456789101112
var browserPrefix;
navigator.sayswho= (function(){
  var N = navigator.appName, ua = navigator.userAgent, tem;
  var M = ua.match(/(opera|chrome|safari|firefox|msie)\/?\s*(\.?\d+(\.\d+)*)/i);
  if(M && (tem = ua.match(/version\/([\.\d]+)/i))!= null) M[2] = tem[1];
  M = M? [M[1], M[2]]: [N, navigator.appVersion,'-?'];
  M = M[0];
  if(M == "Chrome") { browserPrefix = "webkit"; }
  if(M == "Firefox") { browserPrefix = "moz"; }
  if(M == "Safari") { browserPrefix = "webkit"; }
  if(M == "MSIE") { browserPrefix = "ms"; }
})();
The CSS animations thus have been simplified using JavaScript. Concludingly, the task was not as hard as you might think, but it would demand attentiveness on your part. CSS can really produce some astounding animations, but you always have the option of using JavaScript in close conjunction with it. The final results are more than gratifying.