Jump to content
Search Community

Get timeline that is contained within another function

Robert Wildling test
Moderator Tag

Warning: Please note

This thread was started before GSAP 3 was released. Some information, especially the syntax, may be out of date for GSAP 3. Please see the GSAP 3 migration guide and release notes for more information about how to update the code to GSAP 3's syntax. 

Recommended Posts

Hi, dear Greensockers!

 

I wonder if you could help me with a tip concerning this problem:

 

I have various functions build following this scheme:
 

function introAnim(){

  let tl = new TimelineMax()
  tl.add(someTween, 0, vars)
  tl.add(someTween2, 0.2, vars)
  [...]
  return tl
}
   
function hideIntro(){
  // first and foremost this tween has to stop, whatever could still be going on in introAnim - how to???
  let tl = new TimelineMax()
  tl.add(hideTween, 0, vars)
  tl.add(hideTween1, 0.2, vars)
  [...]
  return tl
}

 

As you can see, I find myself in a situation, where I need a hideIntro() timeline animation. This hideIntro will eventually animate over the same elements that introAnim animated (a line, some letters in reverse, e.g.). Since hideIntro() is initialized via a button click, it is very likely that introAnim is not yet finished.

(I have a feeling that I am not the only one with this problem, and maybe somebody already posted a similar question here. If so, sorry for re-posting! I did search, but to be honest - I do not know how in this case.)

Thank you!

Link to comment
Share on other sites

Is the introAnim() returning that timeline to a master? If that's just a function that fires on page load you could just create a regular timeline and give it a name. Then in your hideIntro() function you can kill() the timeline from the introAnim() function.

 

If that's not an option you always have the killTweensOf() method that you could add to the hideIntro() function. 

https://greensock.com/docs/TweenMax/static.killTweensOf()

 

Does that help at all?

 

  • Like 5
Link to comment
Share on other sites

Just to elaborate on what @PointC said, your code is already setup to get a timeline from a function. That's what a return statement does.

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/return

 

 

The onClick handler will kill the intro animation and play the hide animation. 

 

var introAnimation = introAnim();
var hideAnimation;

function introAnim(){

  return new TimelineMax()
    .to(foo, 5, { x: 500 });
}
   
function hideIntro(){
  
  return new TimelineMax()
    .to(foo, 1, { autoAlpha: 0 });
}

function onClick() {
  
  introAnimation.kill();
  introAnimation = null;
  hideAnimation = hideIntro();
}

 

  • Like 5
Link to comment
Share on other sites

1 hour ago, PointC said:

Is it just shorter or is there something I'm not aware of that makes that better? Thanks.

 

 

That's how I usually write code that can be chained together, but there's nothing special about it. It's just shorter.

 

You don't have to create a variable to start chaining timeline methods together. Think about jQuery. Most people would probably do #1 as using a variable might be redundant if you're never going to reference the button again.

 

// #1
$("#button").text("Click Me").click(onClick);

// #2
var button = $("#button");
button.text("Click Me").click(onClick);

 

 

One thing to be careful about when returning something that spans multiple lines is that there needs to be some code directly to the right of the return statement.

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/return#Automatic_Semicolon_Insertion

 

 

// GOOD
function intro() {

  return new TimelineMax()
    .to(foo, 1, { x: 100 });
}


// GOOD
function intro() {

  return (
    new TimelineMax()
  	  .to(foo, 1, { x: 100 })
  );
}


// GOOD
const intro = () => new TimelineMax()
  .to(foo, 1, { x: 100 });


// BAD
function intro() {

  return
    new TimelineMax()
  	  .to(foo, 1, { x: 100 });
}

 

 

 

 

  • Like 4
Link to comment
Share on other sites

10 hours ago, PointC said:

Is the introAnim() returning that timeline to a master? If that's just a function that fires on page load you could just create a regular timeline and give it a name. Then in your hideIntro() function you can kill() the timeline from the introAnim() function.

 

If that's not an option you always have the killTweensOf() method that you could add to the hideIntro() function. 

https://greensock.com/docs/TweenMax/static.killTweensOf()

 

Does that help at all?

 

 

Thank you for your response!

How would you address the "tl" from "introAnim" in "hideAnim.killTweensOf(???)"

Link to comment
Share on other sites

9 hours ago, OSUblake said:

Just to elaborate on what @PointC said, your code is already setup to get a timeline from a function. That's what a return statement does.

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/return

 

The onClick handler will kill the intro animation and play the hide animation. 

 


var introAnimation = introAnim();
var hideAnimation;

function introAnim(){

  return new TimelineMax()
    .to(foo, 5, { x: 500 });
}
   
function hideIntro(){
  
  return new TimelineMax()
    .to(foo, 1, { autoAlpha: 0 });
}

function onClick() {
  
  introAnimation.kill();
  introAnimation = null;
  hideAnimation = hideIntro();
}

 

 

Thanks for this idea!!!
I should have mentioned that all my timelines are defined on a Vue's "method" param, something like this:

 

<template>
  [...]
  <button @onclick="buildUI()">Enter Website</button>
  [...]
</template>

<script>
export default {
  [...],
  data() {
    return {
      $settings
    }
  },
  mounted(){
	this.$settings = UI.getSettings(Back, this)
  },
  methods: {
  	buildUI(route= '') {
      let tl = new TimelineMax({ paused: true })

      tl.addLabel('start', 0)
      // If the IntroState is set to active, dissolve the intro. 
      // If not - which is the case, when a subpage like '/about' was called directly
      // in the browser - then ignore it
      if (this.$settings.states.introActive) {
        tl.addCallback(this.toggleIntroState, 0, [false])
        tl.addCallback(this.introAnimReverse, 0)
      }
      tl.addCallback(this.toggleUIState, 0, [true])
      [...]

      // Could be hidden because:
      //    1. user clicked hide button
      //    2. we are in mobile state
      if(this.$settings.states.aside === 'hidden') {
        tl.add(this.tweenEl(this.$settings.ui.container.cContentAside, "hide"), "start+=0.2")
        tl.add(this.tweenEl(this.$settings.ui.btns.cBtnToggleAside, "hide"), "start+=0.35")
      } else if(this.$settings.states.aside === 'intro') {
        tl.add(this.tweenEl(this.$settings.ui.container.cContentAside, "show"), "start+=0.2")
        tl.add(this.tweenEl(this.$settings.ui.btns.cBtnToggleAside, "show"), "start+=0.35")
      } else {
        tl.add(this.tweenEl(this.$settings.ui.container.cContentAside, "show"), "start+=0.2")
        tl.add(this.tweenEl(this.$settings.ui.btns.cBtnToggleAside, "show"), "start+=0.35")
      }


      [...]

      tl.play()
      return tl
  },
  dissolveUI() {
  	[...]
  },
  // Helper
  tweenEl([...]){
    [...]
  },
  [...]
}

 

Being a Vue newbie, I am sure that this is probably not the best solution - any tips for improvments highly appreciated!
However: so far I couldn't find a way to assign a method on "method" to a var as you show in your example. Would it be possible at all to on Vue's "method object?

Link to comment
Share on other sites

I don't use Vue, so I'm not the best person to ask. Maybe @Dipscom can chime in and give some tips.

 

I looked at Vue's documentation, but I didn't see any mention about where to store stuff besides the data object. I could be wrong, but the data object doesn't seem like the correct place to store stuff like animations, so I'm just going to assume that it's safe to store animations directly to 'this'.

 

export default {
  
  mounted() {
    this.introAnimation = this.introAnim();
  },
  methods: {
    
    introAnim() {
      const tl = new TimelineMax();
      return tl;
    },
    hideIntro() {
      const tl = new TimelineMax();
      return tl;
    },
    onClick() {
      this.introAnimation.kill();
      this.introAnimation = null;
      this.hideAnimation = this.hideIntro();
    }
  }
}

 

 

Since you working with an object, you could also store the timeline directly inside the method instead of returning it.

 

export default {
  
  mounted() {
    this.introAnim();
  },
  methods: {
    
    introAnim() {
      this.introAnimation = new TimelineMax();
    },
    hideIntro() {
      this.hideAnimation = new TimelineMax();
    },
    onClick() {
      this.introAnimation.kill();
      this.introAnimation = null;
      this.hideIntro();
    }
  }
}

 

 

 

  • Like 5
Link to comment
Share on other sites

Looks like I was right about storing stuff on 'this'. Vue doesn't have a proper place to declare/store non-reactive stuff. 

https://github.com/vuejs/vue/issues/1988

 

 

According to that issue, you can add/initialize private stuff in the created hook. For example, if you want to create some sort of master timeline, this should work.

 

export default {
  
  created() {
    this.mainTimeline = new TimelineMax();
  },
  methods: {
    
    someAnimation() {
      const childTimeline = new TimelineMax();      
      this.mainTimeline.add(childTimeline);
    }
  }
}

 

  • Like 6
Link to comment
Share on other sites

4 hours ago, Robert Wildling said:

How would you address the "tl" from "introAnim" in "hideAnim.killTweensOf(???)"

 

I only mentioned killTweensOf() as a possibility because you said you just wanted to stop the element from animating. This method won't kill() the other timeline, but it can stop the element while it's animating. Something like this for example:

 

function introAnim() {
  var tl = new TimelineMax();
  tl.to(yourElement, 5, { x: 500 });
  return tl;
}

function hideIntro() {
  TweenMax.killTweensOf(yourElement);
  var tl = new TimelineMax();
  tl.to(yourElement, 1, { opacity: 0 });
  return tl;
}

 

But this will not kill() the introAnim timeline so it's probably not what you needed.

 

Happy tweening.

:)

 

  • Like 5
Link to comment
Share on other sites

Just to confirm what @OSUblake has already figure out:

 

The data() object in Vue is for reactive data, not to store any static values.

 

You can do exactly what he has sugested and just add any static values to the this of the component you are working with, you will be able to access it from anywhere in that component's instance.

 

However, I am assuming you are working with DOM elements, don't initialize your timelines in the created() hook. Use the mounted() as you will not have access to the DOM in the created() call.

 

Vue has some bluit-in $options property that you might want to use depending on your case. I haven't seen the real need for it and it does make it ever so longer to reach anything this.$options.customOption but it's there. I guess in a big enough project it would be handy.

 

You might even want to consider not putting your timeline methods inside the methods property. You can, depending on when you need to build your timelines just attach them to normal properties of you component's instance.

 

export default {
  
  mounted() {

    this.someAnimation = function() {
      const childTimeline = new TimelineMax();
      
      //...
      
      return childTimeline;
    }
    
    
    this.mainTimeline = new TimelineMax();

    this.mainTimline.add(this.someAnimation());

  }
  
}

 

  • Like 6
Link to comment
Share on other sites

  • 2 weeks later...

I reply quite late, but thank you for your feedback, @Dipscom

It seems that I have troubles to get the DOM at the proper moment and connect GSAP with Vue's transition system. I tried to pinpoint the problem here:

https://stackoverflow.com/questions/51970461/vue-vuex-gsap-animation-add-dom-elements-to-store
but I don't know it I am being clear here...  

Link to comment
Share on other sites

I'm scared of StackOverflow so, let's chat here and if I what I say can help you resolve your issue, I'll post the end result there.

 

I must say I think you are going about this the wrong way.

 

You should not be trying to save DOM elements into the store. Think of the store as a place to hold data or state, not elements. The component that contains the element you want to target is the one that should be doing the trigerring of events or responding to things.

 

One issue you will encounter if you keep adding DOM elements to the store is that you now have to manage their existence in the store as well as in the DOM. Say you have a component you add to the DOM. You then, have it commit itself to the store. If you ever remove the element from the DOM, you will need to make sure it is also removed from the store.

 

Why is it that you feel you need to add the element to the store? I think a better solution is to pass down whatever animation you want that component to perform instead.

 

If you give us a more concrete example of what you are trying to achieve, I am sure we can work something out.

 

 

  • Like 3
Link to comment
Share on other sites

Haha...scared of stackoverflow! Exactly my feelings! :-)
Thank you for your honest feedback, @Dipscom!

 

Just to understand you correctly: This is exactly what I need! When you say:

"You should be trying to save DOM elements into the store."
...then you mean "...should NOT be trying ...", right?

 

I gladly will put together my example app and post it somewhere useful - I am thinking of codeSandbox instead of CodePen. Would that be ok?
 

  • Like 1
Link to comment
Share on other sites

58 minutes ago, Robert Wildling said:

...then you mean "...should NOT be trying ...", right?

 

Correct. Apologies as I am still a tad sleep deprived from IRL issues.

 

I've amended the original post to reflect that lest others will be misled.

 

CodeSandbox is absolutely fine. Just put in the bare minimum to illustrate your issue.

  • Like 2
Link to comment
Share on other sites

I am struggling hard to prepare an example that boils down the problem to a few lines of code. So I thought present my problems step by step and hope that this is ok with you! If not, just let me know!

 

This would be my first step:


At the core of my problem seems to be this idea of having a "settings" object: a Javascript object, that pre-defines parameters for each object that will be animated by GSAP. The structure is like this:
 

let SETTINGS = {
  states: {
    aside: 'hidden', // 'hidden', 'intro', 'visible' - for responsive states
    domBuilt: null,
    firstLoadedFromBaseURL: null, // page called via '/' or '/somepath'?
    introActive: null, // show intro or site-navigation-UI?
    mainNav: 'hidden' // @see 'aside'
  },
  ui: {
    btns: {
      toggleAsideBtn: {
        el: document.querySelector('.c-btn__toggle--aside'),
        tweenParamsTo: {
          hide: {
            autoAlpha: 1,
            right: 0
          },
          intro: {
            autoAlpha: 0,
            right: "-47px"
          },
          show: {
            autoAlpha: 1,
            right: "270px",
          }
        }
      }
    },
    container: {
      rect01: {
        el: document.querySelector('.rect01'),
        tweenParamsTo: {
          hide: {
            autoAlpha: 0,
          },
          // Container have no "intro" state in this case, except one: the intro container
          show: {
            autoAlpha: 1,
          }
        }
      },

[...]

 

My thinking behind doing this is as follows:

 

1. once a DOM element is saved, the access to it is quicker;
2. there is a central storage, where I can manipulate values, so I do not have to dig through various files to make changes;
3. eventually, this could come from a CMS, where a user can configure values;

4. it is a good place to store "app state" parameters.

But of course it causes troubles - at least for me:

1. the initial setup does not allow to define callback functions (see codePen)
2. also the initial setup does not allow to refer to another setting in this SETTINGS object (could be the case in an 'onComplete' callback, which could then fire another animation...)

3. initial "app state" needs extra checks, e.g. via the URL path (is the app loaded from the baseURL – which would imply to build the "Intro" – or is there a path – which would imply to build the "UI"?)

If I would like to define callbacks in a SETTINGS tree, there has to a second step in the setup routine, that adds missing features, probably like this:
 

let SETTINGS = {...}

SETTINGS.ui.btns.toggleAsideBtn.tweenParamsTo.hide.onStart = cb_onStart;
SETTINGS.ui.btns.toggleAsideBtn.tweenParamsTo.hide.onComplete = cb_onComplete;
SETTINGS.ui.btns.toggleAsideBtn.tweenParamsTo.hide.onCompleteParams =  {
  msg: "called from toggleAsideBtn hide"
};


Therefore my first question would be:
Is this at all a good/useful/"smart" approach or is this idea a source for troubles a-la-long?

A Codepen is here:

See the Pen QVjPYJ by rowild (@rowild) on CodePen

 

Link to comment
Share on other sites

If I may say something, please do not take this personally, but when I have issues breaking down the problem into a smaller problem it means I am going down the wrong path... You really should be able to slice whatever you are trying to achieve into ever smaller pieces until the problem that piece represents becomes trivial.

 

I am having a bit of a hard time visualising your scenario but still, it really feels like you are trying to swim again the river here.

 

As far as I understand the code is going to run in Vue, and thus, I am thinking along the lines of how to set this up with Vue as the framework.

 

Firstly, even with a Store, you wouldn't define the DOM element to target there. If anything, you should make the component inform the store it is the chosen one.

 

Looking at your example pen, I see several functions that all do the very same thing but you use different references to your main store. All those functions could be a single one that takes the three parameters that change.

 

// So instead of have a bunch of functions like this
function hideRect01() {
  console.log("hideRect01 called")
  return TweenMax.to(
    SETTINGS.ui.container.rect01.el, 
    SETTINGS.gsapGlobals.duration, 
    SETTINGS.ui.container.rect01.tweenParamsTo.hide
  )
}

// You only really need
function createTween(target, dur, vars) {
  console.log("hideRect01 called")
  return TweenMax.to(
    target, 
    dur, 
    vars
  )
}

// that you would call from somewhere
createTween(
  SETTINGS.ui.container.rect01.el,
  SETTINGS.gsapGlobals.duration,
  SETTINGS.ui.container.rect01.tweenParamsTo.hide
);

 

What exactly is this customization that you need to have? Maybe if we can understand the job it has to do, we can figure out a nice way to do it.

 

  • Like 4
Link to comment
Share on other sites

@Dipscom I do take it personally - and with great gratitude! Please keep saying straight out, what's wrong and stick with me for a little bit longer please! Your help and your critique are INCREDIBLY valuable to me!!!

 

As for the "multiple functions doing the same": that was actually just quickly thrown in. I actually to have such a function, but I wanted to discuss that in the next step, for which I will prepare the next pen today.

But the main intend of my last post was to find out, if having such a SETTINGS object "the way to go" (no matter whether with "pure" GSAP or within Vue). I have a feeling that it is kind of ok as an idea, but it is not really nailing the problem. The fact that callback functions need to be added in a second "settings setup step" makes me insecure.

So I wonder if you could elaborate on that a bit? Do you use such an object? Or are there any other ways to handle such a an idea?

Meanwhile I prepare that pen I mentioned. Thank so much!!!

Link to comment
Share on other sites

I would say you are mixing settings and behaviour where you should not.

 

The idea of a settings object is to hold references to a bunch of, well, settings... Those references can be of methods, I don't see an issue there. But, you shouldn't try to perform actions from the setting object.

 

By adding calls to DOM methods on your settings object, it makes it completely dependant on the DOM being available at the time of your settings object being initialised. That's not something you want. You want the setting to be on its own and only when the DOM is ready, you store the references to the DOM elements on your settings.

 

E.g.

let SETTINGS = {
  ui: {
    el: undefined,
    ref: '.my-button',
    dur: 1,
    vars: {
      x: 100,
      autoAlpha: 0
    }
  }
};


// Then at your initialization code:
SETTINGS.ui.el = document.querySelector( SETTINGS.ul.ref );

 

  • Like 4
Link to comment
Share on other sites

That's a neat trick using a ref attr :-) Thanks so much!

 

I think I managed to strip down my problem to something quite simple (still a lot of code, but to me it seems it is necessary...):
Vue and GSAP

 

See the Pen aavXxV by rowild (@rowild) on CodePen


During Vue's beforeAppear hook, the "content component" is configured, which starts in "appear" . 

The animation reaches a point, where a button has to be clicked to enter the website. "appear" is not yet finished, because "done" is not sent. "done" should be sent only, when the button is clicked, which plays the introAnim in reverse.

Here my problems:
1. first of all "reverse" does not work... and I cannot figure out, why...
2. the "Enter Page" button is inside the component, which means that the event has to be emitted to the parent. That works. But how do I "transport" that "done()" function that "appear" needs to finish its "appear" hook and move on to "appearAfter" and "beforeEnter"? Also, I should change the router path at this time...

 

I assume this will be a special problem, when it comes to routes and dynamicRoutes. But I'd like to keep that problem for a later question... 

Thank you very much!

 

Link to comment
Share on other sites

That is indeed a phat chunk of code... It does seem to me a bit too much still. However, it does help a ton to understand where you are trying to get to. Thanks for that.

 

1. The reverse isn't working because you are calling the function that creates the timeline rather than telling the timeline originally created to play in reverse.

2. You send the done() down as a callback.

 

Have a lookie at this fork. It accomplishes what you are describing as your intention. See if it helps you.

 

See the Pen YOqLXo?editors=0010 by dipscom (@dipscom) on CodePen

 

  • Like 4
Link to comment
Share on other sites

Wow! This is really quite an insight! Awesome!!!

 

So my takeaway from this is:

1. Create the timeline on the current Vue component using "this" (at least when it should be callable by other functions, too);

2 Save 'done()' right away, when the timeline is created and don't bother with it, when calling the reverse.

 

I have a lot to work with now!

Muchas gracias, Pedro! Me alegre mucho!

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.
×
×
  • Create New...