Last Updated: December 30, 2020
·
22.55K
· jjperezaguinaga

AngularJS: Scroll Animations

Introduction

Everyone has seen scroll based animations right? You know, the ones where you start scrolling down the webpage and animations start triggering around depending on how much you have scrolled. One of my favorite examples is Let's free Congress.

Now, sometimes we want to trigger an animation, but we don't want to make the entire page to rely on the scroll... maybe, just a little part of it. However, we can't trigger the animation until the user is viewing the part we want to animate, or else the animation will do all its magic without no audience. How do we do it?

The scrollPosition directive

Let me introduce you the scrollPosition directive.

.directive('scrollPosition', ['$window', '$timeout', '$parse', function($window, $timeout, $parse) {
    return function(scope, element, attrs) {

        var windowEl = angular.element($window)[0];
        var directionMap = {
          "up": 1,
          "down": -1,
          "left": 1,
          "right": -1
        };

        // We retrieve the element with the scroll
        scope.element = angular.element(element)[0];

        // We store all the elements that listen to this event
        windowEl._elementsList = $window._elementsList || [];
        windowEl._elementsList.push({element: scope.element, scope: scope, attrs: attrs});

        var element, direction, index, model, scrollAnimationFunction, tmpYOffset = 0, tmpXOffset = 0;
        var userViewportOffset = 200;

        function triggerScrollFunctions() {

          for (var i = windowEl._elementsList.length - 1; i >= 0; i--) {
            element = windowEl._elementsList[i].element;
            if(!element.firedAnimation) {
              directionY = tmpYOffset - windowEl.pageYOffset > 0 ? "up" : "down";
              directionX = tmpXOffset - windowEl.pageXOffset > 0 ? "left" : "right";
              tmpXOffset = windowEl.pageXOffset;  
              tmpYOffset = windowEl.pageYOffset;  
              if(element.offsetTop - userViewportOffset < windowEl.pageYOffset && element.offsetHeight > (windowEl.pageYOffset - element.offsetTop)) {
                model = $parse(windowEl._elementsList[i].attrs.scrollAnimation)
                scrollAnimationFunction = model(windowEl._elementsList[i].scope)
                windowEl._elementsList[i].scope.$apply(function() {
                  element.firedAnimation = scrollAnimationFunction(directionMap[directionX]);  
                })
                if(element.firedAnimation) {
                  windowEl._elementsList.splice(i, 1);
                }
              }
            } else {
              index = windowEl._elementsList.indexOf(element); //TODO: Add indexOf polyfill for IE9 
              if(index > 0) windowEl._elementsList.splice(index, 1);
            }
          };
        };
        windowEl.onscroll = triggerScrollFunctions;
      };   
    }]);

This directive was used to craft the following animation.

Picture

Pretty much, when you scroll down through the page, the slider starts going up until we reach the limit, and the animation only triggers when the user is currently viewing the element to animate.

Usage

First, we are going to see how we can use this directive in your project and then we are going to break it in little pieces to understand how it works.

In the element you want to perform an animation, add the directive. In our case it was this (Jade HTML):

.row.show-for-large-up
  .teaser(scroll-position, scroll-animation='fireupApplicationDesignAnimation')
    .row
      .large-6.columns.left-align.margin-top
        h1(data-i18n="_CareerDesign_APPLICATIONDESIGNTITLE")

This tells AngularJS that when the user starts to scrolls within the area of this DOM element, it will trigger the scroll-animation function. In your controller, you should have something like this

$scope.fireupApplicationDesignAnimation = function(scrollDirection) {
        scrollDirection > 0 ? reduceAmount() : aumentAmount(); // We want to increase on scrollDown
        setOffsetForImage();
    };

The directive sends to your controller function the direction of the scroll, so you can even perform animations based on this. Sometimes, you need to trigger the animation only once, so you need to return true in order to do so. An example from the same page:

$scope.fireupMarketingDesignAnimation = function() {
      if(!firedMarketingAnimation) {
        window.animations.marketingAnimation.init();
        firedMarketingAnimation = true;
        return firedMarketingAnimation;  
      }
    }

In this one, we have an animation that only needs to be triggered once. Since we don't know at which point of the scroll the user will match the DOM viewport (remember, he/she can scroll really fast!), we ought to wrap our function in a flag that ensures this only happens once.

How it works?

This directive performs the following:

  • Creates an array of elements that require an onScroll eventListener (you can have multiples, they will be stored and removed in case you return a true value from your triggering function in order to ensure memory usage)
  • Adds an eventLister onScroll that checks the current user position on the webage
  • Travels through all your binded elements (although I haven't had performance problems, this could be an issue if you have too many binded elements with the directive) and checks if they have triggered their animation.
  • If they haven't and the user is within the viewport of the scroll, they do. This makes heavy usage of $parse, so make sure to read it's docs in order to understand completely.
  • Pops the element that was binded with the directive if the animation called within the scope's function returned a true value.

Conclusion

This directive is good when you need to perform many animations that are triggered through a specific amount of time. It's also effective when you want a specific behavior that relies on the user position, or even changing CSS3 values as you scroll. I made a Codepen to show off this, which also works in a horizontal scroll.

Picture

Basically I'm moving and rotating the ball on each scroll, depending on which direction the user goes.

Be warned though, that if the user has a kick as screen that allows him to see all your content without scroll, he might not be able to see the show. Fill up a timeout to make sure he/she does. As a final note, I'm using indexOf which it's not IE friendly, but can be easily replaced with a simple for..loop.

3 Responses
Add your response

If I scrolled fast the ball gets off the screen.

over 1 year ago ·

@vohof Aha! True. The directive performs the function every time the user scrolls. Since the user can scroll all the way down (in the case of the ball, all the way to the left) in a single scroll, then the directive performs this only once. The same way, if the user scrolls it reeeallly slow, the function is performed many, many times.

How do we solve this? There are two possible ways. The first one is to create a relationship between the "distance" the user scrolls and the amount of "scrolls" it performs in that distance. Let's call it "scroll speed". In the directive function, calculate that through a function and then pass it to your function the same way I'm passing the direction.

The second way is through step based scrolls. Your element has a finite height (or width, in the case of the ball). You can divide this dimension in the amount of steps you want the animation to be performed. Then, when the user scrolls, calculate at which percentage of this dimension the user is going to finish its scroll. E.g. if I scroll to the 50% of my element, then I want to perform 50 times the function given that I assigned a total of 100 times. This is useful for timeline functions, where you only need maybe 3 or 4 animations to be performed: if the user performs scrolls within the range of 0-25% your element's dimension, perform one step animation; 25-50% second step and so on.

IMHO, unless you really need something quite specific, use this directive as a trigger and THEN control your animation with a different mechanic (e.g. timeouts, intervals). I usually always return true after the first function call to dismiss the triggering for the rest of the scrolling.

over 1 year ago ·

Is the scroll-animation function apart of angular (i.e. a directive), or is that a normal html attribute?

Also, how can I add a css class dynamically if the element is on screen? My first plan was to use the 'fireupMarketingDesignAnimation' callback, and add the css in that function, but I'm not sure how I could add css to the specific element.

I essentially want to animate only if the item is on screen. I have css classes that I'm already using for animations (https://github.com/daneden/animate.css), but I just want to add them dynamically based on scroll position. Any advice?

over 1 year ago ·