Jump to content
Search Community

Managing hero animations

SteveS test
Moderator Tag

Recommended Posts

Hi, my problem is as follows:

I am building a site using GSAP to animate elements in ThreeJS.

ThreeJS renders 3d models in browser to a <canvas> element which is fixed to the viewport and placed behind DOM content.

When the page loads, I have it set for an element to tween from offscreen to a resting position on screen, then when I scroll, it will tween off the other side of the screen using a scrolltrigger with scrub enabled.

Initially, I was making a tween that fired when the page loaded, and then in the onComplete callback I was creating the scrolltrigger tween. The issue is that if I load halfway down the page, the first tween still fires and brings the element into the background.

gsap.from(this.wave.position, {
			x: -20,
			ease:'power4.inOut',
			duration: 4,
			onComplete: () => {
				gsap.to(this.wave.position, {
					x: 11,
					y: "+= 1",
					scrollTrigger: {
						trigger: "#section-27-12",
						start: "bottom 92%",
						end: "bottom 55%",
						scrub: 1
					}
				})
			}
		})


I guess the question I have is as follows:

If I want to have elements which tween into a hero section on load, but then are controlled by a scrollTrigger, how do I handle loading the page lower than said scrolltrigger?

Link to comment
Share on other sites

11 hours ago, OSUblake said:

 

What do mean load halfway? Like the page opens halfway down?

 

You can always force the page to the top.



document.documentElement.scrollTop = 0;

 


Yes, as in the user scrolls halfway down the page and then reloads the page. Or even if the user scrolls immediately on load it will mess things up. I don't want to force users to the top of the screen, but rather I'd prefer a better way of controlling the animations.

Surely there is some sort of best practice for handing off elements from a static tween to a scrolltrigger on page load? To some degree I can force it by checking where on the page the user is at load, but that feels a bit hacky to me.

Link to comment
Share on other sites

I think I figured out a solution, though it isn't perfect.

 

waveAnim.from(this.wave.position, {
			x: -30,
			scrollTrigger: {
				trigger: "#section-27-12",
				start: "center 80%",
				end: "center 40%",
				markers: true,
    			onLeave: (self) => {
					gsap.set(this.wave.position, {
						x: 0
					})
					waveAnim.to(this.wave.position, {
						x: 11,
						scrollTrigger: {
							trigger: "#section-27-12",
							start: "bottom 92%",
							end: "bottom 55%",
							scrub: 1
						}
						});
					self.kill()
				}
			},
			ease:'power4.inOut',
			duration: 4
		})

Here I am using a scrolltrigger to detect whether I am at the top of the page and if I am past the scrolltrigger it immediately kills the tween. Then using an onLeave callback I setup the scroll responsive animation.

Some questions I have remaining:
- Are there any considerations about this method I might not be aware of?
- In the onLeave, the first thing I have to do is manually use gsap.set() or else the second scrolltrigger tween uses the x: -30 from the first scrollTrigger. Is it possible to get it so that it uses the initial position of the element, in this case x: 0?

Link to comment
Share on other sites

I see several problems with that approach:

  1. You're adding a new child tween into that waveAnim (which I assume is a timeline?)...inside an onLeave...in which you're actually killing that ScrollTrigger (and the associated animation). Basically it's suicide ;)
  2. You're making one of the most common ScrollTrigger mistakes of having ScrollTriggers on child tweens: 

What if you just put it in a container <div> or something, so you've got your intro animation separated (affecting the inner content), and the ScrollTrigger controls the container. Perhaps I'm misunderstanding your goal, but that seems to me like it's the simplest, most straightforward way of handling things. 

Link to comment
Share on other sites

33 minutes ago, GreenSock said:

You're adding a new child tween into that waveAnim (which I assume is a timeline?)...inside an onLeave...in which you're actually killing that ScrollTrigger (and the associated animation). Basically it's suicide

 This is true. However, it allows me to create a hero animation which will automatically die if the page is scrolled past a certain point and simultaneously create a scrollTrigger controlled tween. The only part of this I don't like is that I have to use a gsap.set() instead of the tween automatically "completing".

 

36 minutes ago, GreenSock said:

You're making one of the most common ScrollTrigger mistakes of having ScrollTriggers on child tweens: 

I suppose in this instance I'm using the timeline as a means of organizing my animations and not so much as a way to control the animations. I can see this might be an issue if I continue to scale the timeline. I might look at doing the same structure without a timeline for something a bit cleaner.

 

39 minutes ago, GreenSock said:

What if you just put it in a container <div> or something, so you've got your intro animation separated (affecting the inner content), and the ScrollTrigger controls the container.

Since I am using ThreeJS, there are no divs. I'm accessing 3D objects rendered to a 2D canvas. Technically I can do something similar and place the objects in a "group," but there are reasons I wouldn't want to do that. Even if I did, I'm not sure how well that addresses my core question, being what is the cleanest way to have hero elements animate in on load before being handed off to a scrollTrigger. In theory I could write a bunch of if conditions for each aspect of the animation to check the scroll position of the page, but that seems like a significant amount of work and performance cost for the given task.

I found that using an onLeave callback to create the scrolltrigger, kill the current tween, and set the position of the element gives me the results I'm looking for, but feels hacked together. I would have preferred using self.complete() to make sure my element was in the correct position instead of gsap.set(), but I found that .complete() doesn't instantly go to the tween end state, and instead animates through the entire tween very quickly.

 

Link to comment
Share on other sites

5 minutes ago, SteveS said:

I suppose in this instance I'm using the timeline as a means of organizing my animations and not so much as a way to control the animations. I can see this might be an issue if I continue to scale the timeline. I might look at doing the same structure without a timeline for something a bit cleaner.

I think there may be a misunderstanding of the logic though. An animation's playhead is typically controlled by its parent timeline's playhead progressing. This is part of the reason why it's not usually a good idea to nest ScrollTrigger-controlled animations inside of a timeline; it creates a scenario where the same animation is trying to be controlled by BOTH its parent timeline AND the ScrollTrigger (particularly if scrub is set). Like...if the parent playhead is moving forward, but the user scrolls backward...what's the child animation supposed to do? 

 

8 minutes ago, SteveS said:

what is the cleanest way to have hero elements animate in on load before being handed off to a scrollTrigger.

That's what I tried to articulate, but maybe I wasn't clear. 

 

A fundamental problem is once again the multi-control aspect of what you're trying to do. On one hand, it sounds like you want this animation to play independently (on load)...AND you want it to be controlled by a scrubbing ScrollTrigger. Is that correct? 

 

So let's say your page loads and the "loading" animation is halfway done and the user scrolls down quickly? What do you want to happen? Even if you wait to link it to the scroll position until it's done playing, how exactly would you want it to be linked to the scroll position now? For example, let's say you'd normally want it to be linked from a scroll position of 0 to 100 (the top 100px of the page) and fade out...but the user has already scrolled to 300px? Would it suddenly disappear? There are just some inherent logic challenges here. 

 

This is why I thought it was cleanest to use a separate container that's controlled by the ScrollTrigger - it can do its fade and be linked perfectly to the scroll position without any logic challenges, and your "loading" animation can independently run inside that container. If the user scrolls down 300px, the container is faded...done. No issues with playheads being controlled by two sources (timeline and scroll position). No funky jumps to invisibility, no need to dynamically create ScrollTriggers in callbacks, etc. But perhaps I'm misunderstanding some of your goals here. 

 

18 minutes ago, SteveS said:

I would have preferred using self.complete() to make sure my element was in the correct position instead of gsap.set(), but I found that .complete() doesn't instantly go to the tween end state, and instead animates through the entire tween very quickly.

I'm a bit confused - there's no complete() method. Without a minimal demo, it's hard for me to figure out what might be causing your animation to be going really fast like you described. Are you trying to make the animation go to its end? If so, you can animation.progress(1). I'm pretty confident that we can accomplish whatever you want, and there are just some misunderstandings at play. 

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