Jump to content
Search Community

ScrollTrigger unpinning via timeline (i.e. related to animation length without using scrub)

JB Code and Design test
Moderator Tag

Go to solution Solved by GreenSock,

Recommended Posts

Hi, I'm new to gsap and am having lots of fun with it :)

I did an intro animation based on scroll trigger and that worked fine with srubbing - however it's a long animation and results can vary a lot using scrub (mac mouse vs pc mouse etc), so the client and I decided to let the animation play automatically once the user starts scrolling and pinning the animation "for a while". Now here comes the problem. As I understand, the unpinning is always somehow connected to the scrolling ( I tied to a container or I can give it a pixel value, right?), but I can't work out if there is a way to tie it to the animation end? I'd be happy to just add a value to the end (like end: "2000px") to pin the animation a little and let the user scroll past, if they don't want to see it all, but, let's say the user stops scrolling straight away to watch the animation to the end, then they still have to scroll without anything happening to get past the 2000px. Also, I only want the animation to play once, but if I scroll back to the top, again I have to scroll past the additional end value, even though nothing is happening any more.

 

What I want to know: is it possible to unpin the animation container via the timeline, i.e. at the very end? So if I'm impatient I can scroll past, though I'm froced to watch it a little longer (2000px), but if I want to watch it I'm still right at the top of the next content once the animation has completed and from then on the container stays unpinned letting me scroll past like a normal static element.

I've created an extremely simplified codepen to show what I have. Note how I keep having to scroll the extra way, even though the animation is finished.

See the Pen GRvmzbe by judeb (@judeb) on CodePen

Link to comment
Share on other sites

  • Solution

Welcome to the forums, @JB Code and Design 

 

I think there may be some inherent logic flaws in what you're proposing (if I understand you correctly). It sounds like you want to start an animation at a particular scroll position (no problem!) but then you're just "pinning" because you want to force the user to see more of your animation for a while, right? So if they keep scrolling, they continue to watch it even though its progress is NOT tied to the scroll position directly (scrub)? You've got some kind of "feel" in your mind about roughly how far someone should have to scroll before things become unpinned, like let's say 1000px? But if they don't scroll at all, they're still watching your animation and then it finishes and the user still has 900px left to scroll which feels odd? So you're trying to halfway tie things to scroll...but not really in certain cases? Sounds like it could lead to some frustration for users - sometimes they have to scroll a lot, sometimes they don't - it feels unpredictable(?) 

 

ScrollTrigger's pinning is definitely tied to its start/end scroll positions. You can't randomly tell it to "unpin" or "pin" (there are various reasons for this, but I'll spare you the explanation). You have several options: 

  1. Since pinning is controlled by the scroll position, you could just jump to the end scroll position to make it unpin as soon as the user scrolls down a little more, like: 
    let tl = gsap.timeline({
      scrollTrigger: { ... },
      onComplete: () => gsap.set(window, {scrollTo: tl.scrollTrigger.end})
    });
      

     

  2. Do your own pinning with position: fixed, although it's not super easy because you've gotta adjust the end transform (or top) value based on how far you scrolled. You could actually use Flip plugin for that kind of thing and let it do the calculations for you, but I'd personally go with option #1.
8 hours ago, JB Code and Design said:

class name toggling is no longer supported?

Right, className support was removed back in 2019 when v3 came out because:

  • It's expensive performance-wise; it has to literally check every single possible CSS value to isolate the changes and then animate them. It's far better to just have you (the developer) specify which properties should animate. It makes the code clearer too. 
  • It ate up kb in the core even though VERY few people ever used that feature. We wanted to trim the fat. 
  • className tweens seemed to encourage bad habits in my opinion. 

But className tween support wouldn't help you at all in this case anyway. I'm curious why you thought it would.

 

Anyway, I hope that helps. 

  • Like 1
Link to comment
Share on other sites

Hi Jack,

 

Thank you very much for your support! :)

 

I love the solution 1! I think this is very much what I was looking for.

And I agree that forcing users to stay with the animation or having something behave in a way that is not expected is a bad call in terms of an enjoyable experience, which is obviously the ultimate aim. The reason I was looking to abandon scrubbing was simply that this was not always looking very nice, I think mostly because the animation is quite long and therefore it's easy for the user to pause and go fast as it's too long for one smooth scroll with the mouse. Not sure if there is a solution to have the animation speed more smooth and consistent and less tied to scrolling speed.

 

The idea with the changing class was to simply apply a class .fixed at the start and via timeline remove that class when the animation would finish. But thinking about this solution with your comments as well, it would most likely be really frustrating as there is no telling the user that they can finally scroll now, after not being able to do so for 10 ish seconds - most likely resulting in the user abandoning the website.

I'll have a tinker. Thank you very much for your help.

Link to comment
Share on other sites

Awesome this does exactly what I wanted:

10 hours ago, GreenSock said:

Since pinning is controlled by the scroll position, you could just jump to the end scroll position to make it unpin as soon as the user scrolls down a little more, like: 

let tl = gsap.timeline({
  scrollTrigger: { ... },
  onComplete: () => gsap.set(window, {scrollTo: tl.scrollTrigger.end})
});
  

 

It ties the pinning of the animation to the scrolling (no scrub!), allows to add some extra scroll space to ensure the user doesn't scroll right past, but if the user pauses to look at the animation it automatically scrolls to the end when the animation is finished, so the user doesn't have to actively scroll while nothing is happening.

 

For anyone else looking at this and being a newby like me: You need to first import the scrollTo Plugin - it's not included in the core.

https://greensock.com/docs/v3/Installation?checked=core,scrollTo#CDN

  • Like 1
Link to comment
Share on other sites

Once more I need help. Although I get this to work perfectly well in Codepen, I cannot get scrollTo to work on my project :(

I hope someone can help me spot the mistake - (no errors in console and ScrollToPlugin loads fine)

My code looks like this (part of a WP Plugin):

 

window.onload = function() { 
gsap.registerPlugin(ScrollToPlugin, ScrollTrigger);

...

const tl = gsap.timeline({
scrollTrigger:{
trigger: ".hero",
pin: true,
start: "top top",
end: "+=2000px",
toggleActions: "play pause resume none"
},
defaults:{
transformOrigin: "50% 100%",
ease: "power2.in",
opacity: 0,
},
onComplete: () => gsap.set(window, { scrollTo: tl.scrollTrigger.end })
});
  
tl
.from("#all", {autoAlpha: 0})
 ...

};
Link to comment
Share on other sites

Hi JB, 

 

It's a little hard to troubleshoot an environment that we can't edit and interact with like on CodePen. You should first make sure you are using the latest versions as the scrollTrigger.end is a new feature.

 

If it's still not working, then do a sanity check just make sure the plugin is working. 

gsap.set(window, { scrollTo: 400 });

 

And you might want to log out the end to value to make sure it is what you are expecting.

onComplete: () => {
  console.log("end", tl.scrollTrigger.end)
  ...
}

 

  • Like 2
Link to comment
Share on other sites

Hey Blake, Thanks so much for taking time to help.

I tried logging and it's not firing at all. I downloaded the Plugin (gsap member zip) here https://greensock.com/docs/v3/Installation

It says that's version 3.8, which is the latest version, right? So it seams the problem isn't with scrollTrigger, but with the onComplete: ... not firing - I wish I could show this on codePen, but there it works fine. Everything else works - the scrollTrigger is pinning, I get extra scroll space with end: "+=2000px" etc. just onComplete is not doing anything :(

I tested with onStart and that also works as expected.

Is it possible the mistake is in my timeline? it's a fairly long chain with labels (on Codepen this is a lot shorter)

It looks like this:

tl
.from("#all", {autoAlpha: 0})
.to("#all", { opacity: 1} )
.addLabel("sunrise")
.fromTo ('#sun', { opacity: 0, y: 500},{ opacity: 1, y: -800, scale: baseSize, duration: 1, ease: "power3.out"}, "sunrise") // sun rises
.to("#herotext, #arrow", {y: "-100", opacity: 0, duration: 0.5}, "sunrise") // hero text scrolls up
.to("#solar .unit", {opacity: 1, scale: gsap.utils.wrap([baseSize*0.75, baseSize*0.5, baseSize*1.2]), x: gsap.utils.wrap([50, 100, 100]), duration: 0.1}, "sunrise") // placing and scaling solar panels
.from("#solar", {scale: baseSize*0.5, y: 50, x: -100, duration: 1.5}, "sunrise+=0.1") // solar units appear
.to("#solar .unit", {x: gsap.utils.wrap([50, 75, 100]), opacity: 1, duration: 1.5}, "sunrise+=0.25") // solar unitzs individual zoom
.to ("#solar .panel", { opacity: 1, skewX:-40, scaleY: 0.6, x: -50, stagger: 0.1, duration: 3, ease: "back"}, "sunrise+=1") // solar panels turn up ================
.to ("#solar", { scale: baseSize*3, x: 2000, opacity: 0, duration: 1.5 }, "sunrise+=3") // solar panels zoom in and move out of view
.to ("#sun", { opacity: 0, y: -1000, duration: 1 }, "sunrise+=3") // sun vanishes
.addLabel("windrise")
.to("#ground", {opacity: 1, scale: baseSize*1.2, x: -700, duration: 5}, "windrise" ) // start moving
.to("#windmill1 .wheel", { transformOrigin: "50% 50%", rotation:360, duration: 5, repeat: -1, ease:'none', opacity: 1 }, "windrise-=1.5")
.to("#windmill2 .wheel", { transformOrigin: "50% 50%", rotation:360, duration: 4, repeat: -1, ease:'none', opacity: 1 }, "windrise-=1")
.fromTo ('.windmill', { scale: gsap.utils.wrap([baseSize*0.5, baseSize*0.75]), x: gsap.utils.wrap([400, 200]), y: 200 }, { scale: gsap.utils.wrap([baseSize*1.25, baseSize*1]), x: gsap.utils.wrap([100, 100]), y: 0, opacity: 1, duration: 2 }, "windrise-=2") // windmills zoom in
.fromTo ("#clouds", { y: baseSize*900, x: 400, scale: baseSize*0.5, opacity: 0.5}, { y: baseSize*-600, x: -200, scale: baseSize*1.7, duration: 2, opacity: 0}, "windrise-=0.5") // clouds moving through
.to('.windmill', { scale: gsap.utils.wrap([3, 3]), x: gsap.utils.wrap([-2000, -1400]), opacity: 1, duration: 2 }, "windrise+=1.5") // windmills moving out
.to(".raindrop", { rotation: 15, opacity: 1}, "windrise-=1") // raindrop to have an angle
.fromTo("#rain", {x: 2500, y: -800, opacity: 0},{scale: baseSize*1.5, x: baseSize*-2500, y: -200, opacity: 1, duration: 4}, "windrise-=1") // rain moves through
.to("#rain .raindrop line", {y: 900, opacity: 1, duration: 1}, "windrise") // rain falls
//.from ("#biomass", {x: 50, duration: 2}, "windrise+=1") // stuff grows
.to ("#biomass", {scale: baseSize*1, transformOrigin: "50% 100%", y: baseSize*500-500, duration:1, opacity: 1 },"windrise+=1") // stuff grows
.from("#grass", {y: 200, duration: 2}, "windrise+=1") // stuff grows
.from("#flowers", {y: 400, duration: 2.5}, "windrise+=1") // stuff grows
.addLabel("city")
.to(".bioleft", {x: -100, duration: 1.5, opacity: 1, ease: "power2.inOut"}, "city")
.to(".bioright", {x: 200, duration: 1.5, opacity: 1, ease: "power2.inOut"}, "city")
.fromTo("#city", {scale: baseSize*0.75, opacity: 0}, {scale: baseSize*1.25, opacity: 1, duration: 1.5, ease: "power2.inOut"}, "city") // city appears on horizon
}
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...