mulegoat

How to toggle tweens in a DRY fashion

Recommended Posts

Hi there,

 

I'm trying to build a UI with different components using tweenlite to add/remove items from the viewport.

 

As I add more elements I'm finding that alot of code is getting repeated in my functions and although the animations applied to each element are slightly different, the principal behind them is the same, i.e. Add / Remove a class to an element with a click event and tween the element accordingly. E.g.

        (function openMainMenu() {

            var $openMenu         =   $('.openMenu'),
                $menuContainer    =   $('#jsMainMenu');

            $openMenu.click(function(e) {
                e.preventDefault();
                  $(".openMenu").addClass("is--hidden");
                  $(".openMenu").removeClass("is--active");
                  $(".closeMenu").addClass("is--active");
                  $(".closeMenu").removeClass("is--hidden");
                  // slide menu in
                  TweenLite.to($menuContainer, 0.2, {position:"fixed", zIndex:"998", display:"block", delay:0.1, right:"0%", bottom:"0%", ease:Power2.easeOut});
                  console.log("Menu Inview");
            });

        })();


        (function closeMainMenu() {

            var $closeMenu        =   $('.closeMenu'),
                $menuContainer    =   $('#jsMainMenu');

            $closeMenu.click(function(e) {
                e.preventDefault();
                  $(".openMenu").addClass("is--active");
                  $(".openMenu").removeClass("is--hidden");
                  $(".closeMenu").addClass("is--hidden");
                  $(".closeMenu").removeClass("is--active");
                  // slide menu out
                  TweenLite.to($menuContainer, 0.2, {display:"none", zIndex:"0", delay:0.1, right:"-100%", ease:Power2.easeOut});
                  console.log("Menu Hidden");
            });

        })();

So clicking the button with class 'openMenu' hides the button, displays the close button and tweens the menu container into view. If you check the codepen demo you'll see a very similar pair of functions for a search container, and I'd also like to add an off-canvas contact form, search bar etc...

 

 

What I'd like to know is how best to restructure my code so that 1. large portions of code to not get repeated, and, more importantly, 2. How to chain events so that if one element is active e.g. Main Menu, clicking outside of it or clicking Search Tours or another button activates the 'close' function on the main menu as well as opening search tours - or whatever else I add to the UI

 

Many thanks in advance for any help 

Share this post


Link to post
Share on other sites

OSUblake, I love that demo, because I was trying to figure out how to really use the reverse/reversed methods... I have a question that might help me understand it better. What exactly does the "reverse()" at the end of this tween do ? It doesn't look like the regular GSAP syntax the way it hangs off of the end of the tween.

 var tween = TweenLite.to(element, 2, { x: 400, backgroundColor: "#673AB7" }).reverse();

Share this post


Link to post
Share on other sites

The reverse on the end might look weird, but it's regular GSAP syntax. It could also be written like this...

var tween = TweenLite.to(element, 2, { x:400, backgroundColor: "#673AB7" });

tween.reverse();

So here's is how that animation works. After you create a tween, immediately reverse it. This will put the tween in it's final reversed state, eliminating the need to pause it. It's kind of like if you had your back against the wall and then turned around. Now you can't go anywhere because the wall is in your way, so it looks like you are paused. Now you don't need to figure out if your animation is paused, and what direction it should be playing. 

 

To get this to work, I created all the animations in a loop using jQuery's map method, which returns the following function for each animation.

// This is shorthand
return function(target) {
  var reversed = element !== target ? true : !tween.reversed();
  tween.reversed(reversed);
}

// It could be written like this
return function(target) {

  var reversed;
  if (element !== target) {
    reversed = true;
  } else {
    reversed = !tween.reversed();
  }
  tween.reversed(reversed);
}

To toggle the animations, I loop through those functions, passing in the element (event.target) that was clicked. If the element that the function belongs to is not equal to the target, that means it wasn't clicked, so we'll just reverse it and send it back to it's starting position. If the target and element are the same, we'll just toggle it's reversed state by passing in the opposite of what it is right now.

  • Like 6

Share this post


Link to post
Share on other sites

OSUblake, I'm interested in this bit here:

// Cycle through list of animations, toggling reversed state
  animations.each(function(i, animate) {
    animate(event.target);
  });

I see you wrote using Babel and I am guessing this bit is just a newer way of declaring a function and calling it immediately. Is that right?

Share this post


Link to post
Share on other sites

I have Babel set as a default, but I didn't use any new JS features. 

 

So what don't you get in that code? Where the names are coming from, or where the function is coming from?

 

I used jQuery's map method as the loop to create the animations, which stores whatever you return to it in jQuery's version of an array. Notice how in the createAnimation function I'm returning a function. That's what's being stored in the animations variable. Just a bunch of functions that were created every time I called createAnimation.

 

And since it's stored in a jQuery object, you can use jQuery's each method to loop through those functions. So no, I'm not creating some function on-demand. It's what was returned during the map calls. As for the names, they can be whatever you want them to be because it's a callback. So this is perfectly fine.

animations.each(function(dipscom, likesTurtles) {
  likesTurtles(event.target);
});

jQuery's each and map method are similar to an array's forEach and map method, but slower and a little backwards. So here's a little code using arrays instead of jQuery. Can you understand what's going on here? It's pretty much how I have the animation setup. Run it and you'll set it log out their names and ages.

var names = ["John", "Sally", "George"];
var ages  = [19, 49, 32];

var persons = names.map(function(name, i) {
  
  return {
    name: name,
    age:  ages[i],
    info: function() {
      console.log(name + " is " + this.age + "yrs old")
    }
  }
});

persons.forEach(function(person, i) {
  person.info();
});
  • Like 3

Share this post


Link to post
Share on other sites

What I did not get was where the `animate` was coming from. Now I kind of understand but my head hurts when I start to think I actually get it. Which, is good because it means I am learning something I don't know.

 

Going back to the Greensock side of this, then.

 

Does it mean that on each click, the code loops thru every single one of the tweens, only the one that was clicked plays? Like, every tween is triggered but we only see one move because the others do not get reversed?

Share this post


Link to post
Share on other sites

Blake, thanks for going the extra mile in explaining the technique. Very cool and helpful!

  • Like 1

Share this post


Link to post
Share on other sites

As far as the naming thing goes, just think of minified file. Order matters, but the names you choose doesn't unless you are doing something like dependency injection. 

// Original
function foo(these, are, my, params) {}

// Minified
function foo(a,b,c,d) {}

The naming convention I use is to give collections, like an array, a plural name. In a loop I give the object being referenced a singular name. So boxes => box, persons => person, cars => car

 

 

Does it mean that on each click, the code loops thru every single one of the tweens, only the one that was clicked plays? Like, every tween is triggered but we only see one move because the others do not get reversed?

 

Sort of. Only one animation can play forward, but other animations can still play reversed. On a click it loops through the list, and sets every tween's reversed state to true except for the one that was clicked. The one that was clicked, it just tells it to go in the opposite direction it was going, so it could play forward or reversed.

 

Here's another version of that code as an accordion menu. Maybe that will make it easier to see what is going on.

 

http://codepen.io/osublake/pen/JYQqZr/

  • Like 4

Share this post


Link to post
Share on other sites

I see. Very nice little trick. 

 

You can be sure it shall be used in the first opportunity. :D

Share this post


Link to post
Share on other sites

Wow. I go away for a couple of days and come back to this! Thanks a bunch Blake and co for helping me understand all this a bit better. I'll have a play and come back if there's anything else that comes up. One thing I'm still wondering though is what about a use case where the animations need to be different? Can the same / similar technique be used where the current animation just gets reversed. Also, and this is really crucial, how do you kill or reverse tweens that are active when the user clicks content outside of the tweened content area? Like i said, I'm going to play around with this and post back later but if anyone has any thoughts on this that would be awesome.

 

Cheers again

Share this post


Link to post
Share on other sites

The type of animations doesn't matter. I just made them all the same to simplify it. We only care about an animations reversed state based on a certain condition.

 

Here's another version that includes multiple conditions. Instead of returning a function this time, I'm returning an object with different functions to call based on the condition to use. So I added the ability to toggle even/odd animations, and if you click outside the box, all the animations will reverse and go back to their starting position.

 

http://codepen.io/osublake/pen/bVXLYz/

  • Like 1

Share this post


Link to post
Share on other sites

Hi Blake

 

Many thanks for the time and effort here. All examples given are extremely helpful and well documented, but i'm having difficulty implementing this method with my project because the click handler is attached to object being tweened. Would it be possible to edit your last example to illustrate how to apply the reverse method to target a specific box where the click handler is not attached to 'this' box? IE Button 1 tweens box 1, Button 2 tweens box 2 (reverses/resets other boxes)?

 

Understand if it's too much to ask, and am super grateful for the effort you've made. I've leanred alot with the examples you provided but I can't for the life of me figure out how to use map(createAnimation) to an individual box.

 

Many thanks again!

Share this post


Link to post
Share on other sites

Hi Blake

 

This works perfectly! Thank you very much for your help. One thing I'm trying to do is apply the click event to a class name rather than button?

 

I've tried 

var buttons  = document.getElementsByClassName(".js-button");

and then 

buttons.click(".js-button", toggleAnimations);

It's no biggie though as I can just use it in a button. Was just thinking of a scenario where i might want to call the same function in a link within a text field. 

 

In any case, thanks again for all the help

Share this post


Link to post
Share on other sites

I was using jQuery to get the elements and handle the clicks, so the code you have above wouldn't work. The string I placed before the function gets passed as event.data. I then used that string to call a particular function. Whatever function it called ran some type of condition to check against. It could be anything, and I showed several different ways, but of course it's going to vary based on what you're doing.

 

Here's a simplified version of the pattern I used. I'm passing in "bravo" as the event data, and then its going to call the obj.bravo function.

$("button").click("bravo", function(event) {
  
  var obj = {
    alpha: function() {
      console.log("alpha");
    },
    bravo: function() {
      console.log("bravo");
    }    
  };
  
  obj[event.data]();  
});

http://codepen.io/osublake/pen/40511d568471341e0ec2404e6d3fc43c?editors=001

Share this post


Link to post
Share on other sites
On 11/26/2015 at 3:18 PM, OSUblake said:

Study this technique. An animation is created only once, not on every click. Instead, on a click you just change the reversed state of an animation. There's some logic in place so only the target element will play forward.

 

http://codepen.io/osublake/pen/wKLmzK/

Hey everyone! So I realize this thread is really old, but I was messing around with Blake's implementation of the jQuery / TweenLite animation toggle last night and ended up rewriting it in vanilla JS in case anyone's interested in using that instead of jQuery for whatever reason: 

 

 

  • Like 2
  • Thanks 1

Share this post


Link to post
Share on other sites

Nice job @maxxheth

 

This was my way of giving of everyone a soft introduction to creating animations in a more object oriented way using jQuery. If you want to take it a step further, the pattern I'm using could easily be adapted for ES6 classes.

 

 

  • Like 3

Share this post


Link to post
Share on other sites

@OSUblake Is there a way to toggle items individually without reversing other elements that have already been triggered?

Share this post


Link to post
Share on other sites

Actually, I managed to figure it out. I tried working it out in the code itself, but the easiest way is to just to target each element through its own class or ID and and run it through each function individually. Since each element will reside in its own array, the reverse / toggle logic will only apply to the element inside that array.

 

Maybe there's a better way (I'm always open to suggestions!), but this will work for my purposes. =) 

 

 

  • Like 1

Share this post


Link to post
Share on other sites
10 hours ago, maxxheth said:

@OSUblake Is there a way to toggle items individually without reversing other elements that have already been triggered?

 

I'd just let each element handle the click individually instead of looping through the entire set.

 

 

And who needs loops when you can when you can emit events. I do that a lot in games.

 

 

  • Like 3
  • Thanks 1

Share this post


Link to post
Share on other sites

The only thing I would have done differently is to handle the click inside the createAnimation function.

 

const createAnimation = (element) => {
  let tween = TweenLite.to(element, 1, {
    width: 400,
    backgroundColor: "#673AB7",
    borderRadius: 0,
    ease: Elastic.easeOut
  }).reverse();

  element.addEventListener("click", () => tween.reversed(!tween.reversed()));
}

 

  • Like 3
  • Thanks 1

Share this post


Link to post
Share on other sites
17 hours ago, OSUblake said:

 

Thanks Blake! That's pretty handy! I like how you used the event emitter plugin and data attributes to toggle the tween for the whole set. 

 

Yeah, not looping when you don't have to is def ideal.

 

17 hours ago, OSUblake said:

 

I'd just let each element handle the click individually instead of looping through the entire set.

 

 

And who needs loops when you can when you can emit events. I do that a lot in games.

 

 

 

17 hours ago, OSUblake said:

I'd just let each element handle the click individually instead of looping through the entire set.

 

 

And who needs loops when you can when you can emit events. I do that a lot in games.

 

 

 

Share this post


Link to post
Share on other sites
17 hours ago, OSUblake said:

 

 

The only thing I would have done differently is to handle the click inside the createAnimation function.

 


const createAnimation = (element) => {
  let tween = TweenLite.to(element, 1, {
    width: 400,
    backgroundColor: "#673AB7",
    borderRadius: 0,
    ease: Elastic.easeOut
  }).reverse();

  element.addEventListener("click", () => tween.reversed(!tween.reversed()));
}

 

 

Yeah, fair enough! I guess I just wanted to be able to switch back and forth between being able to target single elements and multiple elements on a whim, but your event emitter / data attribute solution works perfectly for that as well.

Share this post


Link to post
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.