Jump to content
Search Community

play, reverse, restart a child timeline

fcdobbs test
Moderator Tag

Recommended Posts

Hi,

I'm trying to play, reverse, and restart a child timeline from a dynamically generated parent timeline.  There's something I'm not understanding about where the child timeline playhead is when progress is either 1 or 0.

Here's a simplified example.

The child timeline dynamically creates a random number of elements.

When the parent timeline is generated, I'd like to add the child timeline and play it from the beginning if the progress is 0, and I'd like to run the child timeline in reverse at an increased timescale if progress is 1, and make no change to the child timeline if progress is between 0 and 1.

The animation isn't running on reverse or on restart.

 

Any insight would be appreciated.  Thanks!

See the Pen mdzNLOa?editors=0011 by fcdobbs (@fcdobbs) on CodePen

Link to comment
Share on other sites

Hey there,

 

I've looked this over for a while now and I've got to say I'm pretty baffled.

 

Could you maybe explain your goal a little more clearly? I think there's likely a different solution here and we may be better off focusing on that rather than trying to fix or understand what's happening here.

 

Link to comment
Share on other sites

I don't have time to dig into this fully right now, but this certainly looks like a problem to me: 

let t1 = gsap.timeline();
t1 = gsap.timeline({                      
   ...,
   onStartParams: [t1],                                         
});

You are passing a completely different timeline inside the onStartParams. You're creating TWO timelines here. The first one is totally empty, never gets anything added to it, and that's the one you're passing to the onStart. I wonder if you were assuming that the 2nd one was being passed in. JavaScript does not work that way - you're literally in the middle of creating that instance and it hasn't been instantiated yet when you're trying to reference that in one of the properties. 

Link to comment
Share on other sites

Thanks, guys.

I have several animations that I'd like to run on the background of my page.  I'd like to add them to a main timeline so that I can control the overlap of the animations, pause the background animation on user interaction, and resume it after a hiatus in user interaction.

My idea was to store references to the child timelines in an array as they are created and to have a function refresh the main timeline onComplete with the child timelines referenced in the array.

The master timeline appears to play the child timelines with the current targets and properties, but the durations stay the same as the first iteration.  I've logged out the durations for the child timelines from each function to demonstrate.

I've experimented with invalidate.  The durations of the child timelines as referenced by the variable don't update when the targets and properties update.

Maybe if I can sync the current durations with the current targets and properties, then reverse and restart will work as expected for the child timelines.

I hope the attached demo will make my goal more comprehenisble.  It includes a child timeline with an onComplete, a child timeline with an onReverseComplete, and a master timeline that refreshes onComplete.

Thanks for your help!

See the Pen JjePbom?editors=0011 by fcdobbs (@fcdobbs) on CodePen

Link to comment
Share on other sites

I'm super short on time, but I'll mention a few quick things: 

  1. You're still making that same mistake I mentioned earlier, but now you're doing it twice (t1 and t2).
  2. This looks very odd to me: 
    animation.timeScale(4).reverse(0, false)

    Maybe you meant to just do animation.progress(1).timeScale(-4)? 

  3. I don't really understand at this point why you're nesting things like this, especially since you keep clearing the whole timeline and replacing the contents, but keep in mind that by default timelines have smoothChildTiming set to false, so children don't shift around when you do things like altering their timeScale, reversing, etc. Typically that's the behavior people want, but it looks to me like maybe you're trying to do things that would necessitate altering the children's startTime (that's what smoothChildTiming does). Maybe you should set smoothChildTiming: true on your parent timeline. See the docs: https://greensock.com/docs/v3/GSAP/Timeline

  4. I find it extremely difficult to follow your code and understand what you're trying to do (and why). Maybe it's because it's very late at night and my brain is shutting down, but you'll GREATLY increase your chances of getting a good answer here if you simplify things quite a bit more. You really shouldn't need all that code to illustrate the core issue. Maybe just use one timeline, one child, and say "after the first iteration, I expected the duration to log out as 2 but it's actually 3...why?" (just as an example)
Link to comment
Share on other sites

Heya!

 

Just dropping this here

https://xyproblem.info/

 

Both Jack and I are pretty baffled by your solution, so maybe it's best to just focus on what you're trying to acheive. Rather than digging into the issues with your solution.


Is this your only goal? If so there are much easier ways to accomplish this.

Quote

I'd like to add them to a main timeline so that I can control the overlap of the animations, pause the background animation on user interaction, and resume it after a hiatus in user interaction.


Let's start with adding children to a master timeline - this way allows you to control the overlap of animations, you'll also be able to control the main timeline on interaction.
 

function childOne() {
  const tl = gsap.timeline();
  ...
  return tl;
}

function childTwo() {
  const tl = gsap.timeline();
  ...
  return tl;
}

const main = gsap.timeline();
main.add(childOne(), 0.5);
main.add(childTwo(), "+=3");

 

One question -

If you tried this approach and ran into a limitation or something you couldn't achieve, what was the limitation? 


 

  • Like 2
Link to comment
Share on other sites

Thanks, guys.

 

Here I've eliminated the child timeline with an onComplete eventcallback.  Hopefully, if I get the main timeline working for a child timeline with an onReverseComplete callback, it will work for both.  I included both in the example because it's important that both types work, and it's important that the child timelines are only added to the main timeline after they have been created.  The order in which the child timelines are created and whether or not they are created at all is unknown at start time.

See the Pen GRwKGVN?editors=0011 by fcdobbs (@fcdobbs) on CodePen

 

I've changed

animation.timeScale(4).reverse(0, false)

to :

animation.progress(1).timeScale(-4)

 

and set:

smoothChildTiming:true

 

It looks to me like javascript is successfully maintaining a reference to the correct child timeline.  When the timeline is accessed through the variable, javascript logs the correct id and progress, and the most recently created targets are animated with the most recently defined properties.

 

I have the durations increasing incrementally on each iteration of the child timeline.

 

On the first iteration, t2 duration is set to 2.  The log from the t2 function reads duration 2.  The main timeline function accesses t2 through a variable, and the log correctly reads t2's id, t2's progress, and t2's duration, and sets the main timeline duration to 2.

 

"t2 duration 2"

"maintl t2: progress 0 duration 2"

"maintl duration 2"

 

On the second iteration, t2 is unchanged because onReverseComplete has not been called.  The main timeline function accesses t2 through a variable, and the log correctly reads t2's id, t2's progress, and t2's duration, and the main timeline's duration as 0.5 (t2 duration at timescale 4).  The main timeline correctly runs t2 as accessed through the variable in reverse at timescale 4:

 

"maintl t2: progress 1 duration 2"

"maintl duration 0.5"

 

On the third iteration, t2 has been rebuilt because onReverseComplete been called.  The log from the t2 function reads duration 4.  The main timeline function accesses t2 through a variable, and the log correctly reads t2's id, t2's progress, but reports t2's duration as 2, the initial value, and reports the main timeline duration as 2.  I expect these duration values to be 4.  The main timeline correctly runs t2 as accessed through the variable forward at timescale 1, animating t2's current elements with t2's current properties:

 

"t2 duration 4"

"maintl t2: progress 0 duration 2"

"maintl duration 2"

 

On the fourth iteration, t2 is unchanged because onReverseComplete has not been called.  The main timeline function accesses t2 through a variable, and the log correctly reads t2's id, t2's progress, but reports t2's duration as 2 and the main timeline's duration as 0.5. I expect these duration values to be 4 and 1, respectively :

 

"maintl t2: progress 1 duration 2"

"maintl duration 0.5"

 

The fifth and sixth iterations repeat the process of calling onReverseComplete at the correct t2 progress, and javascript reporting the correct id and progress for the timeline as referenced by the variable, and for the duration of the child timeline as referenced by the variable to remain at the initial value.

 

The main timeline animates the child timeline's current targets with the current properties on each iteration.

 

I'm wondering if I'm missing a GSAP setting that will allow the duration of the child timeline as referenced by the variable to update at the same time the targets and properties are updated.

 

The main timeline gets out of sync faster as the rate of change of the child timeline's duration is increased, so I'm hopeful that if I can get the child timeline's duration as referenced by the variable to update, everything else will work as expected.

 

Thanks again for all of your help.

Link to comment
Share on other sites

52 minutes ago, fcdobbs said:

It looks to me like javascript is successfully maintaining a reference to the correct child timeline.  When the timeline is accessed through the variable, javascript logs the correct id and progress, and the most recently created targets are animated with the most recently defined properties.

Here's proof: 

let t1 = gsap.timeline({id: "wrong"});
t1 = gsap.timeline({
  id: "right",
  onComplete: log,
  onCompleteParams: [t1]
});
t1.to({}, {duration: 0.5})

function log(timeline) {
  console.log(timeline.vars.id); // "wrong"!
}

See the Pen ZEmzMwe?editors=0010 by GreenSock (@GreenSock) on CodePen

 

I think there may be a fundamental misunderstanding about how timing and timelines work. This code tipped me off: 

  if (animation.progress()==0) {maintl.add(animation.timeScale(1).restart(true, false))}
  if (animation.progress()==1) {maintl.add(animation.progress(1).timeScale(-4))}
  if (animation.progress()>0 && animation.progress()<1 ) {maintl.add(animation.resume())} 
  if (animation.progress()>1) {maintl.add(animation.timeScale(1).restart(true, false))}  

Think of a timeline like a container for other animations. Each child animation must have a startTime. The parent timeline's playhead sweeps across its children and updates their playheads, so they're all synchronized. Let's say you add a 4-second tween to an empty parent timeline, thus its startTime is 0. But then you immediately set the CHILD animation to progress(0.5). So the parent timeline's playhead is at 0 but you want the child animation to act as if it's halfway done already...that would require that the child's startTime get set to -2 so that its playhead is properly aligned. When you enable smoothChildTiming: true, that's what it allows (shifting around of child animations' startTimes to keep things aligned). But if you allow a negative startTime in a parent timeline, it would mess up other things like the timeline's duration and playhead bounds. For example, what would you expect to happen if you tried rewinding the parent timeline to its start, like parent.time(0) or parent.progress(0)? Wouldn't it be super weird if that child animation could NEVER get back to its very start because it's a negative value? That's why GSAP automatically senses when there's a negative startTime in a child, and it adjusts things accordingly so that the bounds shift appropriately. 

 

Here's some simple code that illustrates the concept:

let tl = gsap.timeline({smoothChildTiming: true}),
	tween1 = gsap.to(".box-1", {duration: 4, ease: "none", x: 500}),
	tween2 = gsap.to(".box-2", {duration: 4, ease: "none", x: 500});

tl.add(tween1);
tl.add(tween2, 0); // aligned at start
tween1.progress(0.5); // now tween1's playhead should be in its middle, but at what was 0 on the parent timeline, shifting it BACKWARD by 2 seconds
console.log(tl.duration(), tl.progress(), tween1.startTime()); // 6, 0.3333, 0

Again, this is all essential so that things work logically. Notice the parent timeline's duration went from 4 to 6. And tween1's startTime would have to get pushed back by 2 seconds to keep its playhead aligned with its parent (smooth timing). But it can't be negative, so the parent timeline actually adjusted its own startTime backwards 2 seconds so that it still encompasses all of its children. It's a trickle-down effect that corrects everything to keep it all lined up. 

 

It looks like maybe you were expecting that maintl.add(animation.resume()) would add that animation such that its playhead would be adjusted backwards. For example, if it's a 4-second animation and its playhead was currently at 3.5 seconds (only had 0.5 seconds left to play), it would insert it into the parent at a startTime of -3.5. That's NOT how it works. The default position parameter is at the end of the timeline (in this case, your timeline is empty so it gets inserted at a time of 0). So that child tween would play in its entirety. 

 

You can, of course, do whatever you want with the timing. GSAP gives you full control of things. So if you want to insert it into the timeline such that it continues playing from its current position, you could do: 

tl.add(tween1, -tween1.time())

 

I hope that helps clear things up. If you need more help, I think you could greatly simplify things and isolate it down to just a timeline and a tween or two without all the complicated callbacks, setTimeouts, various function calls, Arrays that track numbers of times things play, meta objects, DOM manipulation, utility function calls, etc. 

 

Good luck!

  • Like 1
Link to comment
Share on other sites

Thanks.

 

If I use this structure, what's the recommended process for only adding child timelines that have been created, so far, maintaining the order in which they are created, reading their current progress, and accommodating timelines with onComplete, onReverseComplete, or neither eventcallbacks?

 

function childOne() {
  const tl = gsap.timeline();
  ...
  return tl;
}

function childTwo() {
  const tl = gsap.timeline();
  ...
  return tl;
}

const main = gsap.timeline();
main.add(childOne(), 0.5);
main.add(childTwo(), "+=3");
Link to comment
Share on other sites

49 minutes ago, fcdobbs said:

If I use this structure, what's the recommended process for only adding child timelines that have been created, so far, maintaining the order in which they are created, reading their current progress, and accommodating timelines with onComplete, onReverseComplete, or neither eventcallbacks?

I read your question 4 times and I'm still not quite sure what you mean. 

  • You can figure out the animations' order by comparing their .startTime(). It should be quite straightforward.
  • You can get their current progress() anytime, like animation.progress().
  • You can use onComplete/onReverseComplete wherever you'd like.
Link to comment
Share on other sites

Example here to take a look -

 

See the Pen oNQvQyY?editors=1011 by GreenSock (@GreenSock) on CodePen

 

Quote

it's important that the child timelines are only added to the main timeline after they have been created.  The order in which the child timelines are created and whether or not they are created at all is unknown at start time.

 

Because the child timelines are created inside functions, that code never gets run until you 'add' them to the timeline. If this doesn't work for you, maybe you can explain a little more about your situation?

  • Like 2
Link to comment
Share on other sites

Thanks, Cassie!

I think this is getting closer.

I'd like to rebuild main timeline on each iteration with a sequence determined by the available timelines, but a random order would be fine for testing.

I'd like to only include childTwo if the event has been triggered and include childThree with the onReverseComplete alternating between forward and reverse. 

This was the part that made me think I should read the timelines into an array as they are created.

Also, I'm wondering if I assign an id to each timeline each time it's created, can I be sure that other code that grabs a timeline with gsap.getById() will only find one timeline with that id, or at least always find the most recently created timeline with that id?

Thanks again!

  • Like 1
Link to comment
Share on other sites

8 hours ago, fcdobbs said:

I'm wondering if I assign an id to each timeline each time it's created, can I be sure that other code that grabs a timeline with gsap.getById() will only find one timeline with that id, or at least always find the most recently created timeline with that id?

I certainly wouldn't recommend assigning the same ID to multiple instances. It'll return the first one it finds as it searches through the active animations (not necessarily the most recent one).

Link to comment
Share on other sites

Quote

I'd like to only include childTwo if the event has been triggered

Sure, that's easy enough, you can toggle a boolean in an event and write some conditional logic.

I think you may be complicating things and trying to handle this stuff down at a timeline level whereas the best place to deal with conditional logic is a little higher up, usually toggling some sort of global state in the callbacks which you can check in on at the point that you manually add things.

See the Pen wvQwLym?editors=0011 by GreenSock (@GreenSock) on CodePen

 

Quote

I'd like to rebuild main timeline on each iteration with a sequence determined by the available timelines, but a random order would be fine for testing.

You can tweak the start times. If I were you I'd handle that logic in it's own function which you could fire off in an onRepeat.

See the Pen vYQBqoW?editors=0011 by GreenSock (@GreenSock) on CodePen

 

Quote

and include childThree with the onReverseComplete alternating between forward and reverse. 

Could you explain what you mean by this? OnReverseComplete is just a callback that plays when the timeline finishes playing backwards, it has no reverse or forward state.

Do you maybe mean that you want to alternate between firing an OnComplete and and OnReverseComplete callback? If so, there's no harm to just setting up both, you could just fire them both and run some conditional logic in them to control what happens when they fire. They're just functions at the end of the day, what they do when they fire is entirely up to you.

 

Quote

This was the part that made me think I should read the timelines into an array as they are created.

I don't understand this need, with this setup, the timelines are only ever being created *at the moment* that they're being added to the timeline, you can choose to move the start times. I don't think you'd need to keep an array of timelines, maybe just a state object to store some boolean values?

 

As I said, It sounds very much to me that you're trying to do some higher level logic down at a timeline level, whereas this should probably be handled higher up, wrapped up in whatever logic is adding things to the timeline.

 

Hope this helps somewhat!

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...