JavaScript to play and replay CSS animations on load and scroll

Posted on

Problem

The following plays and replays CSS animations on load and scroll.

It works but I’m wondering if there is a better way to write this, so it’s cleaner and performs better.

<script>
    var invoiceAnim = document.getElementsByClassName('mod-anim__invoice')[0],
    invoiceBgLoading = document.getElementsByClassName('mod-anim__invoice__bg-loading')[0];
    invoiceLoadingBar = document.getElementsByClassName('mod-anim__invoice__loading-bar')[0];
    invoiceModern = document.getElementsByClassName('mod-anim__invoice__modern')[0],
    invoiceClassic = document.getElementsByClassName('mod-anim__invoice__classic')[0],
    invoiceContemporary = document.getElementsByClassName('mod-anim__invoice__contemporary')[0],

    paymentAnim = document.getElementsByClassName('mod-anim__payment')[0];
    paymentInterface = document.getElementsByClassName('mod-anim__payment__interface')[0],
    paymentBtn = document.getElementsByClassName('mod-anim__payment__btn-pay')[0],
    paymentSuccess = document.getElementsByClassName('mod-anim__payment__success')[0],

    accountingAnim = document.getElementsByClassName('mod-anim__accounting')[0],
    accountingBgReport = document.getElementsByClassName('mod-anim__accounting__bg-report')[0],
    accountingBars = document.getElementsByClassName('mod-anim__accounting__bars')[0],

    payrollAnim = document.getElementsByClassName('mod-anim__payroll')[0],
    payrollInterface = document.getElementsByClassName('mod-anim__payroll__interface')[0],
    payrollReviewBtnRollover = document.getElementsByClassName('mod-anim__payroll__btn-review-payroll-rollover')[0],
    payrollWhiteBar = document.getElementsByClassName('mod-anim__payroll__white-bar')[0],
    payrollPayStub = document.getElementsByClassName('mod-anim__payroll__pay-stub')[0],
    payrollApproveBtn = document.getElementsByClassName('mod-anim__payroll__btn-approve-payroll')[0],
    payrollApproveBtnRollover = document.getElementsByClassName('mod-anim__payroll__btn-approve-payroll-rollover')[0],

    invoicePlayState = 'notLoaded',
    paymentPlayState = 'notLoaded',
    accountingPlayState = 'notLoaded',
    payrollPlayState = 'notLoaded';

    // (mouseover) > replay invoice
    invoiceAnim.addEventListener('mouseenter', function() {
      if (invoicePlayState === 'readyToReplay'){
        console.log('invoice anim hover, IN IF');
        invoiceBgLoading.style.webkitAnimation = 'none';
        invoiceLoadingBar.style.webkitAnimation = 'none';
        invoiceModern.style.webkitAnimation = 'none';
        invoiceClassic.style.webkitAnimation = 'none';
        invoiceContemporary.style.webkitAnimation = 'none';

        setTimeout(function() {
          invoiceBgLoading.style.webkitAnimation = '';
          invoiceLoadingBar.style.webkitAnimation = '';
          invoiceModern.style.webkitAnimation = '';
          invoiceClassic.style.webkitAnimation = '';
          invoiceContemporary.style.webkitAnimation = '';
        }, 10);

        invoicePlayState = 'playing';
        setTimeout(function() {
          invoicePlayState = 'readyToReplay';
          console.log('invoice ended');
        }, 500);
      }
    }, false);

     // (mouseover) replay payment
    paymentAnim.addEventListener('mouseenter', function() {
      if (paymentPlayState === 'readyToReplay'){
        console.log('payment anim hover IN IF');
        paymentInterface.style.webkitAnimation = 'none';
        paymentBtn.style.webkitAnimation = 'none';
        paymentSuccess.style.webkitAnimation = 'none';

        setTimeout(function() {
          paymentInterface.style.webkitAnimation = '';
          paymentBtn.style.webkitAnimation = '';
          paymentSuccess.style.webkitAnimation = '';
        }, 10);

        // prevent user from replaying anim too soon
        paymentPlayState = 'playing';
        setTimeout(function() {
          paymentPlayState = 'readyToReplay';
          console.log('payment ended');
        }, 500);
      }
    }, false);

    // (mouseover) replay accounting
    accountingAnim.addEventListener('mouseenter', function() {
      if (accountingPlayState === 'readyToReplay'){
        console.log('acct anim hover');
        accountingBgReport.style.webkitAnimation = 'none';
        accountingBars.style.webkitAnimation = 'none';

        setTimeout(function() {
          accountingBgReport.style.webkitAnimation = '';
          accountingBars.style.webkitAnimation = '';
        }, 10);

        accountingPlayState = 'playing';
        setTimeout(function() {
          accountingPlayState = 'readyToReplay';
           console.log('acct ended');
        }, 500);
      }
    }, false);

    // (mouseover)  > replay payroll
    payrollAnim.addEventListener('mouseenter', function() {
      console.log('payroll play state = ' + payrollPlayState);
      if (payrollPlayState === 'readyToReplay'){
         console.log('payroll anim hover IN IF');
        payrollInterface.style.webkitAnimation = 'none';
        payrollReviewBtnRollover.style.webkitAnimation = 'none';
        payrollWhiteBar.style.webkitAnimation = 'none';
        payrollPayStub.style.webkitAnimation = 'none';
        payrollApproveBtn.style.webkitAnimation = 'none';
        payrollApproveBtnRollover.style.webkitAnimation = 'none';

        setTimeout(function() {
          payrollInterface.style.webkitAnimation = '';
          payrollReviewBtnRollover.style.webkitAnimation = '';
          payrollWhiteBar.style.webkitAnimation = '';
          payrollPayStub.style.webkitAnimation = '';
          payrollApproveBtn.style.webkitAnimation = '';
          payrollApproveBtnRollover.style.webkitAnimation = '';
          }, 10);
        }

        payrollPlayState = 'playing';
        setTimeout(function() {
          payrollPlayState = 'readyToReplay';
           console.log('payroll ended');
        }, 500);
    }, false);

    // play anim
    function playAnim(anim){
      switch (anim){
        case 'invoiceAnim':
          if (invoicePlayState === 'notLoaded'){
            invoiceBgLoading.className += " mod-anim__invoice__bg-loading--animate";
            invoiceLoadingBar.className += " mod-anim__invoice__loading-bar--animate";
            invoiceModern.className += " mod-anim__invoice__modern--animate";
            invoiceClassic.className += " mod-anim__invoice__classic--animate";
            invoiceContemporary.className += " mod-anim__invoice__contemporary--animate";
            invoicePlayState = 'playing';
            setTimeout(function() {
              invoicePlayState = 'readyToReplay';
               console.log('invoice 1 ended');
            }, 500);
          }
          break; 
        case 'paymentAnim':
          if (paymentPlayState === 'notLoaded'){
            paymentInterface.className += " mod-anim__payment__interface--animate";
            paymentBtn.className += " mod-anim__payment__btn-pay--animate";
            paymentSuccess.className += " mod-anim__payment__success--animate";
            paymentPlayState = 'playing';
            setTimeout(function() {
              paymentPlayState = 'readyToReplay';
               console.log('payment 1 ended');
            }, 500);
          }
          break;
        case 'accountingAnim':
          if (accountingPlayState === 'notLoaded'){
            accountingBgReport.className += " mod-anim__accounting__bg-report--animate";
            accountingBars.className += " mod-anim__accounting__bars--animate";
            accountingPlayState = 'playing';
            setTimeout(function() {
              accountingPlayState = 'readyToReplay';
               console.log('acct 1 ended');
            }, 500);
          }
          break;
        case 'payrollAnim':
          if (payrollPlayState === 'notLoaded'){
            payrollInterface.className += " mod-anim__payroll__interface--animate";
            payrollReviewBtnRollover.className += " mod-anim__payroll__btn-review-payroll-rollover--animate";
            payrollWhiteBar.className += " mod-anim__payroll__white-bar--animate";
            payrollPayStub.className += " mod-anim__payroll__pay-stub--animate";
            payrollApproveBtn.className += " mod-anim__payroll__btn-approve-payroll--animate";
            payrollApproveBtnRollover.className += " mod-anim__payroll__btn-approve-payroll-rollover--animate";
            payrollPlayState = 'playing';
            setTimeout(function() {
              payrollPlayState = 'readyToReplay';
               console.log('payroll 1 ended');
            }, 500);
          }
          break;
      }
    }

    // (helper) get element document position
    function offset(el) {
      var rect = el.getBoundingClientRect(),
      scrollTop = window.pageYOffset || document.documentElement.scrollTop;
      return { top: rect.top + scrollTop }
    }

    // assign element position
    var scrollTopOffset = 500;
      invoicePos = offset(invoiceAnim),
      paymentPos = offset(paymentAnim),
      accountingPos = offset(accountingAnim),
      payrollPos = offset(payrollAnim);

      invoicePos = invoicePos.top - scrollTopOffset;
      paymentPos = paymentPos.top - scrollTopOffset;
      accountingPos = accountingPos.top - scrollTopOffset;
      payrollPos = payrollPos.top - scrollTopOffset;

    // check if anim is visible
    function isAnimVisible (){
      var scrollPos = window.pageYOffset;

      if (scrollPos > invoicePos && scrollPos < paymentPos){
        playAnim('invoiceAnim');
      }
      if (scrollPos > paymentPos && scrollPos < payrollPos){
        playAnim('paymentAnim');
      }
      if (scrollPos > accountingPos && scrollPos < payrollPos){
        playAnim('accountingAnim');
      }
      if (scrollPos > payrollPos){
        playAnim('payrollAnim');
      }
    }

    window.onload = isAnimVisible;
    window.onscroll = isAnimVisible;

  </script>

Solution

If you find yourself falling into the pattern of:

var someElement = document.getElementsByClassName('some_class')[0];

Then I would question whether you should in fact use an id on these DOM elements vs. using a class, since you are clearly only exepcting a single element for each of those classes. Selecting by ID is preferred approach in javascript to get a single element, as elements are indexed by id. They are not indexed by class name, thus requiring document traversal to find all elements that meet your condition.

I also think you need to think about breaking your application into logical components, each one with its own event listeners and animation logic. right now having single functions with complex case statements and branching logic has the bad code smell. Have you considered trying to define different javascript object classes for each of your application elements, with each object owning properties for play state, DOM elements that are related to the object, callbacks, etc.

This current approach seems very fragile from a maintenance perspective, in that if you need to make a change to one of your page components, you could unexpected impact the other components.

This code should also not be running in global namespace. At a minimum, I would think this code could be placed inside an IIFE.

Leave a Reply

Your email address will not be published. Required fields are marked *