janpaepke / ScrollMagic

The javascript library for magical scroll interactions.
http://ScrollMagic.io
Other
14.9k stars 2.17k forks source link

when scroll with high speed miss tweens #145

Closed GeorgeGeorgiev closed 10 years ago

GeorgeGeorgiev commented 10 years ago

Hi, when you scroll with high speed like scrollTo with the plugin of tweenMax for 1 sek it miss the tweens... Here is the example : http://ag.sitetester.biz/indexscm.html . The superscrollarama do the same thing. I don't know why it miss some tweens.

janpaepke commented 10 years ago

Hi George, Because there's so much happening on the demo page it's really hard to tell which ones are skipped and which ones not. Can you please explain how to (possibly consistently) reproduce the error? Even better would be to create a jsfiddle just showcasing the problem with as little code and elements as possible.

My suspicion is that it is the same problem as in these issues: https://github.com/janpaepke/ScrollMagic/issues/92 and https://github.com/janpaepke/ScrollMagic/issues/97

But as of yet I do not have any proper environment to reproduce and debug it.

GeorgeGeorgiev commented 10 years ago

Hi Janpaepke,

ok i will try to make a jsfiddle with all the code except my custom plugins. You can reproduce the error when click on the lowest section of the right navigation and when the scrolling is done click on the topest section of the navigation. You will see that thte sections are overlaping so some of the tweens which has opacity:0 was not triggered. If you extend the time to scrolling from the navigation to 10 sek it has normalize but sometime it has a issues with the same thing which i have writed above.

GeorgeGeorgiev commented 10 years ago

Hi Janpaepke,

here is one fiddle for experiment with :

http://jsfiddle.net/GeorgeGeorgiev/56owtpf8/

It is not perfekt but you will see that it remakes the issue if you scroll down fast from the right menu and again scroll up with the right menu for 1 sek it overlaps some of the texts.

GeorgeGeorgiev commented 10 years ago

Here is close copy:

http://jsfiddle.net/GeorgeGeorgiev/56owtpf8/2/

GeorgeGeorgiev commented 10 years ago

You can put onupdate on the scrollto event console.log to see how munch steps does it make.

GeorgeGeorgiev commented 10 years ago

Ahh sorry for such a spamm but i want to make your job easier. Here is the full window view:

http://jsfiddle.net/GeorgeGeorgiev/56owtpf8/2/embedded/result/

GeorgeGeorgiev commented 10 years ago

You are right the 92 and 97 are the same things i have search trough one of the sites and it makes the same thing http://kingofthehill.esfxtv.com/ . So the easiest way to reproduce the bug is enter on the site wait all the js to load and click on the end button then click on the home button you will see how the elements are missed the tween to hide them. See the same on my site. Click End button and then home. This is not from the other scripts.

janpaepke commented 10 years ago

So I did some research on the matter.

  1. There is no problem if the tweens concern different elements: http://jsfiddle.net/janpaepke/sn04juhp/
  2. The same if the scenes have a duration: http://jsfiddle.net/janpaepke/sn04juhp/1/
  3. It also works if multiple tweens change different parameters on the same element: http://jsfiddle.net/janpaepke/sn04juhp/2/
  4. This is where things get messy. This fiddle moves the same block right, then left, then right and then back left: http://jsfiddle.net/janpaepke/sn04juhp/3/ However it only works as expected, if you scroll very slowly. The faster you scroll, the more likely to break out of the desired pattern. Sometimes the square is on the wrong side, sometimes it's even stuck in the middle. The same goes for scenes with duration even though they at least fix themselves after showing the wrong position.

So here's the problem: because all scene's get triggered at once, all tweens are started at the same time. It's really difficult to tell which has precedence over the other. The reason is that Scenes can be added in random order and their starting position can change at any time.
So how to tell which is the last one in order and should be the one played?

I experimented with the overwrite option of TweenMax without any success. Then I tried to create an example That just uses GSAP and no ScrollMagic. In this fiddle: http://jsfiddle.net/janpaepke/sn04juhp/13/ all tweens are triggered at the same time and you see a small jump when playing the tweens. When hitting reverse it will cause the box to move to that position. I will write to one of the guys over at GreenSock and ask them to have a look. I'm sure they can explain. Note: ScrollMagic wraps all tweens in a TimelineMax object to include delay and repeats in the duration and standardize methods.

As for the imminent problem I have found a solution that only works when the scene has a duration: http://jsfiddle.net/janpaepke/sn04juhp/5/ I wrapped all Tweens in a Timeline and it works, because the order in which they are supposed to be triggered is clear. Obviously this is not a good solution for the times you want to fade in an element at the top of the page and fade it out at the bottom.

At the moment I am at a loss as to how to address this issue. I would kind of have to check if multiple scenes with tweens that have conflicting parameters are triggered at the same time. Then I'd have to decide which of them has precedence (i.e. which trigger point would be the first or last) and the animation of this particular parameter for this particular tween. So my questions to all or specifically to the Greensock guys:

Any notes and suggestions are highly appreciated.

GeorgeGeorgiev commented 10 years ago

Hi jan. I have worked a lot with these too and make the same things. The overwrite property can make a difference but only in the one way :) So if you scroll back somewhere will be broke again. So for now the best way to work around is to have multiple DOM objects on one object :) To say the DOM structure of the elements is div#1>div#2 and you call first on the div#1 opacity:1 then you call on the div#2 the opacity:2. It this work around we can make a bypass for now. So see the result :

http://ag.sitetester.biz/indexin.html

Also i have tryed to kill the tweens great mistake ... It kills them and never fire again :)

janpaepke commented 10 years ago

Well what I meant in https://github.com/janpaepke/ScrollMagic/issues/134 was to kill all and then fire only one tween (the appropriate one) whenever a scene is triggered. But I realize now this will have the same problem, albeit working slightly better: http://jsfiddle.net/janpaepke/sn04juhp/7

Your suggestion to use different objects does work, but of course is rather dirty and only works with opacity tweens. There has to be a better, more universal solution.

I made a new version of the fiddle, that always checks for the tween with the highest precedence http://jsfiddle.net/janpaepke/sn04juhp/8 (also mind the console log) As far as I can tell It works and the behaviour is as expected but has two drawbacks, one small one big:

  1. (small) The first and last animation will always run, even though the target position might be the same. The reason is that the tweens that shouldn't be played still need their progress to be updated to enable them to play and reverse correctly on future triggers. This could be solved if there was a way to update the tween progress without triggering a render, which to my knowledge there isn't.
  2. (big) The bigger drawback of this method is that the Tween with the highest precedence will always win, regardless of the tweened parameters. So if not all parameters are overlapping or are different alltogether the script wouldn't care and skip all to completion apart from the one with the highest precedence. This makes this approach practically unusable apart from single application workarounds.

I think the best solution would be this:

This however does currently not work as expected with GSAP. I have created another example that again doesn't use ScrollMagic, but just mimics the way the tweens would be called by ScrollMagic. http://jsfiddle.net/janpaepke/sn04juhp/11 When firing multiple tweens the box jerks for a bit and the tweens somehow get broken. Logging their progress (use button) shows weird numbers. I noticed this is sporadic, so if it doesn't happen for you, reload and try again. When reversing the tweens in reverse order the outcome is again unexpected. It should stop at the left position but instead stops somewhere else. From now on the logged progress seems correct after every play or reverse, but the positioning is off. I tried to see if adding delays changes anything, without success...

So I guess this is as far as I can take this – I'll need some input from the GSAP folks now.

jackdoyle commented 10 years ago

From what I can tell, this is all expected behavior, but I can see why some if it appears odd or cryptic. Let me explain...

Summary of how "auto" overwriting works: The first time a tween renders, it looks for active tweens of the same target(s). If it finds any, it analyzes individual tweening properties and if it finds any, it kills just those individual overlapping properties in the previously active tween, and if there are no longer any properties left in that particular tween, it kills the tween itself (no use keeping that around and wasting resources to tween nothing).

Why were there weird progress values? Because all the first 3 tweens got killed after their first render, and the 4th stayed alive. Here's the process: 1) Tween 1 started, found no overlapping active tweens. 2) Tween 2 started, found Tween 1 that was overlapping, killed its "left" property, and since there were no tweening properties left active, it killed Tween 1 completely, thus halting its progress. 3) Tween 3 started, found Tween 2, and did all the same stuff to that (killed "left" property and the entire tween, halting its progress) 4) Tween 4 started, found Tween 3, killed all that stuff, and then completed as expected.

So as expected, the progress of Tween 4 made it all the way to 1, whereas the progress of the first 3 tweens got halted after their first render.

In what order will tweens render? In the order they're created, or if you place them in a timeline, they'll always render according to their scheduled time of execution and if you place two tweens at exactly the same spot on the timeline, they render in the order in which they were added.

How can I prevent overwriting? If you want to avoid overwriting altogether, simply add this one line of code first:

TweenLite.defaultOverwrite = false;

Just remember, overwrite management is a unique feature of GSAP that is intended to help you and in most situations, it prevents unintended behavior by protecting you from conflicts that you created in your code. However, in this situation, since ScrollMagic wants/needs total control over everything and intends to take responsibility for managing things properly, it's fine to disable the built-in overwrite:"auto" behavior.

You can also prevent an individual tween from overwriting others by setting overwrite:false in its vars/config object.

Also, paused tweens are immune from overwriting, so you can use that as a management tool if you want. In other words, if you've got 4 tweens of the same object's properties, you can pause() them all and just play() the one you want.

Why didn't all the tweens reverse() the way I expected? Because all but the last one had been killed.

Why did things render a little differently each time I reloaded? It all depends on the browser's rendering cycles and how much time elapses between when the tweens are created and when they render for the first time. In the example, all of the tweens had a duration of 0.2 seconds which is pretty short, and the first tween was animating "left" to 200, meaning it's moving at 1000px per second. Typically the browser will try to render frames at 60fps, meaning every 16.7 milliseconds, but that's not guaranteed. It may take 20ms sometimes, or 42ms or whatever. GSAP always renders things EXACTLY where they should be at that particular time (whenever the render happens). So if the first tick took 20ms, Tween 1 would render at left:4. If it took 50ms, it'd render at left:10, etc. Then, Tween 2 would start and treat whatever left is at that point as the starting value, and if 50ms had elapsed and left should end at 0 for that tween, it'd render it correctly at 9.5. Then, Tween 3 would do its calculation, treating 9.5 as the starting value and 200 as the end, thus render it at 19.025. And so on.

If the next time you reload, the browser happens to execute that tick faster, at 18ms, you'd see those values change accordingly. Again, that's exactly what GSAP should be doing.

Summary To get the behavior you're asking for (if I understand you correctly), just change the default overwrite mode to false and then you can take responsibility for managing things instead of letting GSAP's logic dictate things. Alternatively, you can pause() and play() things as you please because pausing shields tweens from being overwritten.

Does that answer your questions adequately?

janpaepke commented 10 years ago

Hi Jack,

thanks a lot for your very detailed and enlightening input. I think we've reached the core of the problem.

It also dawned on me that there was also a bit of a terminology misunderstanding as to what "overwrite" actually means. Until your last post I viewed "overwrite" as more of a 'soft' overwrite in the sense that it will keep other tweens from having an effect on the element. But as it turns out it's a pretty a 'hard' overwrite that removes properties of conflicting tweens or even kills them.

So from now on I will distinguish between ignore and overwrite.

When multiple tweens are called on the same object have conflicting properties I was expecting the tween that was called last to be rendered (for that particular property) and all previous to be ignored. Ignored means it only concerns this particular property and it means that the tween will still run, even if it was the only property it was supposed to tween, effectively doing nothing other than changing its progress.

This is because when they are called later one by one in reverse or forward they should still work as expected. The tween object ist sort of the master description of what is supposed to happen in a scene in ScrollMagic. It should NOT be modified without the users knowledge. Permanently killing the whole tween, because it had conflicting parameters, is in this case unexpected behaviour.

I also can't pause() tweens I want to be ignored, because it may be the case that they contain animations for other properties, that should still have effect.

So disabling overwrite does seem like the right way to go... I have updated the example to take us closer to a solution: http://jsfiddle.net/janpaepke/sn04juhp/14/ The start and end position of the box as well as all the progresses are correct, when playing or reversing all tweens.

But it raises the following questions:

  1. Is there a performance drawback from the conflicting properties and will the last called tween always have precedence?
  2. When playing all tweens you still see the box jump a bit. You touched the reason for this in your previous post. If I understand correctly it is because the previous tween is already in render phase, when the next one is being processed. Is there now way of checking for conflicting parameters before the tweens enter their render phase? (this is more of a cosmetic issue and ultimately no real problem, as the final outcome is still as expected)
  3. If I understand correctly the tweens save their start position the first time they are rendered and stick to it. This causes the following problem in the above example (no 14):
    1. click "play all"
    2. click "reverse all"
    3. click "play tween 0"
    4. click "play tween 1"
    The tween will now render from the position where it was overwritten by tween 2, not from the current position of the box. One solution is using a `fromTo` tween, like I did here: http://jsfiddle.net/janpaepke/sn04juhp/16/ But what would be much better is to keep the start position flexible, regardless of how often the tween is called. So is there a way to somehow always update the start position to the current position, when hitting play?
  4. ##

In Summary: As of now the answer to the question "How can I tween the same property in multiple scenes?" is "Disable overwrite, use fromTo(), while setting immediateRender to false to avoid start position overwrites."

NOTE: For this to work properly all scenes need to be updated in order of their position on the page. This is something which I'm currently working on for the next ScrollMagic release. I created this fiddle to showcase that it will actually work: http://jsfiddle.net/janpaepke/sn04juhp/17/ It behaves as expected for jumps to start and end, but doesn't work for slow backward scrolls (because of how I faked the call order.)

Still this approach does seem rather complicated for a seemingly simple usecase (fade x in at 100px down, fade x out at 200px down).

First it would be really awesome if we found a better solution to question 3. Secondly I might need to change the overwrite setting for all tweens added to a scene. Obviously I can't just change it globally without the user noticing. Actually it might be even better to try detect conflicting tweens and make the user aware of how to make them work. I might store the tweened parameters in the data attribute of each element, but keeping that in sync sounds like a nightmare. So final question: Is there a way, when examining a TweenMax object to tell if there are conflicting Tweens?

thanks, J

jackdoyle commented 10 years ago

Let me preface all of this by saying that the overwhelming priority for the architectural decisions was performance. I'll get into the nitty gritty later...

1) Is there a performance drawback from the conflicting properties and will the last called tween always have precedence? There are performance tradeoffs either way. If overwriting is disabled and you run 4 competing tweens at the same time, they'll all be setting the property (or properties) on each tick, but frankly that's not a massive performance drain (JS execution is pretty fast). The only other option is to have GSAP implement logic internally to constantly evaluate which tweens should have precedence and re-order the linked list accordingly which also costs resources. Since that option would impose a cost for ALL tweens across the board (bad), and it's FAR less likely to have competing tweens, it's wiser (in my opinion) to incur the cost on the less common scenario. It results in better performance overall.

As far as doing "soft" vs. "hard" overwrites, that's another cost consideration. The current behavior is very fast - it runs the logic once at the start, kills any conflicting properties, and lets the tween rip through its renders. But to do a "soft" overwrite, we'd have to implement new logic that would retain the data for the conflicts, suspend them for only the current rendering cycle and then...when would it re-enable them? When they go all the way back to 0? When they reverse? When they play after having been paused? Any of these options imposes a file size and performance cost across ALL tweens. We can't just re-enable the conflicting properties after the other [conflicting] tween finishes because that'd lead to odd behavior, like imagine if a rollover tween is 2 seconds and animates the scale up to 2, and then a rollout tween animates scale back to 1 over the course of 1 second (shorter) - if the user rolls over and then off quickly, you'd see the scale return to 1 (correctly) and then suddenly jump to a larger value and end at 2 (probably not at all what the developer intended).

As far as which one renders first/last, things on the root timeline render in the order they were created. The only exception is if a tween ends (or is killed) and then you re-enable it, like by calling play(0) or restart() in which case it gets added to the end of the rendering queue at that point.

When playing all tweens you still see the box jump a bit. You touched the reason for this in your previous post. If I understand correctly it is because the previous tween is already in render phase, when the next one is being processed. Is there now way of checking for conflicting parameters before the tweens enter their render phase? That's correct - the previous tween had already rendered, so the new one picks up where that left off (after killing it). In order to avoid that, we'd have to add more performance-draining code that would do a dry-run on all of the tweens across the whole engine (in nested timelines, etc.) on every tick and look for overlaps, then kill whatever's necessary, then back up and run through the whole list again and do the final renders. That's significantly more work for the CPU, and would slow down the whole engine across the board.

Alternatively, we could retain the previous values on every render, then when a conflict is detected, it would force those values back to their prior state before killing, but that's pretty costly in terms of memory and execution.

I don't know the exact number, but let's say it reduces performance by 20% just to accommodate an exceedingly rare edge case that may effect 0.001% of the user base, and it wouldn't even affect the final values, plus there are already ways around that if you run into it (use a fromTo() for example, or set overwrite:true).

There isn't a way in the current public API to find and report conflicts, but you can use TweenLite.getTweensOf(yourTarget, true) to get all the active tweens of a particular object at any given time, and you can loop through that list and call tween.kill(target, otherTween.vars) to kill the overlapping properties.

If I understand correctly the tweens save their start position the first time they are rendered and stick to it...But what would be much better is to keep the start position flexible, regardless of how often the tween is called. So is there a way to somehow always update the start position to the current position, when hitting play? Yep, the tweens record their starting values the first time the tween renders. That way, it can very quickly interpolate between them during the tween (another performance optimization). It would not only be VERY slow to keep re-checking the starting values, but it'd also be impossible because the tween itself is moving/changing the values all the time, so when it reads them, it'd just get the previous value that was set.

That being said, you can simply call yourTween.invalidate() to tell a tween (or timeline) to flush its starting values, and then the next time it renders, it'll record the current ones and go from there. It sounds like that's what you're looking for.

Does that clear things up?

janpaepke commented 10 years ago

Hi Jack,

thanks again for your detailed answer!

1. performance / precedence I understand. I thought it might be possible to have flag for tweens that tells them which properties should be ignored on that run. But like you pointed out this would also create the need for them to be constantly compared. The more important point here was the precedence and in short I think the question "will the tween called last always have precedence?" can be answered with a yes. 2. checking for conflicting parameters The reason for this question was that my users might not be aware of the pitfalls of having tweens with conflicting parameters. So I asked so I could have some sort of warning message, when they try to do it. It is however virtually impossible, because, as you know, you can also supply multiple elements to a TweenMax.to, only some of which might have conflicting properties. Furthermore ScrollMagic also supports Timelines. So in oder to achieve some sort of warning I would have to loop through all tweens and then through all tweened elements and then through all the tweened properties. Talk about excessive! :) 3. update the start position Obviously I meant to update the start position only when .play() is called, not while playing. tween.invalidate() does indeed sound like what I was looking for.

I took all this into consideration and implemented a lot of new features into the upcoming release of ScrollMagic. Among other things it updates the scenes in the order of their starting position.

:warning: Using TweenMax.to()

Fiddle: http://jsfiddle.net/janpaepke/sn04juhp/18 It seems to work at first but it breaks down if right after page load you jump to the end using the link. If you scroll up now, the tween positions are off. This is expected behaviour, because of the start position save at first render, discussed above at length.

:warning: Using TweenMax.fromTo()

Fiddle: http://jsfiddle.net/janpaepke/sn04juhp/19 Again it seems to work, but when jumping to page end at the end and then scrolling back up nothing moves. This is also expected behavior, because of the auto overwrite feature, that kills concurrent tweens (also discussed above).

So let's try disabling overwrite... Note: As we know the overwrite feature can be overwritten globally like in the following fiddles or by using the overwrite false option for every tween concerned.

:warning: Using TweenMax.to() and disabling overwrite

This will have the same problem as the above example, as with .to the problem is that the start values are saved at first render, not overwritten by other tweens. So lets go right to...

:warning: Using TweenMax.to(), tween.invalidate() and disabling overwrite

Fiddle: http://jsfiddle.net/janpaepke/sn04juhp/23 When using invalidate to reset the start positions of the tweens, everything seems to work at first. But when jumping to the end and then reversing, there are still some positioning problems. However when scrolling down again, they disappear, because the invalidate fixes the tween. Unfortunately it is also the reason for the bug: When moving very quickly the start positions are also reset to the current position. Since the previous tween has already started rendering at that point the start position is off and thus will be off, when the tween is reversed. So with this approach we kind of mimic GSAP's overwrite behaviour, with the difference, that it fixes itself on future runs. But we're getting closer!

:white_check_mark: Using TweenMax.fromTo() and disabling overwrite

Fiddle: http://jsfiddle.net/janpaepke/sn04juhp/20 Now everything works as expected and every tween lands exactly where it's supposed to, even after jumping or reversing multiple times.

So it seems we have a winner – it's the one I suspected in my previous post:

The solution is to use fromTo and disabling overwrite.


Still I'd like to make things as easy and intuitive for ScrollMagic users as possible. So I'm still looking for a way to do it with just .to() tweens.

Experiment 1: Knowing what I know now (thanks to @jackdoyle) I figured I just trigger the rendering of the tweens in order to get their correct starting values and then reset them to start, before the user even notices. And what do you know, it works: http://jsfiddle.net/janpaepke/sn04juhp/24 This fiddle behaves exactly as the fromTo example above. Both have a drawback though: The animation will always animate the full 200px of movement, and to thus appear unnecessarily jumpy when skipping scenes.

Experiment 2: Knowing that the new ScrollMagic version supports calling scenes in the right order I came up with an even better solution – ironically benefiting from the GSAP overwrite feature. See this fiddle: http://jsfiddle.net/janpaepke/sn04juhp/26 Using the scene's start event I simply initiate a tween to the appropriate position. This actually offers the best user experience, as the movement is very smooth, because the Tween will always start at the current position. This becomes apparent, when adding a couple more positions to the array. The only drawback is that his method breaks the learned ScrollMagic design pattern.


Summing up: In the future release of ScrollMagic users will have two ways of tweening the same properties of the same object.

  1. Using the native setTween() method, combined with a TweenMax.fromTo() that has the overwrite option disabled and immediateRender disabled on all but the first.
  2. Using a custom event listener that triggers animations to certain target positions.

So one last question for the GSAP folks, in case I haven't written you to sleep yet:

Solution 2 is kind of advanced and assumes a more advanced user who knows what he's doing. Most people though will try to do solution 1 intuitively and then wonder why it fails. Especially because the same code works if you give the scenes a duration: http://jsfiddle.net/janpaepke/sn04juhp/27 (The reason is that here we have a hard progress update, thus mimicking what I did in Experiment 1.)

So can you think of any way I can make this more intuitive for ScrollMagic users?

I could try to set the progress to 1 for all previous scenes, when a tween is added to a scene and reset them all afterwards. But apart from performance considerations this really sounds ugly... And it still wouldn't solve the overwrite issue.

Any ideas?

jackdoyle commented 10 years ago

It looks like you've done a fantastic job of wrapping your head around the mechanics, and I like your solution 2. The tricky thing here is that you're wanting two different behaviors in various spots - sometimes you're wanting the tweens to honor the current position as a starting point, and sometimes you're wanting to force them to particular values.

So yes, I think the smoothest, most intuitive option would be to simply re-create a to() tween based on the context (or simply reuse the same tween, but invalidate() it). If they're scrolling backwards, you tween to the "from" vars. Otherwise you tween to the "to" vars.

I haven't had a chance to dig into the source code of ScrollMagic, but for what it's worth, I envisioned the plumbing working like this:

But I haven't attempted to build a whole framework like you did, so perhaps the way I envisioned it has pitfalls that I hadn't thought of. In any case, it sounds like you've got a very good grasp of the dynamics involved here, how overwriting works, and how you can leverage the tools to get what you need. Good job!

janpaepke commented 10 years ago

Hi Jack, theoretically speaking you are correct.

But putting the whole site in one Timeline has huge drawbacks. With ScrollMagic a start position can be defined by a trigger element whose position is defined by DOM flow. Furthermore the total height of the DOM can change at any time. So I would have to constantly translate these changes into a Timeline Object, which, as you can imagine, would be a nightmare. Instead ScrollMagic creates a separate Timeline for each Scene. Now with synced animations it's easy, because every scene has a definite length and the internal start for the Timeline is always 0. Where it gets tricky is the non-linked tweens like in our case. With them ScrollMagic simply plays them forward or in reverse, whenever passing the respective trigger position. Adding a callback in ScrollMagic, using the supplied tween as a basis is almost impossible. The reason is that you can supply anything to ScrollMagic, even nested Timelines. So in order to extrapolate the behaviour the user actually wanted I would need to walk every element, extract the vars and generate a copy of the tweens in reverse. I wouldn't even know where to begin. ;)

If I push the responsibility to the user (namely using a callback-like-design-pattern) it would basically be exactly what is done in solution 2.

So I guess the bottom line is: The user has to change his learned pattern, when using ScrollMagic to animate the same properties of an object multiple times.

Still I'd love to have a way of at least pointing the user's nose to that by posting a warning to the console.

Can you think of any way to scan for conflicting tweens, when one is added to ScrollMagic?

regards, J

jackdoyle commented 10 years ago

Are you asking about scanning ahead of time, like before the tweens render for the first time?

Overwriting is much trickier than you may think because plugins may do some fancy things and create new properties that don't necessarily exist directly in the vars object. For example, think of a bezier tween that has "autoRotate:true" and you animate an HTML element. It'd end up creating a tween of the CSS "rotation" even though that word doesn't exist anywhere in the tween/vars. The same sort of thing could happen on a physics2D animation, where x and y are controlled and should therefore be overwritten even though there's no "x" or "y" mentioned in the vars at all.

So ultimately you cannot truly "search" for overlaps until the tween instantiates and renders for the first time, thus giving plugins a chance to setup whatever they need to setup. Furthermore, it seems like in ScrollMagic, certain tweens may not overlap at all, or they may completely overlap based on how quickly the user scrolls, thus pre-searching for overlaps isn't all that helpful. Or perhaps I misunderstood your question.

I am going to look at the feasibility of adding some sort of global callback like "TweenLite.onOverwrite" or something that'd give you the ability to call a function when an overwrite occurs. You could use that to do your console.log(), for example. Would that help? Feel free to suggest a particular API structure. My goal here, however, is to keep things small and fast, so I don't want to add a bunch of fancy bells and whistles, especially because this feature likely wouldn't be used by very many people.

One think I'm trying to weigh is whether it makes more sense to do a global onKill() that'd get called anytime any part of a tween is killed (which could be an overwrite or it could be someone calling tween.kill(...), etc.), or if it should only report overwrites. Again, your (and anyone's) input is welcome.

janpaepke commented 10 years ago

Yes this was exactly what I was asking for. And you are right - it is very complicated, which is why I couldn't possibly scan through them manually (as mentioned several times before). What I forgot though is that the tweens themselves are unaware of any possible overwrites until they enter the render phase. So of course there can also be no builtin way to know.

But I love your idea of a sort of overwrite event. Having this kind of callback could be a way for me to let the user know why his setup might not work. I think it would be perfect if only the Tween that gets overwritten triggers the callback. It should bubble up to any possible containers though. So If I have a Timeline that contains a Tween that gets overwritten it should also receive the onOverwrite callback.

I do understand your urge to keep things simple and light, so this would probably be something that should only be implemented for TweenMax and TimelineMax. Furthermore I can think of other applications where a callback like this could be practical. For example when you want to stop or force complete the whole tween, when only parts of it are overwritten by another one.

What do you think?

regards, J

janpaepke commented 10 years ago

All right, guys, good news. The upcoming version of GSAP (v.1.14.0) will provide an onOverwrite callback. I have implemented a check in the new ScrollMagic version, that takes advantage of it and warns the user if he runs across the issue we have discussed here. If GSAP is >= 1.14.0 a warning message will be output to the console, which directs the user to this URL to get more information abut how to resolve it: https://github.com/janpaepke/ScrollMagic/wiki/WARNING:-tween-was-overwritten-by-another

Thanks to everyone involved to get to the bottom of this and special thanks to Jack, for implementing this great new feature.

jonnymarshall commented 4 years ago

If anyone is looking for a solution to this in 2020, here's what worked for me:

const secondScene = new ScrollMagic.Scene({ triggerElement: secondSceneTrigger }).on("start", e => { if (e.scrollDirection === "FORWARD") { // Forces first scene to be in end position firstSceneTimeline.totalProgress(1); // Forces current scene to play secondSceneTimeline.play(); } if (e.scrollDirection === "REVERSE") { // Forces third scene to be in start position thirdSceneTimeline.totalProgress(0); // Forces current scene to play secondSceneTimeline.reverse(); } }).addTo(controller);

Essentially it forces previous timelines to their start or end position (depending on scroll direction) when each new trigger point is hit. Crucially it doesn't care about Timeline durations, so it will respond to fast scrolling in either direction.

Here's a gist for easy copy/pasting: https://gist.github.com/jonnymarshall/78d9eafe52a15a94a534d9a6676030a2.js