Jump to content
Search Community

Using labels with nested timelines and pinned sections

20bs test
Moderator Tag

Recommended Posts

Hello! I'm hoping someone can help me demystify what is going on with my animations and help me understand how I can achieve my goal.

 

I have a CodePen attached that outlines what I'm attempting to do. Basically, in my demo I have three full height sections. Each section has an associated timeline in the js, and then they are nested into a master timeline. When you scroll through the page, the section pins, then there is are a couple of short animations (rectangle alpha in and then spins around) and then the section unpins and you can scroll to the next section. The timelines each have labels in them in between the animations.

 

I have a button on the page labeled "Advance". What I'm looking to do is advance to the next label when the button is clicked. And when we reach the end of the animation in one section, I need the Advance button to unpin and scroll to the next section (i.e. advance to next label in the global nested timeline).

 

I have looked at some threads on this in the forums, such as:

 

but this doesn't seem to work with gsap 3.

 

I have some code in the codePen that shows what *doesn't* work:

nextLabel() doesn't work, since they are nested.

master_tl.duration() always shows 0 for the master timeline, (because it's nested?)

gsap.globalTimeline.duration() is always increasing, doesn't appear related to the scroll position.

 

Can someone point me in the right direction or explain a methodology to achieve this? How I can make a nextLabel() type function that will work with nested timelines? How can I make it work to advance to the next pinned section?

 

Thank you!!

See the Pen ZEjYVaN by 20bs (@20bs) on CodePen

Link to comment
Share on other sites

Hi @20bs and welcome to the GreenSock forums!

 

You're using GSAP's 2.x API and not 3.x. For example getLabelTime is no longer in the API:

https://greensock.com/docs/v3/GSAP/Timeline

 

Also now you can use the labels property of a timeline which returns an object with each label and it's time, which you can combine with the next and previous label methods. Those methods just return the name of the previous/next label based on the current position of the timeline's playhead.

 

I think you could combine the labels property of the timeline instance with ScrollTrigger's labelToScroll() method in order to get that and finally use the ScrollTo Plugin to animate the scroll to that particular position:

https://greensock.com/docs/v3/Plugins/ScrollTrigger/labelToScroll()

 

Hopefully these resources are helpful to get you started. Let us know if you have more questions.

 

Happy Tweening!

  • Like 1
Link to comment
Share on other sites

Hi Rodrigo, 

 

Thanks so much for taking the time to reply. I think I made a mistake in adding some of my previous attempts at getting this to work in my demo and yes, I indeed did have a reference to a v2 call. To be clear, I'm loading v3 and want to use that going forward.

 

I've made a new pen that is now just trying to console.log the labels in the main timeline using the labels property when the Advance button is clicked. I'm not seeing any labels getting output when I click the button.

 

I saw somewhere that labels are not passed to the master timeline when they are nested like this, is that correct? How come I don't see the labels in the label property when attempting to output?

 

See the Pen XWBJvRL by 20bs (@20bs) on CodePen

Link to comment
Share on other sites

Hi,

 

There are a few issues with your setup. First you're adding a jquery ready callback on all your methods, any particular reason for that? That is not necessary, just call the combine method on the jquery ready callback and remove those. Second, your combine method, which also has the jquery ready method in it, is returning an empty timeline:

function combined_tl() {
  jQuery(document).ready(function ($) {
  
    var combined_tl = gsap.timeline();
    master_tl.add(main_one_tl());
    master_tl.add(main_two_tl());
    master_tl.add(main_three_tl());
    // This timeline has nothing in it
    return combined_tl;
    
  });
}

You don't need to add the returned result from that method, in fact you don't need to return anything from that method, just add the timelines to the parent one and be done with it:

function combined_tl() {
  master_tl.add(main_one_tl());
  master_tl.add(main_two_tl());
  master_tl.add(main_three_tl());
}

jQuery(document).ready(function ($) {
  combined_tl();
});

This seems to do what you're expecting in terms of adding the timelines to the master one and getting a single array of all the labels of all the nested timelines and an array with the scroll positions:

See the Pen bGjdwbR by GreenSock (@GreenSock) on CodePen

 

Hopefully this clear things up. Let us know if you have more questions.

 

Happy Tweening!

  • Like 1
Link to comment
Share on other sites

Hi Rodrigo

 

Thanks again for taking the time to explain this. I realize I now have to look at the label properties of children and that they're not passed to the main timeline. 

 

The issues you pointed out are unfortunate artifacts of me copy/pasting from my more complex animation and I should have been more careful when creating my minimal version.

 

The reason I'm returning the main timeline is that this whole shebang gets loaded into *another* timeline in my project. This also means that the method to simply look at the children of the main timeline isn't going to work exactly like this to get all the labels, as my project has multiple nests deep.

 

I think I understand now this method and I'll attempt to create something that will be able to recursively search for children and combine them. Thank you!

Link to comment
Share on other sites

In case it helps, here are some helper functions: 

 

Feed in a timeline and a label and it'll search all nested children too and return the associated time according to the top-level timeline that you fed in:

function findLabelTime(timeline, label) {
  let time = timeline.labels[label],
      i = 0,
      children, tl;
  if (isNaN(time)) { //if the label is in the main timeline, just return the time (performance optimization)
    children = timeline.getChildren(true, false, true); //if the label wasn't found, we need to seach all the child timelines.
    while (isNaN(time) && i < children.length) {
      tl = children[i];
      time = tl.labels[label];
      i++;
    }
    if (!isNaN(time)) { //the label doesn't exist
      time = tl.startTime() + time / tl.timeScale();
      tl = tl.timeline;
      while (tl && tl !== timeline) {
        time = tl.startTime() + time / tl.timeScale();
        tl = tl.timeline;
      }
    }
  }
  return time;
}

 

And here's one that'll grab all the labels from all nested timelines and add them to the corresponding places on the top-level one: 

function flattenLabels(timeline) {
  let convertTime = (tl, time) => {
    time = tl.startTime() + time / tl.timeScale();
    tl = tl.timeline;
    while (tl && tl !== timeline) {
      time = tl.startTime() + time / tl.timeScale();
      tl = tl.timeline;
    }
    return time;
  }
  timeline.getChildren(true, false, true).forEach(tl => {
    for (let p in tl.labels) {
      timeline.addLabel(p, convertTime(tl, tl.labels[p]));
    }
  });
}

So you could just run that once and BOOM all your labels are at the top level.

 

Does that help? 

  • Like 3
Link to comment
Share on other sites

  • 2 weeks later...

Jack and Rodrigo, 

 

Thanks so much you guys for working through this with me. I've updated this pen to remove some of the unnecessary cruft and outline my overall problems. Hopefully this will make more sense.

 

In a nutshell, my animations are added like this: 3 timelines added to > main timeline added to > uber timeline.

 

See the Pen XWBJvRL?editors=0011 by 20bs (@20bs) on CodePen

 

 

Jack, I think your helper functions set me on the right track, but it the flattenLabels doesn't appear to work like I thought that it would. It will only work for one level, which I'm getting around right now by just running every time I add timelines at each level (main and uber), but I might be able to alter this so that it recursively adds every child in the whole nest and I can just run after the last addition (uber).

 

The main issues I'm struggling with and don't understand are:

- in my demo above, I'm using console.log to spit out the labels when you click Advance. The labels are added correctly to the uber timeline (yes!), but they are each showing as the 'start' label of each respective timeline at 0, 'center' at .5 and 'finish' at 1. I was expecting that 'timeline-two-start' time would be after 'timeline-one-finish'.  Perhaps this is a function of the convertTime part of the flattenLabels not calculating correctly? 

 

- probably related is this: I'm also using console.log to show the time(), totalProgress() and totalDuration() for the uber_tl timeline - but they all show as 0. I would expect the total duration to be 3 - since that's the amount time they are showing as taking in the labels.

 

- likewise, progress always show 0, no matter where you are in the animation.

 

What am I missing here? Is the total duration not showing due to the scroll trigger not configured correctly? Do I have to refresh it after I add the items? Why is the time, total progress and duration all 0? 

 

Thank you!

Link to comment
Share on other sites

The main problem here is that you're making one of the more common ScrollTrigger mistakes - you're nesting an animation with a ScrollTrigger inside another timeline. That's logically impossible to have work properly. The playhead can either be controlled by the scroll position (ScrollTrigger) -OR- by a parent timeline, not both because of course they could be in completely different places (both fighting for control). 

 

Remove the ScrollTriggers from the nested timelines and you'll see it all works exactly as you'd expect, including the flattenLabels() function (right?)

 

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

 

Does that help? 

Link to comment
Share on other sites

19 hours ago, GreenSock said:

The main problem here is that you're making one of the more common ScrollTrigger mistakes - you're nesting an animation with a ScrollTrigger inside another timeline.

 

Hi Jack,

 

I believe this is the root of my issue, thanks again for your help on this. Your demo does now show the correct times and labels when clicking Advance - but as you can see the section pinning and animations don't work now.

 

I guess I'm discovering that to have three separate pinned sections that are controlled by scroll trigger, like in my original demo, they cannot be nested. Even though the nested scroll triggers in my original demo work exactly like I want - I can't control them like I need to after they are placed inside another timeline.

 

I'll have to head back to the drawing board and work out the best way to accomplish what I'm trying to build.

 

Thanks for all your help!

Link to comment
Share on other sites

11 hours ago, 20bs said:

I guess I'm discovering that to have three separate pinned sections that are controlled by scroll trigger, like in my original demo, they cannot be nested. Even though the nested scroll triggers in my original demo work exactly like I want - I can't control them like I need to after they are placed inside another timeline.

Yeah, can you see why it'd be logically impossible? Imagine creating three different timelines, each with a ScrollTrigger that fires exactly where you want scroll-wise. Let's say timeline1 scrubs between a scroll position of 100 and 200, and then timeline2 scrubs between 200 and 300, and finally timeline3 scrubs between 300 and 400. Great. Perhaps they each animate the rotation of a <div>, timeline1 to 90deg, timeline2 to 180deg, and timeline3 to 270deg.

 

But now you NEST all of them into a parent timeline, one-after-the-other.  

 

...now let's play that timeline. Well, we should see the <div> spin from 0deg to 90deg to 180deg to 270deg because that timeline's playhead scrubbed across all those child timelines. But wait...we didn't scroll yet! Now let's scroll 1px. DOH! That'll suddenly make the rotation JUMP to 0.9deg. Shoot. And then what if we parentTimeline.progress(0.5). Another jump. And then we scroll a little bit. Jump! There are essentially two separate (and competing) controllers of the playhead(s) and they're totally out of sync. The moment you move the scroll position, it affects the child timelines' playheads and wrestles it away from the parent timeline's control. When the parent timeline moves its playhead, it breaks the connection between the child timelines and the scroll position. 

 

See what I mean? 

 

I'm struggling to understand WHY you're trying to nest timelines with ScrollTriggers inside of a parent timeline (which may also be inside another parent timeline). Perhaps if you explain the "why" behind what you're doing, we may be able to offer a better solution. 

Link to comment
Share on other sites

On 1/4/2023 at 12:37 AM, GreenSock said:

I'm struggling to understand WHY you're trying to nest timelines with ScrollTriggers inside of a parent timeline (which may also be inside another parent timeline). Perhaps if you explain the "why" behind what you're doing, we may be able to offer a better solution. 

 

Thanks for taking the time with this, Jack.

 

I logically understand the issues with why this would be problematic.

 

The effect I'm after is actually completely working in the original demo I posted, with the nested scroll triggers: 

 

The first section (black) is pinned, then the square fades in and rotates (animated with scroll). Then as you scroll, the first section unpins and then scrolls to the next section. Same type of interaction for the other two sections, pin, animate then unpin. Basically the animations in each section are not starting until you get to that section and it's pinned. I'm searching for the correct and way to make this happen. In my more complex animation, each section has much more going on - animated SVG's, fading text, all manners of stuff - they are not the same as in my demo.

 

To get multiple sections to pin at the right timing in the nested animations, how would I accomplish that? Multiple ScrollTriggers on the main animation? Pinning somehow connected to the animations? If I have a scroll trigger on the main animation, how to I pin the sections and trigger the animations at the right time? I nested the scroll triggers, because they controlled each colored section separately. In my more advanced animation, I may have different settings for them.

 

I'm also nesting these because on the site I'm building, each of the sections might be loaded in different places around the site. So for instance, I might have section 1 on one page, then another page I might use section 1, 2 and 3 all together in a row. I'm trying to hash out a way to compartmentalize these, so that the animations are separate and I only am loading the sections and code I need for each one. In my larger, more complex animation, each of these sections I'm creating with it's own js, php and css files and loading them in dynamically depending on need. Then I just create the timeline and add only the sections I need on each page.

 

Thanks again for your help and guidance.

 

 

Link to comment
Share on other sites

I assume that each of these "sections" (or modules or whatever) probably live inside a container element, right? So couldn't you just have them live independently because they would get triggered based on where that element is in the viewport (like when it "center" of the element hits the "center" of the viewport)? Why would you need to nest things into a master/parent timeline? That's the part that's fuzzy for me. 

 

If you're literally building out a modular timeline so that you could just add individual pieces and they all kinda coordinate together (or sequence or whatever), then you'd never WANT those to have independent nested ScrollTriggers (as I explained above - it totally ruins your cohesiveness since you'd EITHER have the master timeline controlling things OR the scrollbar position). Of course you can apply a ScrollTrigger to that master (top-level) timeline so that the playhead gets scrubbed/triggered based on the scroll position. Easy-peasy. 

 

It's pretty tough for me to advise you with such generic information. We do offer paid custom consulting services if you need some help. I suspect that what you want to accomplish is totally doable once you wrap your head around the mechanics. I've done some consulting for larger companies with top-notch developers and once the light bulb went on for them, they were like "oh my gosh, this is amazing! It makes a lot of sense now..." and off they went. They just needed a little hand-holding at first.

 

Perhaps you've got what you need now to sprint ahead. 👍

Link to comment
Share on other sites

Thanks Jack.

 

I guess I'll pour through the docs again and see if I can figure out what the solution is. I feel like I've tried to explain the issue here many times and I guess I'm not doing a good job.

 

The root of the issue I'm having is that the scroll triggers are controlling the pinning, like this:

 

Timeline one:

- black section pins

- red square fades in

- red square rotates

- black section unpins

 

*page scrolls* to next section

 

Timeline two:

- red section pins

- yellow box fades in

- yellow box rotates

- red section unpins

 

etc. If I remove the scroll triggers from the nested areas - the pinning is gone. The demo needs 3 sections to each pin respectively when they hit the top of the screen. The animations inside the section need to run in coordination with the scroll, only after their respective section pins, but if I add one scroll trigger to the timeline, there's only one pin.

 

How do I program the pinning to happen in between the animations, if I can't use scroll trigger?

 

 

Link to comment
Share on other sites

14 minutes ago, 20bs said:

How do I program the pinning to happen in between the animations, if I can't use scroll trigger?

 

Ok, I suppose the answer is you don't, you use scroll trigger, but don't nest anything, like this:

See the Pen oNMzgdP by 20bs (@20bs) on CodePen

 

Ok, so, I don't have to nest them, no problem. There's no nesting requirement here, really. I was running with the thought that controlling the whole animation would be better/easier if they were all in one nested timeline.

 

In my thought patterns for this, I could add them all into one timeline, then use something like nextLabel() and it would advance to the next label across all of the animations.

 

gsap.globalTimeline doesn't seem to have the label or progress info for all items. Do I have to calculate which pinned area is in the viewport, then target the associated timeline that I have with that pinned section? 

 

Link to comment
Share on other sites

9 hours ago, 20bs said:

Ok, I suppose the answer is you don't, you use scroll trigger, but don't nest anything

Sounds about right. :)

 

9 hours ago, 20bs said:

In my thought patterns for this, I could add them all into one timeline, then use something like nextLabel() and it would advance to the next label across all of the animations.

I feel like we might be getting our wires crossed here again. If they're all nested in one timeline, sequenced, and they've got ScrollTriggers that pin stuff at certain spots...let's say you scroll down to 500px and at that spot the red section is pinned and the yellow box is halfway faded. Cool. Now what would you expect to happen if you tween the timeline to the nextLabel()? Hm. Maybe it'd perform that animation but remember you're still at a scroll position of 500px...what if that nextLabel() is associated with a ScrollTrigger that's supposed to fire at 700px? Hm. Imagine what would happen now if you scroll even one px (to 501px) - since you've got a ScrollTrigger with a scrub based on that spot, it'd suddenly jerk everything to render the way it should at 501px. Doh!

 

It's logically impossible - you'd be setting things up with competing controllers, vying for the same playhead(s). 

 

Another way to think about this...

 

Imagine setting up two pinned sections, one after the other with scrubbed animations in each (like your demo above). Black section scrubs between scroll position 100px and 500px and then the red section scrubs between 500px and 900px. Perfect. But now place them both into a timeline where they share the same startTime of 0. So in the parent timeline, they're right on top of each other, set to play simultaneously...but in terms of the ScrollTriggers, they'd trigger/scrub sequentially (black, then red). What happens when you play() the parent timeline? What if you're at a scroll position of 300px and then you play() that parent timeline...and while it's playing, you scroll 5px. Hopefully it's becoming crystal clear why this is logically impossible to accommodate nested ScrollTriggers. It's not a bug or a problem with GSAP/ScrollTrigger - it's just logically impossible. 

 

10 hours ago, 20bs said:

gsap.globalTimeline doesn't seem to have the label or progress info for all items. Do I have to calculate which pinned area is in the viewport, then target the associated timeline that I have with that pinned section? 

You lost me there. I've read that about 6 times and it still doesn't make any sense to me, sorry. Maybe it's too late and my brain is shutting down. Are you trying to add labels to the global timeline? I've never seen anyone do that and I cannot imagine how it could possibly be a good idea. And "calculate which pinned area is in the viewport" - what do you mean? How would you calculate that? Isn't that exactly what ScrollTrigger does? I mean...you can check any ScrollTrigger's isActive property, and of course there are onEnter/onLeave/onEnterBack/onLeaveBack/onToggle callbacks. It should all be super easy to accomplish, so no calculations should really be necessary for you, but I'm sure I'm just misunderstanding your question. 

 

Perhaps it'd be best if you take your simplified demo with maybe 2 sections and then give us a question like "how do I make the yellow box rotate when the black section unpins" (or whatever). I just feel like we're talking past each other a bit in theoreticals but it'd help us align if we had a very simple demo and a specific challenge to accomplish. 

 

I'm relatively confident that there's a small misunderstanding and once the light bulb goes on for you, it'll all totally make sense and it won't seem as complicated as before. Or maybe once I see your revised demo and the challenge you're facing, the light bulb will go on for me and I'll be able to show you a better way to accomplish it.

  • Like 1
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...