The Setup
Before I was a ‘modern web application developer’, I actually learned how to program in Flash and then Flex. Platform rhetoric aside, the Flash guys are the original gangstas when it comes to slick, animated user interfaces. In this post, I want to show you a technique for doing smooth rollovers that I have been using for years on a Flash timeline and how I was able to replicate it with Greensock’s TimelineLite with surprisingly little effort. The idea is that you lay your animations out on a ‘timeline’ and when the user mouses over the element the timeline begins to play and on mouse out the timeline plays the timeline in reverse. This creates a really nice animation by having the outro animation be the exact opposite of the intro animation. Mouse over the demo below for an example.
Demo
Download the code below to play with my idea and post your result in the comments for ‘valuable prizes’.
CodeInclude the Javascripts
To make this animation work, we need to include jQuery, TweenMax and AngularJS in our HTML file. The entire animation effect is going to be encapsulated into a directive that we will put in our app.js file.
<body> <!-- Omitted --> <pre><code><script src="//code.jquery.com/jquery-1.11.1.min.js"></script> <script src="//cdnjs.cloudflare.com/ajax/libs/gsap/1.13.1/TweenMax.min.js"></script> <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.3.0-rc.0/angular.min.js"></script> <script src="js/app.js"></script> </code></pre> </body>
Create the Smooth Button Template
Because this is a fairly focused and small example, I wanted to abstract the button HTML to keep the DOM clean but I didn’t really want to create an external template since this is not a ‘real’ project. AngularJS allows you to define a template in a script tag with the type of text/ng-template and a string id to identify it later. In this case, we are going to call it smooth-button.tmpl.html which we will reference in the smoothButton directive in a moment.
<body> <!-- Omitted --> <pre><code><script type="text/ng-template" id="smooth-button.tmpl.html"> <div class="circle red"></div> <div class="circle orange"></div> <div class="circle yellow"></div> <div class="circle grey"></div> </script> <!-- Omitted --> </code></pre> </body>
The layout for the button is essentially four colored circles that are going to lay on top of each other. To see the circles individually, take off the position: absolute on the circle class and you will have something that looks like the image below.
.circle { border-radius: 50%; width: 100px; height: 100px; position: absolute; } .yellow { background-color: #ffff00; } .orange { background-color: orange; } .red { background-color: red; } .grey { background-color: grey; }
We are also giving each circle a color by assigning a color class to it as well.
Create the Smooth Button Directive
Now that we have the template defined, we can create the smoothButton directive and reference it in the directive definition object (DDO) via templateUrl: ‘smooth-button.tmpl.html’. One of the interesting ‘features’ of AngularJS directives is that they share a single scope object by default. This is why I have defined scope: true on the DDO so that each directive instance gets its own child scope. This allows us to modify one directive without it updating all of the other directives. #proTip!
var app = angular.module('website', []); app.directive('smoothButton', function(){ var linker = function (scope, element, attrs) { // Omitted }; <pre><code>return { scope: true, link: linker, templateUrl: 'smooth-button.tmpl.html' } </code></pre> });
Define the TimelineLite Instance
And now we are going to create a TimelineLite instance on the tl variable so that we can act upon it when the user interacts with the directive. Once tl is defined, we are going to call tl.stop() so that the tl does not automatically play the animation sequence. We are also going to expose two methods on scope called scope.play and scope.reverse that act as a proxy to tl.play and tl.reverse, respectively. This will allow us to control the playback of the animations that we add to the TimelineLite instance.
var app = angular.module('website', []); app.directive('smoothButton', function(){ var linker = function (scope, element, attrs) { var tl = new TimelineLite(); // Omitted tl.stop(); <pre><code> scope.play = function() { tl.play(); }; scope.reverse = function() { tl.reverse(); }; }; return { scope: true, link: linker, templateUrl: 'smooth-button.tmpl.html' } </code></pre> });
Smooth Button Enter Stage Left
We will now add three smoothButton instances to our layout by creating three div tags and adding smooth-button as an attribute. Remember that AngularJS converts camel case to snake case in HTML which is why it went from smoothButton to smooth-button.
<body> <div smooth-button class="container" ng-mouseenter="play()" ng-mouseleave="reverse()"></div> <div smooth-button class="container" ng-mouseenter="play()" ng-mouseleave="reverse()"></div> <div smooth-button class="container" ng-mouseenter="play()" ng-mouseleave="reverse()"></div> <pre><code><!-- Omitted --> </code></pre> </body>
And now we can invoke the play method by calling ng-mouseenter=”play()” and the reverse method by calling ng-mouseleave=”reverse()”.
Grand Finale: Show Me The Animations!
And now the only thing left to do is add the animations. TimelineLite makes this really easy by providing an add method that we can use to add our animations. We are going to use TweenLite.to to animate the red, orange and yellow circles. Because element is a jQuery object, we can query element to get us the circle we want such as element.find(‘.red’) to fetch the red circle. We will set the duration of all three animations to 0.4 seconds to keep things lively and we are going to set a negative delay on the last two animations by adding ‘-=0.2’ as the last parameter. Timeline animations are generally sequential but you can cause them to overlap with a negative delay. #proTip!
Now that we know WHO we are animating and WHEN it is all going down, it is time to define the WHAT. For each circle, we want to increase width and height by some number and we can do this with scaleX and scaleY. Also, we want to make it pretty so we use Power2.easeOut as our easing function. Hooray!
var app = angular.module('website', []); app.directive('smoothButton', function(){ var linker = function (scope, element, attrs) { var tl = new TimelineLite(); tl.add(TweenLite.to(element.find('.red'), 0.4, {scaleX:1.8, scaleY:1.8, ease: Power2.easeOut})); tl.add(TweenLite.to(element.find('.orange'), 0.4, {scaleX:1.6, scaleY:1.6, ease: Power2.easeOut}), '-=0.2'); tl.add(TweenLite.to(element.find('.yellow'), 0.4, {scaleX:1.4, scaleY:1.4, ease: Power2.easeOut}), '-=0.2'); tl.stop(); <pre><code> scope.play = function() { tl.play(); }; scope.reverse = function() { tl.reverse(); }; }; return { scope: true, link: linker, templateUrl: 'smooth-button.tmpl.html' } </code></pre> });
And the HTML in its entirety.
<body> <div smooth-button class="container" ng-mouseenter="play()" ng-mouseleave="reverse()"></div> <div smooth-button class="container" ng-mouseenter="play()" ng-mouseleave="reverse()"></div> <div smooth-button class="container" ng-mouseenter="play()" ng-mouseleave="reverse()"></div> <pre><code><script type="text/ng-template" id="smooth-button.tmpl.html"> <div class="circle red"></div> <div class="circle orange"></div> <div class="circle yellow"></div> <div class="circle grey"></div> </script> <script src="//code.jquery.com/jquery-1.11.1.min.js"></script> <script src="//cdnjs.cloudflare.com/ajax/libs/gsap/1.13.1/TweenMax.min.js"></script> <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.3.0-rc.0/angular.min.js"></script> <script src="js/app.js"></script> </code></pre> </body>
Review
For a quick review, we were able to encapsulate an interesting animation within a directive and then control playback via the TimelineLite API. In this example, we used circles but you could easily change this by defining other shapes via CSS and swapping them out.
One of my primary goals as I share the things that I have learned is to not only present them in such a simple way that anyone can get them but also leave the stage wide open for you to extend my ideas into anything you want. So for homework, grab the code and post a variation. I would love to see it!
Resources
Getting Started with the JavaScript Version of the GreenSock Animation Platform (GSAP)
Greensock TimelineLite Documentation
Code
Why do you need all that overhead for something that can be simply done with one element and some CSS3?
http://codepen.io/davidkpiano/pen/weHnd
David… sweet example! Thanks for sharing. I absolutely agree that this particular example could be accomplished a few different ways as you have proven. My intention was to lay a foundation for developers to extend when CSS is not an appropriate animation mechanism. There are a lot of good points in this Myth Busting: CSS Animations vs. JavaScript blog post on CSS Tricks where such a case may arise.
Excellent write up.
I have been thinking about looking at Greensock for a while and your post has given me the impetuous to have a go.
Well done.
Excellent article! Two suggestions:
1) You make it sound like scope:true always creates a new child scope. Maybe I’m not reading your correctly, but it’s certainly not true http://plnkr.co/edit/dzIU3XDk3w0odWh1Qx0U?p=preview
2) Name your directive scope-smooth-button and in your DDO use controllerAs: smoothButton….. then you will have markup like this:
It’s a little bit more verbose, but once you establish this convention it makes it really easy to read the markup. Otherwise you’ll see play() and reverse() and have to dig through the codebase to figure out what’s going on.
Thanks Gil! Concerning scope: true… from the docs
“If set to true, then a new scope will be created for this directive. If multiple directives on the same element request a new scope, only one new scope is created. The new scope rule does not apply for the root of the template since the root of the template always gets a new scope.”
I presumed that the ‘new scope’ was a child scope… well it is a child of something! haha
As for point two… I am still warming up to the controller as syntax but a very valid point. Appreciate the input!
Great tutorial! I would be interested in more integration of Greensock with Angular in the future. Thanks a lot for your posts, I learn something new every time!