Jump to content
GreenSock

Search In
  • More options...
Find results that contain...
Find results in...
JD9

DRY JS/GSAP syntax question for GSAP Effect Snippets Library

Recommended Posts

Hi everyone! Being inspired by standard effect galleries in css3 such as hover.css and animate.css

I reckoned "why not develop something like that for GSAP beginners?". My goal is to develop a basic set of "Add Water" effects on top of GSAP, to help absolute beginners discover the power of GSAP. So I've picked up the challenge to develop my own "GSAP Effect Snippets Library" and thinking about deploying it on GitHub or so, or in any case "sharing is caring"! ;-)

 

Yet I'm a GSAP and JS beginner myself ;-) And currently I'm faced with a simple Don't Repeat Yourself (DRY) JS/GSAP architecture question. Can you help me out?

Current state:
This is where I'm at right now, I've just started with three very simple effects. The code is working, yet with a very clumsy architecture, not suited for my goal at all.

$(document).ready(function() {	

	
	// IIFE: grow
(function grow() {
    $('.gsap-anim-grow').hover(enter, leave);

    function gsap_Anim_Grow(el) {        
        var tl = new TimelineLite();
        tl.to(el, 0.3, { scale: 1.2, ease: Sine.easeOut} )
        return tl;
    }

    function enter() {
          if( !this.animation ) {
              this.animation = gsap_Anim_Grow(this);
          } else {            
               this.animation.play();
          }
    }

    function leave() { this.animation.reverse(); }    
})();



// IIFE: shrink
    (function shrink() {
    $('.gsap-anim-shrink').hover(enter, leave);

    function gsap_Anim_Shrink(el) {        
        var tl = new TimelineLite();
        tl.to(el, 0.3, { scale: 0.8, ease: Sine.easeOut} )
        return tl;
    }

    function enter() {
          if( !this.animation ) {
              this.animation = gsap_Anim_Shrink(this);
          } else {            
               this.animation.play();
          }
    }

    function leave() { this.animation.reverse(); }    
})();

// IIFE: skew
    (function skew() {
    $('.gsap-anim-skew').hover(enter, leave);

    function gsap_Anim_Skew(el) {        
        var tl = new TimelineLite();
        tl.to(el, 0.2, { skewX: -8, ease: Sine.easeOut} )
        return tl;
    }

    function enter() {
          if( !this.animation ) {
              this.animation = gsap_Anim_Skew(this);
          } else {            
               this.animation.play();
          }
    }

    function leave() { this.animation.reverse();  }    
})();



});

I'm repeating myself over and over this way where the only thing changing is the animation itself. It would seem far more logical to me if I could put all animations in a JSON-style object, like so:

 

var animations = {
    gsap_Anim_Grow: function (el) {            
        var tl = new TimelineLite();
        tl.to(el, 0.3, { scale: 1.2, ease: Sine.easeOut} );
        return tl;
    },

    gsap_Anim_Shrink: function (el) {        
        var tl = new TimelineLite();
        tl.to(el, 0.3, { scale: 0.8, ease: Sine.easeOut} )
        return tl;
    }
}

Note: On the other hand... on some effects (e.g. a horizontalWobble) it's desirable that the mouseleave handle triggers no effect (desired effect: "only wobbling at mouseenter, nothing happening on mouseleave"), so in that case the "leave" function / parameter should do nothing. It seems as an improvement to include the enter / leave behavior within each animation function. But how do I accomplish that syntactically?

 

Also, I'd like to use ONE handlerInOut on the .hover() method, so that any given effect can be called from

a simple jQuery selector, like so:

// My preferred animation call
// Any jquery class selector, same as the animation function name
// right now, but can be any class in the DOM,
// And preferably just one handlerInOut for beginner convenience.

$('.gsap-anim-grow').hover(gsap_Anim_Grow);	

How to re-write this JS structure into a proper DRY manner so that the complete animation library can be simply declared in the HTML?

Can you put me on the right track?
Thanks very much in advance,
-Jos

Link to comment
Share on other sites

Objects are your friend! Since you're new to JavaScript, I think using factory functions to create objects might be the easiest pattern to start with. So what's a factory function? It's just a function that creates and returns an object.

 

Want an example? There you go.

var foo = $(".foo");
 

Calling jQuery creates and returns an object which exposes some more functionality like this.

var color = foo.css("color");
 

The object a factory function creates can be something like a tween or timeline. Just think of what the name "factory" means. It's creating something. Most of the code you posted are actually factory functions. The only problem is that you wrapped them in IIFEs, so they aren't reusable.

 

Simple factory creating a tween…

function moveTween(element) {
  return TweenLite.to(element, 1, { x: 100 });
}
  

But that's rather static. Let's pass in some config values with some default values.

function moveTween(element, config) {
  
  config = config || {}; // Prevent errors in case config wasn't passed in

  var time = config.time != null ? config.time : 1; // Allows 0 to not be evaluated as false

  return TweenLite.to(element, time, { 
    x: config.x != null ? config.x : 100, 
    backgroundColor: config.backgroundColor || "red"
  });
}
 

If you aren't familiar with the ? symbol, that's a ternary operator. It's basically a shorthand way to write an if/else statement.

// This can be rewritten…
if (condition) {
  var foo = trueValue;
} else {
  var foo = falseValue;
}

// …like this
var foo = condition ? trueValue : falseValue;
 

And notice that the != null uses only 1 equals sign, not 2. This allows for falsey values like the number 0 to be evaluated as true. You could also use jQuery's .extend() method to set default values for the config object.

 

Check out multiple variations of that factory in action…

See the Pen d7cb86869a0e7c0333d29ecd6a3e21eb?editors=0010 by osublake (@osublake) on CodePen

 

Pretty cool? One thing I really like about animations in Angular are the factory functions, allowing you define multiple animations for a certain class selector like this...

function angularAnimation() {
  return {
    enter: function(element, done) {
      TweenLite.to(element, 1, { autoAlpha: 1, onComplete: done });
    },
    leave: function(element, done) {
      TweenLite.to(element, 1, { scale: 0, onComplete: done });
    }
  };
}
  

You can actually do something very similar with events in jQuery using the .on() method, and this includes custom events.

function jqueryAnimation() {    
  return {
    click: function(event) {
      TweenMax.to(this, 0.25, { scale: 1.1, yoyo: true, repeat: 1 });
    },
    mouseenter: function(event) {
      TweenLite.to(this, 0.25, { opacity: 1 });
    },
    mouseleave: function(event) {
      TweenLite.to(this, 0.25, { opacity: 0.5, rotation: 360 });
    }
  };
}
 

Check it out. Binding animations to events…

See the Pen d120a69a9637f7b3568b69e7cc3775c6?editors=0010 by osublake (@osublake) on CodePen

 

The possibilities are endless. Want to see a more advanced factory function? This one adds Material Design ripples to a host element. If you look at the config object starting at around line 45, those are all the different options you can pass into the factory to create and animate different looking ripples. Holding your mouse down makes the ripple stay.

See the Pen mPgqPY by osublake (@osublake) on CodePen

 

Try building some factories based on the animations in hover.css and animate.css. Most of those animations are fairly simple, and you'll get the hang of doing them with GSAP in no time. The hardest ones are probably going to be the animations that do 3d transforms and shadow/border effects, but this forum has lots examples if you need help.

  • Like 6
Link to comment
Share on other sites

Just to throw more confusion at you. Here's a way to setup animations in your HTML using valid JSON with jQuery. So I can setup the animations for an element doing something like this.

<div data-animations="rotate,scale" data-animation-config='{"rotate":{"rotation":720},"scale":{"scale":0.2}}'></div>

See the Pen 05ce4c289c94e3b36d40f432c809fa33?editors=0010 by osublake (@osublake) on CodePen

  • Like 2
Link to comment
Share on other sites

@Blake: You, Sir, are a true JavaScript sensei!

You have already gotten me this far (working code example, ref my initial code):

$(document).ready(function() {	

        // call this code from within a project's webpage custom JavaScript
	var target = $('.gsap-anim-grow');
	target.on( gsap_Anim_Grow(target) );
    
        // put this code into the Effects Snippets Library
	function gsap_Anim_Grow(element, config) {
		config = config || {};
		var time = config.time != null ? config.time : 0.3;
		var animation = TweenLite.to(element, time, { scale: 1.2, ease: Sine.easeOut, paused: true } );

		return {	    
			mouseenter: function() {      
				animation.play();
			},
			mouseleave: function() {
				animation.reverse();	      
			}       
		};
	}
});

Now, looking at my goal to create an absolute beginner's "Just-add-water GSAP Effects Snippet Library", I'm going to overthink for a bit what's the most convenient syntax for that purpose:

My thoughts:
- currently the function call is easily deployable to any HTML element (event on element A, animation on element A or somewhere else), yet limited to a .hover() event. I might want to pass an 'event' argument to the function. On the other hand, the more flexible/configurable the setup, the harder it becomes to deploy for beginners.

 

PS Blake, does your nickname come from the karate world?
If so: JavaScript wa rei ni hajimari, rei ni owaru. Osu! ;-)

-Jos

Link to comment
Share on other sites

If you want some inspiration for how to structure something this, check this out...

https://angularclass.github.io/ng-fx/#/ng-show-hide

 

It's for Angular, but you can get an idea of how they set it up by looking at some of the code. It used to use GSAP, so I'm linking to an older version of the code. 

 

First off, you can see from the demo that all the options for the animations are class names. So a duration for 600ms would be fx-dur-600. They have a function to parse the class names to get the values.

https://github.com/AngularClass/ng-fx/blob/v1.0.4/src/animationsAssist.js

 

And they're combining different animations to create all those effects. They're all based on these simple effects, zoom, slide, scale, fade, and bounce. Look at how they define an effect in these files. Looks pretty close to what you were looking to do.

https://github.com/AngularClass/ng-fx/tree/v1.0.4/src/animations

https://github.com/AngularClass/ng-fx/tree/v1.0.4/src/transitions

 

Those effects are then passed into an animation factory, which is where they bind the animations to certain Angular events.

https://github.com/AngularClass/ng-fx/blob/v1.0.4/src/animationClass.js

 

This is pretty much all those factories are doing. Pretty close to what I did earlier, except I didn't pass an effect into the factory.

function animationFactory(someEffect) { 

  return {
    mouseenter: function(){

      // Some function to parse any options from the class list
      var options = parseClassList(this);
      TweenMax.to(this, options.duration, someEffect);
    }
  };
}
  • Like 2
Link to comment
Share on other sites

Blake, I understand the direction you're pointing me to. I'm diving into it straight away.
Thanks again!
-Jos

  • Like 1
Link to comment
Share on other sites

Ha. I had to Google what Osu meant. Nope, not from the karate world. Just a stupid name I came up with a long time ago because I couldn't think of one, so I used part of my school email address from Ohio State University.

 

I've never looked into doing something like this, but I looked at that Angular example some more, and came up with some ideas. First, I think trying to reuse timelines/tweens might be hard because it will be very easy to create conflicting animations. Probably best to create every animation as they are called.

 

So here's what I came up with it. I call it gsFx, for GreenSock Effects. Just pass in the events and the effect.

// Simple
$(".foo").gsFx("mousedown touchstart", "fadeOut");

// You can even pass in a hover event
$(".foo").gsFx("hover", "spinLeft");

// And you can pass in options too. 
// A good one is to play the animation on another element
$(".foo").gsFx("hover", "zoomIn", {
  target: $(".bar"),
});

// You can also set an effect to play only once
$(".foo").gsFxOne("click", "bounceIn");

// Or play the effect on demand, so you wouldn't pass in any events
$(".foo").gsFxPlay("shake");

// You can also remove effects
$(".foo").gsFxOff("hover", "spinLeft");

Here's another really cool trick that most people don't know about. This will return a live node list.

var list = document.getElementsByClassName("gsFx");

What does that mean? The list gets updated automatically so you don't have to keep calling it. Now setup a ticker to check the length of that list on every render, and you can find out if a class was added to an element. Yeah, we can use that exact API I just explained, but with CSS classes and data attributes, so adding classes or elements dynamically won't be a problem.

 

First set one of those methods as a class name on an element. It could be any combination of those four, gsFx, gsFxOne, gsFxPlay, gsFxOff.

<div class="gsFx"></div>

We'll check if any those classes were added, and if so, remove the class and create the effect from values declared in the HTML. So you would need also need to define data attributes to match up with it.

<div class="gsFx"
     data-gsfx="slideIn,fadeOut"
     data-slide-in-events="mousedown touchstart"
     data-fade-out-events="hover"
     data-slide-in-duration="0.2"
     data-fade-out-from='{"alpha": 0.8 }'>
</div>

Basically everything that you can do you with the jQuery methods can also be used on the elements. You just have to write everything out using that dash-casing, so no camelCasing the data attribute names. The values inside the quotes don't matter.

 

I didn't actually test everything out, but it should work just like I described.

See the Pen 13d254fc750b86d4012122400a77955e?editors=0010 by osublake (@osublake) on CodePen

 

  • Like 2
Link to comment
Share on other sites

Waaaaahhhh! Blake! Cool! :P

 

Some thoughts:

1) Portability

Yesterday I already created a bunch of hover effects mimicking hover.css, in this format:

function gsap_Anim_Skew(element, config) {
	config = config || {};
	var time = config.time != null ? config.time : 0.3;
	var animation = TweenLite.to(element, time, { skewX: -8, ease: Sine.easeOut, paused: true } );

	return {	    
		mouseenter: function() {      
			animation.play();
		},

		mouseleave: function() {
			animation.reverse();	      
		}			   
	};
}

$('.gsap-anim-skew').on( gsap_Anim_Skew( $('.gsap-anim-skew') ) );

Yet if we write it like so ...

var effects = {};
$.extend(effects, {
    skew: function() {
        var effect = {
            enter: {
                skewX: -8, 
                ease: "Sine.easeOut"
            },
            leave: {
                skewX: 0, 
                ease: "Sine.easeOut"
            },
            duration: 0.3,
            animation: "skew"
        };
        return gsFxAnimation(effect);
    } 
});

... that's better from a library point-of-view, but less portable for copy-pasting single "Add Water"-effects into website projects. If we would put in 1.000 effects into 1 big effects library, yet only 2 effects are used in a given website project, that's a whole lot of redundant code. So I'd like the effects functions to be "self-contained", copy-paste-able.

 

2) Method calls, html classes

I really like your .gsFx() method call, in stead of jQuery's .on() though, and I also like setting class names and using the ticker.

 

Structure proposal:

So how would we structure things for usage this way? How about this:
part 1) one obligatory library file for convenient-use-functionality { .gsFx() method calls, class listeners, and configuration options }

part 2) another library file, to include optionally, with a giant effects object

part 3) another library file, same as "part 2", in the form of self-contained effect functions, for "plug-n-play" use within website projects.

-Jos

Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
  • Recently Browsing   0 members

    • No registered users viewing this page.
×