Jump to content
Search Community

A final clarification about invalidate() and responsive timelines

Acccent 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

Hello everyone :)

 

So this is a topic that comes back often, I guess both in the forums and in my head. Today's one of those times, and it would be nice if we could find a way to put it to rest together!

 

I'm talking about how to properly use invalidate() and to make timelines that can adapt to things like a window resize or other events.

 

Please have a look at the codepen. When you press play, the sphere moves along the line using 5 different techniques:

  • passing values to the x and y parameters
  • passing functions to x and y
  • passing values that are calculated with getBoundingClientRect() inside the timeline
  • tweening the transform property as a string (in order to use em units)
  • tweening the xPercent and yPercent parameters

If you resize the window or use the up and down buttons, the square changes size (it's based on its font-size). Every time you press the play, the timeline is invalidated before playing again. It becomes apparent that the only technique that is affected by invalidate() is the 2nd one: the functions are re-evaluated at this point. xPercent and yPercent are unaffected but they do work as expected since they're percent-based (you don't even have to invalidate).

 

Something like val = 2; tl.to(element, 1, {x: val}); will always behave the same way, even if you change val and then invalidate tl. I believe this is because val is a primitive, and so what's passed to x is the value itself, not a reference to the variable. Is this correct, or am I missing something?

 

In other words, what's the optimal way to create and work with timelines to make sure they are responsive (without relying entirely on xPercent and yPercent, as that isn't always an option)?

 

Ideally, I hope we can fully clear this up but for myself and others, and maybe have a pinned thread or a page in the docs that clarifies this, as it's a constant headache I think :)

 

See the Pen OZgzgm?editors=0100 by Acccent (@Acccent) on CodePen

Link to comment
Share on other sites

Great demo!

 

I think it will be helpful to step back and just discuss how invalidate() works on a single tween.

Its a tricky method that probably doesn't do exactly what you (or anyone else) expects it to do in all situations.

I did a quick video for you as it was much easier than trying to type it all out. 

 

Hopefully it helps you see where invalidate() is good and where it might cause unexpected results. 

 

the TL;DR is that TweenLite.invalidate() is not really The Solution for responsive stuff

 

 

 

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

 

  • Like 6
Link to comment
Share on other sites

This is super useful!

So invalidate does basically 2 things:

- re-record the start and end values of tweens based on the current position of elements (but without reevaluating the values passed when creating the tween)

- HOWEVER, it somehow does re-evaluate the function-based parameters, which is useful to know.

 

Does that mean that essentially, resize handlers (and other similar stuff) should basically recreate timelines entirely?

Link to comment
Share on other sites

Yeah, you got it!

 

Does that mean that essentially, resize handlers (and other similar stuff) should basically recreate timelines entirely?

 

Yes, to my understanding its the most reliable approach.

  • Like 1
Link to comment
Share on other sites

5 minutes ago, determin1st said:

Is there any reason why .invalidate() is not done automaticly when .play() happens?

 

I assume it's for both for performance, and also because these are two distinct behaviours and both can be useful in some cases.

 

Here's a follow-up question: assuming there's a choice between a percent-based timeline, that's responsive by default, and one that uses x and y but needs to be recreated every time, what's the better approach? I guess both performance-wise and when it comes to workflow? (This may be subjective!)

Link to comment
Share on other sites

Just now, Acccent said:

 

I assume it's for both for performance, and also because these are two distinct behaviours and both can be useful in some cases.

 

Here's a follow-up question: assuming there's a choice between a percent-based timeline, that's responsive by default, and one that uses x and y but needs to be recreated every time, what's the better approach? I guess both performance-wise and when it comes to workflow? (This may be subjective!)

 

i think, it's the same speed, because element's computed style is always returned in pixels, so the initial % needs to be determined together with the ending %, but this is done inside the lib, more flexibility, less codewriting.

 

 

Link to comment
Share on other sites

24 minutes ago, determin1st said:

Is there any reason why .invalidate() is not done automatically when .play() happens?

 

Absolutely - because invalidate() delivers a totally different result which isn't intuitive in many cases. Plus invalidate() is expensive performance-wise (it must flush the recorded start/end values and re-query and re-record them). When you replay an animation, typically people expect it to look the same as it did the first time. And imagine if your timeline is at its end state and then you invalidate() and try playing again - it's probably gonna look really weird because it's already sitting at its end state...so the new starting values will match the end state and you'll likely get no animation (though of course that'll get more complex if you've got other tweens on the same values in a sequence that are all invalidated()). See what I mean? 

 

@Acccent when you say "percent-based timeline" do you just mean populating it with xPercent/yPercent values or something? Perhaps %-based top/left? 

 

It really depends on the situation. I imagine that %-based values can be very convenient and they're also probably more processor-intensive for the browser (not GSAP) merely because it's gotta figure out the dependencies, like "if left is 25.16%...I've gotta figure out the container element's width and the multiply that by 0.2516...and if that container element is itself relative to some other thing I've gotta get that value...." and so on. A pure x/y value that's plugged into the matrix is much faster for the browser to render. But sometimes it's less convenient when it comes to responsive layouts. 

 

Does that help at all? :)

  • Like 5
Link to comment
Share on other sites

It does, in the sense that I tend to build stuff that's already on the "too expensive for the CPU" side of things, so I guess now the strategy is to try to recreate the timelines when they're not needed so it can happen in the background and they're ready when they actually do have to play :)

  • Like 1
Link to comment
Share on other sites

This is what the docs currently say:

Quote

Clears any initialization data (like starting/ending values in tweens) which can be useful if, for example, you want to restart a tween without reverting to any previously recorded starting values. When you invalidate() an animation, it will be re-initialized the next time it renders and its vars object will be re-parsed. The timing of the animation (duration, startTime, delay) will not be affected.

 

Another example would be if you have a TweenMax(mc, 1, {x:100, y:100}) that ran when mc.x and mc.y were initially at 0, but now mc.x and mc.y are 200 and you want them tween to 100 again, you could simply invalidate() the tween and restart() it. Without invalidating first, restarting it would cause the values jump back to 0 immediately (where they started when the tween originally began). When you invalidate a TimelineLite/TimelineMax, it automatically invalidates all of its children.

 

I think it could use some more specific language, and the phrase "its vars object will be re-parsed" seems  especially confusing now!

 

Here's a suggestion:

Quote

Clears any initialization data (like starting/ending positions of elements) which can be useful if, for example, you want to restart a tween without reverting to any previously recorded starting values. When you invalidate() an animation, it will be re-initialized the next time it renders and its vars object will be applied to the tweened elements, using their current state as the new starting point. The timing of the animation (duration, startTime, delay) will not be affected.

 

The values passed in the vars object will remain the same. Something like TweenMax(mc, 1, {x:variable}) will always tween mc.x to the value that variable had when the tween was first created. Using invalidate() in this scenario allows you to apply that same initial value, but with new starting and ending points. The exception to this rule is function-based parameters, which are reevaluated whenever invalidate() is called.

 

For example, if you have TweenMax(mc, 1, {x:100}) and mc.x is set to 0 initially, every time you run it mc.x will go from 0 to 100. Even if you manually change mc.x to 200, when you next run the tween mc.x will jump back to 0 and animate to 100. You can avoid that jump by using invalidate(), which will use the current state of mc to set a new starting value for the tween; in this case, mc.x will now animate from 200 to 100.

 

When you invalidate a TimelineLite/TimelineMax, it automatically invalidates all of its children.

 

 

***

 

Another question: does invalidate() have any effect on from() tweens? I can't see how it would?

Link to comment
Share on other sites

50 minutes ago, GreenSock said:

 

Absolutely - because invalidate() delivers a totally different result which isn't intuitive in many cases. Plus invalidate() is expensive performance-wise (it must flush the recorded start/end values and re-query and re-record them). When you replay an animation, typically people expect it to look the same as it did the first time. And imagine if your timeline is at its end state and then you invalidate() and try playing again - it's probably gonna look really weird because it's already sitting at its end state...so the new starting values will match the end state and you'll likely get no animation (though of course that'll get more complex if you've got other tweens on the same values in a sequence that are all invalidated()). See what I mean?

 

 

Sure, I see your logic and I can argue that, but as you said it's about intuition, okay, let it be.

 

Link to comment
Share on other sites

30 minutes ago, Acccent said:

I think it could use some more specific language, and the phrase "its vars object will be re-parsed" seems  especially confusing now!

 

I tweaked the docs to hopefully make things clearer. Your specific suggestions for the language wasn't quite accurate (or could be confusing in other ways), so I didn't use it verbatim. 

 

31 minutes ago, Acccent said:

Another question: does invalidate() have any effect on from() tweens? I can't see how it would?

 

Yep, it sure does. Remember, from() tweens are the same as to() tweens except you're just inverting the values (using the current value as the destination, and the vars value as the starting point). So since invalidate() flushes the internally-recorded start/end values and forces the tween to re-parse things, it'd use the [new] current values as the destination, thus it could be different than before. 

 

Example: if element.x is 0 and you do TweenMax.from(element, 2, {x:100}), it'd animate from 100 to 0 and if you later manually set element.x to 500 and restart() that tween, it'll still go from 100 to 0 (as it should). But if you set element.x to 500, called invalidate() and then restart(), it'll animate element.x from 100 to 500 instead now. Again, the point is it flushes recorded values and makes the tween re-parse stuff. 

 

It could also be useful if you manually change/update the vars value as well. 

 

Make sense? 

  • Like 3
Link to comment
Share on other sites

Oh, of course, haha.

 

Later on I'll post a draft for a full guide on when to use xPercent, invalidate(), function-based parameters or when to recreate timelines entirely, depending on the situation. Just need to find the time hehe :)

  • Haha 2
Link to comment
Share on other sites

One more thing. The greensock animation plays it's duration even if starting/ending values are equal, I've tested it for className, but suppose it is true for manual setting. The .isActive() returns true and it could be more appropriate for this case to act like a duration=0.

Link to comment
Share on other sites

26 minutes ago, determin1st said:

The greensock animation plays it's duration even if starting/ending values are equal, I've tested it for className, but suppose it is true for manual setting. The .isActive() returns true and it could be more appropriate for this case to act like a duration=0.

 

Oh, that's very intentional. If a developer tells a tween that its duration should be 1 second (or whatever), it'd be pretty weird if we were like "Nope, sorry, we're gonna make that 0." It's common for developers to write code that depends on specific timing and the values that get plugged into the tweens may be dynamic, so it might cause a lot of confusion if we shifted timing like that. 

 

Reporting isActive() as false when the parent timeline's playhead is overlapping that tween's placement would be inaccurate and likely cause confusion. 

 

If you want that behavior, I'd encourage you to just run conditional logic: 

if (obj.value === 100) {
  doStuff();
} else {
  TweenMax.to(obj, 1, {value:100, onComplete:doStuff});
}

 

Does that clear things up? 

  • Like 3
Link to comment
Share on other sites

  • 5 years later...

@Carl your video explaining invalidate() was SUPER helpful! Thanks a bunch for walking it through so clearly.

I'm currently working on a ScrollTrigger animation that requires recalculating x position on resize and found invalidate() works as expected while the timeline's progress is 0. However, things break when scroll position puts the timeline's progress past 0. Below is a pen to demonstrate. Try resizing the width while above the scrollTrigger's start, and then try resizing somewhere after the start position. Any suggestions on how to handle this?

 

See the Pen c2d6de92e189404436590c055a3c7a4f by creativeocean (@creativeocean) on CodePen

  • Like 1
Link to comment
Share on other sites

Hey Carl. Thanks for the reply. ScrollTrigger's invalidateOnRefresh has the same effect as if you call invalidate() on the timeline (though it doesn't work if you do both). Since I needed to put the updated x calculation in the onRefresh function, I thought it read easier this way.

 

PS, I swear I didn't mean to @ you for such a fast reply!  Thanks so much for your time + thoughts on this!! :)

 

Link to comment
Share on other sites

Hi,

 

Maybe you're looking for something like this:

const getPos = () => {
  rect = lastItem.getBoundingClientRect();
  rowWidth = gsap.getProperty(".row", "width");
  rowX = row.getBoundingClientRect().x;
  pos = rowWidth - (rect.x + rect.width) + rowX;
  return pos;
};

let tl = gsap
  .timeline({
    scrollTrigger: {
      trigger: ".row",
      start: "100% 100%",
      end: "0% 9%",
      scrub: 0.5,
      markers: { endColor: "blue" },
      invalidateOnRefresh: true
    }
  })
  .fromTo(".row-inner", { x: 0 }, { x: () => getPos(), ease: "none" });

Here is a fork of your codepen:

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

 

Hopefully this helps.

Happy Tweening!

  • Like 1
  • Thanks 1
Link to comment
Share on other sites

Hi Tom,

 

Actually @Carl's solution is exactly the same, use invalidateOnRefresh in the ScrollTrigger configuration with a function based value, the only difference is the calculations happening in said function.

 

Carl, as the excellent teacher He is, created an example from scratch. Me, being a bit lazy, just forked your example and changed a few things here and there :D

 

Happy Tweening!

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