Marten Zander
Marten Zander

Reputation: 2565

GSAP: Managing multiple tweens and timelines at once

first things first, I am new to GSAP, so please bear with me. I try to give my best to describe what I am struggling with.

In order to animate an SVG file I started diving into GSAP. I created several Tweens for different elements within my SVG file. Everything worked fine and ist animating as expected.

Since I have a lot of elements to animate within my SVG, I started adding TimelineLite to have more control about the whole thing.

At this point, my script file looked something like this:

First I declared all the elements I'd like to animate:

this.phone = document.querySelector('#gsap-phone-svg');
this.body = document.querySelectorAll('.gsap-phone-body');
this.body.shadow = this.phone.querySelectorAll('.gsap-phone-body-shadow');
this.body.mask = this.phone.querySelectorAll('.gsap-phone-body-mask');
this.layer = this.phone.querySelectorAll('.gsap-phone-layer');
this.screen = this.phone.querySelectorAll('.gsap-phone-screen');
this.screen.clipPath = this.phone.querySelectorAll('.gsap-phone-screen-clipPath');
.
.
.
// many more following here

Than I created an Object to save my Tweens in:

const tweens = {};

// creating tweens

tweens.body = TweenMax.to(this.body, this.animDur/2,{
y: this.maxExpand/6*-1,
ease: this.ease
});

.
.
.

// many more following here

At the end I added all my tweens to an temp array which I passed into a new TimelineLite() afterwards like so:

const tl = new TimelineLite();
tl.add(tweensArray, 0, "start", 0.05);

Seems logic so far, I guess... Now here is the crux. You may have noticed or not that I have like more than 20 elements or so to animate. Thats why adding a tween for every element on its own becomes very confusing. Also I want the whole main timeline to be repetitive. The problem here is, that I want different easing for all my tweens on the "in-animation" than on the "out-animation", as well as I want to have no stagger on the "out-animation".

All these little cruxes made me think about an alternative solution to manage the creation of my tweens and timelines.

The most handy solution that came in my mind was to store all the information about my anim elements and timelines within an object:

const animation = {
   settings : {
      duration: 1.5,
      expansion: 1,
      easeIn: Elastic.easeOut.config(1, 0.5),
      easeOut: Power2.easeInOut
   },
   timelines : {
      main : {
         delay : 0,
         paused : true,
         align : 'start',
         stagger : 0.05,
      },
      test : {
         delay: 0,
         paused : true,
         align : 'start',
         stagger : 0.5
      }
   },
   items : {
      phone : {
         id : '#gsap-phone-svg',
         start : { },
         end : { },
         timeline : 'test',
      },
      body : {
         class : '.gsap-phone-body',
         start : {
            y : 0,
         },
         end : {
            y : -21,
         },
         timeline : 'test',
      },
      layer : {
         class : '.gsap-phone-layer',
         start : {
            y : 0,
         },
         end : {
            y : -62.5,
         },
         timeline : 'main',
      },
      radar : {
         class : '.gsap-phone-radar',
         start : {
            y : 0,
         },
         end : {
            y : -25,
         },
         timeline : 'main',
      },
      radarBase : {
         class : '.gsap-phone-radar-base',
         start: {
            y : 0,
         },
         end : {
            y: -16,
         },
         timeline : 'test',
      },
      ringOne : {
         class : '.gsap-phone-radar-ring-1',
         start : {
            y : 0,
         },
         end : {
            y: -25,
         },
         timeline : 'test',
      },
      ringTwo : {
         class : '.gsap-phone-radar-ring-2',
         start : {
            y : 0,
         },
         end : {
            y: -41,
         },
         timeline : 'main',
      },
      ringThree : {
         class : '.gsap-phone-radar-ring-3',
         start : {
            y : 0,
         },
         end : {
            y: -62.5,
         },
         timeline : 'main',
      },
      cancel : {
         class : '.gsap-phone-cancel',
         start : {
            y : 0,
         },
         end : {
            y: -50,
         },
         timeline : 'main',
      },
      submit : {
         class : '.gsap-phone-submit',
         start : {
            y : 0,
         },
         end : {
            y: -100,
         },
         timeline : 'main',
      }
   }
};

Than I wrote this "createTweens" method to return the GSAP Tweens

/* create tweens */
function createTweens(anim){
   const el = anim.items;
   const settings = anim.settings;
   const duration = settings.duration;
   const easeIn = settings.easeIn;
   const easeOut = settings.easeOut;
   const tweensIn = [];
   const tweensOut = [];
   let tempTween = null;

   for (const key in el){
      const curEl = el[key];
      const selector = curEl.class || el[key].id;
      const startPoint = curEl.start || '';
      const endPoint = curEl.end || '';
      const timeline = curEl.timeline || '';
      const nodes = document.querySelectorAll(selector);

      nodes.forEach(object => {
         tweensIn.push(getTween(object, endPoint, duration, easeIn, `${timeline}-in`));
         tweensOut.push(getTween(object, startPoint, duration, easeOut, `${timeline}-out`));
      });
   }

   function getTween(tw, twValues, twDur, twEase, tl){
      const vars = twValues;
      vars.paused = false;
      vars.ease = twEase;
      tempTween = TweenMax.to(tw, twDur/2, vars);
      tempTween.data = {
         timelineName : tl
      };
      return tempTween;
   }

   return tweensIn.concat(tweensOut);
}

and another function to return the timelines:

/* create timelines */
function createTimelines(anim, tweens){
   const el = anim.timelines;
   const timelines = {};
   // timelines.mainIn = new TimelineLite();
   // timelines.mainOut = new TimelineLite();
   const tweensForTimelines = {};

   for(const key in el){
      const delay = el[key].delay;
      const paused = el[key].paused;
      const align = el[key].align;
      const stagger = el[key].stagger;
      const vars = {};
      vars.paused = paused;

      timelines[`${key}-in`] = new TimelineLite(vars);
      timelines[`${key}-in`].delay = delay;
      timelines[`${key}-in`].align = align;
      timelines[`${key}-in`].stagger = stagger;

      timelines[`${key}-out`] = new TimelineLite(vars);
      timelines[`${key}-out`].delay = delay;
      timelines[`${key}-out`].align = align;
      timelines[`${key}-out`].stagger = stagger;

      tweensForTimelines[`${key}-in`] = [];
      tweensForTimelines[`${key}-out`] = [];
   }

   if(Object.keys(tweensForTimelines).length !== 0){
      for(let i = 0; i < tweens.length; i++){
         const curTween = tweens[i];
         const tlTarget = curTween.data.timelineName;
         tweensForTimelines[tlTarget].push(curTween);
      }
   }


   for(const key in timelines){
      try{
         timelines[key].add(tweensForTimelines[key], timelines[key].delay, timelines[key].align, timelines[key].stagger);
         console.log(TweenMax.getTweensOf(timelines[key]));
         timelines[key].data = tweensForTimelines[key];
      } catch(e){

      }
   }

   return timelines;
}

If I execute the following code than, it would play my "main-in" timeline.

const tweens = createTweens(animation);
const timelines = createTimelines(animation, tweens);
timelines['main-in'].play();

So far, this is actually working. But if I try to add the "main-in" timeline to a new timeline, its not working anymore.

const anotherTimeline = new TimelineLite();
anotherTimeline.add(timelines['main-in']);
anotherTimeline.play();

In order to debug this, I tried

TweenMax.getTweensOf(anotherTimeline);

but all this returns is an empty Array. Then I logged the same for my "main-in" timeline:

console.log(TweenMax.getTweensOf(timelines['main-in'])); 

also returning an empty Array which is very confusing to me, since even though this timeline seems to be empty, it plays my "in-animation" on:

timelines['main-in'].play()

I am really stuck here and would really appreciate some help from the more advanced users or simply anyone who has an idea for this. I hope you guys were able to follow me ... in case not, have a look at the codepen provided..

UPDATE: Click for Codepen DEMO

Thanks in advance!

Upvotes: 0

Views: 4032

Answers (1)

Jack
Jack

Reputation: 2970

I didn't have time to parse through all your code and craft a full replacement, but it did strike me as a tad over-engineered, but I also realize that my brain may just work differently and it's a stylistic choice (not good-or-bad).

What I find most intuitive, readable and flexible is an approach where you break your animation down into chunks that you put in functions that each spit back a TimelineLite/Max or TweenLite/Max that you can nest in a master timeline (if you so choose).

Kinda like:

function phoneIntro() {
    var tl = new TimelineLite();
    tl.to(...)
      .to(...);
    return tl;
}

function flipPhone() {
    var tl = new TimelineLite();
    tl.to(...);
    return tl;
}

var master = new TimelineMax({repeat:-1});
master.add(phoneIntro(), 0)
      .add(flipPhone(), "-=1"); //overlap by 1 second
      ...

And of course if you've got lots of elements that you're doing the same kinds of animations to, this modularized approach is very useful as well because you can feed in whatever variables you need to functions that do the work for you and spit back animations.

function buildStep(element, duration, x, y) {
    return TweenMax.to(element, duration, {x:x, y:y, rotation:30});
}

Hopefully it's becoming clear how flexible things can be when you create some modularized functions that you just feed whatever you need. This whole approach can make it much faster to edit your animations too (and experiment with timing, etc.) because it's so simple to find your place in the code. "I want to make the intro 2 seconds longer..." just find the phoneIntro() function and tweak stuff inside there. Done. Since you strung things together with relative timing in a master TimelineMax, the changes that you make to that first modularized chunk automatically push back the timing of subsequent things and it just flows through nicely. No messing with 20 different delays.

Also, the reason TweenLite.getTweensOf(anotherTimeline) returned an empty array is because that method finds the tweens of that object. Like, literally, if you were tweening the timeline itself (perhaps its progress), it would return that tween. It sounds like you assumed it gets the tweens INSIDE of some timeline instance (it doesn't). If that's what you're after, though, it's as easy as anotherTimeline.getChildren().

I hope that helps at least a little. Feel free to post questions in the forums at http://greensock.com/forums where there's a great community of GSAP professionals. It's a fabulous place to learn, even if you never post a question :)

Happy tweening!

Upvotes: 2

Related Questions