Jump to content
GreenSock

Search In
  • More options...
Find results that contain...
Find results in...
Tonycre8

ScrollTrigger and ThreeJS

Recommended Posts

Hey guys, I'm having some trouble on something this evening, was wondering if you guys could help.
Given that this uses a 3d model, I can't attach a codepen unfortunately. But I'll do the best to include all relevant code snippets and give all the details I can.

 

I've been working on a website that uses scroll trigger to move content around, and also to move around this 3D car model that I have.

I have 4 different slides, and for the sake of simplicity, I've gone ahead and recoloured them. 

Slide 1: Blue, start slide

Slide 2: Pink, slides in from the right

Slide 3: Yellow, slides in from bottom
Slide 4: Green, slides in from the right

 

Now Slide 4 is the main culprit, I'm trying to move this car around to have the car in the 4th slide look like the image below:

7grhKFq.png

However, the transition to the image is a little less than sleek. I've attached a video of how the transitions currently look, scrolling between these different slides. Ignore the music, I just recorded this very quickly on my ShadowPlay.

Now let's talk about the code I've got.

 

I'm trying to use ScrollTrigger to move between all of these, and I created a variable for a timeline just so I could keep it all in check (or so I thought). Here's what I have thusfar.

 

scene.rotation.set(0, 9.8, 0)
camera.position.set(-1.2,0,5)

let car_anim = gsap.timeline()

// Slide 2

car_anim.to(scene.rotation, {y: 4.79, easing: Power1.easeInOut, scrollTrigger: {
    trigger: "#slide2",
    scrub: 1,
}}, 0)
car_anim.to(camera.position, {x: -0.1, easing: Power1.easeInOut, scrollTrigger: {
    trigger: "#slide2",
    scrub: 1,
    start: "right right"
}}, 0) 

// Slide 3

car_anim.to(scene.rotation, {z: 1.6, easing: Power1.easeInOut, scrollTrigger: {
    trigger: "#slide3",
    scrub: 1,
    start: "top top"
}}, 0)

// Slide 4 - The problem child

car_anim.to(scene.rotation, {z: 0.02, y: 3.1, easing: Power1.easeInOut, scrollTrigger: {
    trigger: "#slide4",
    scrub: 1,
    start: "right+=2500 right"
}}, 0)

car_anim.to(camera.position, {x: 0.16, easing: Power1.easeInOut, scrollTrigger: {
    trigger: "#slide4",
    scrub: 1,
    start: "right+=2500 right"
}})

I start out with setting a base camera position and scene position. The scene position refers to the car, and the camera just refers to the camera. The first three slides work well. I have another video somewhere of all of those working just fine. I'm just having trouble of smoothly continuing the movements of the car. It's not a ThreeJS issue I don't think. I'm pretty sure it's just the start of the scroll trigger, given that the other ones have worked fine. I guess it's as I've started making more complex movements.

So yeah, how do you think I could get these working at all? I'm not entirely sure what I'm doing wrong to be very honest with you.

 

Thanks in advance, and sorry for the lack of codepen - it's a little hard to do with all the technical set up.

UPDATE: Just an edit after I've played around with it a bit more. I think it's becoming less and less of a gsap issue, but I'll keep this thread up in case anyone else knows how to solve it / it can be used as a future reference. It seems as if when I modify a different axis more than once (i.e. the y-axis), it updates the value weirdly. I don't have console log proof, but I messed around with the 4th slide and it seems to transition fine when I set it to affect the x-axis - a property that has yet to be used with the scene rotation.

Link to comment
Share on other sites

Hey Tony.

 

A few notes about your code: 

  • We recommend using the condensed string form of eases. In this case it'd be ease: "power1.inOut".
  • We recommend using GSAP's defaults to save you from having to type the same property and value over and over again in your timeline tweens.
  • You use a value of 0 in the position parameter for different tweens which makes them conflict. Why are you even using the position parameter in this case since you want the animations to be sequenced? I recommend that you learn more about the position parameter :) 
Link to comment
Share on other sites

 

Since I am interested in this myself, I prepared a little reduced and slightly differed scenario that basically represents what @Tonycre8 is experiencing (at least I think so).

 

See the Pen 856fe178c736444b2d6e1d7b1e13bce2 by akapowl (@akapowl) on CodePen

 

When getting to the point, where a parameter (x, y or z of camera position or scene rotation), that already has been tweened, is going to be tweened again, the value where it is tweened from appears to be the value that has been set up prior to setting up the ScrollTriggers and not the value it has been tweened to before. In this case:

 

    scene.rotation.set(0, 1.88, 0)
    camera.position.set(2, 0, 5)

 

I hope that is somewhat understandable.

 

Interestingly enough though, I found, that if you scroll to the bottom of the page and refresh the page, the animation seems to run as probably intended by OP - in both directions, scrolling up and down.

 

  • Like 1
Link to comment
Share on other sites

Just set immediateRender: false on your ScrollTriggers. 

 

By default, ScrollTrigger will force an initial render on the associated animation in order to ensure maximum performance and (the first render is always the most CPU-intensive because it must parse and record the starting/ending values so that it can quickly interpolate during the tween). So everything is working the way it's supposed to, but you've got multiple tweens affecting the same properties of the same object, thus the initial state is getting recorded for them all. 

 

You can tell ScrollTrigger not to do that with immediateRender: false. 

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

 

100% buttery smoothness in my pen now.

 

See the Pen 94ebcbff6f403f34075b36738ad41c3d by akapowl (@akapowl) on CodePen

 

 

 

Also, I cleaned things up just a tiny bit to show the usage of GSAP's defaults, as @ZachSaucier hinted to.

 

Thanks for the help and explenation @GreenSock

 

You might wanna try this, @Tonycre8

  • Like 1
Link to comment
Share on other sites

5 minutes ago, akapowl said:

 

100% buttery smoothness in my pen now.

 

 

 

 

 

 

Also, I cleaned things up just a tiny bit to show the usage of GSAP's defaults, as @ZachSaucier hinted to.

 

Thanks for the help and explenation @GreenSock

 

You might wanna try this, @Tonycre8

This works like a charm! Thanks a lot you guys! I'll definetely have a look at making my code a little cleaner too haha. Thanks a lot to the lot of you champs.

Also, quick shoutout to @akapowl for making a CodePen because I was too lazy too :'> Appreciate it

  • Like 3
Link to comment
Share on other sites

One more thing:

 

When chaining the tweens up one after the other as tightly as that, I saw, that you will eventually still get jumps, when using

 

scrub: 1

 

and scrolling too fast, because the scrubbed value is yet to be applied when the next tween already starts.

 

So I guess, either just use scrub: true and lose that nice effect, or make sure to implement some "buffer-zones" in your markup to give it enough air to breathe, when you want to keep that nice effect, the scrub: 1 delivers.

 

Someone please correct me if I am wrong or if there is another way to avoid that.

 

 

  • Like 1
Link to comment
Share on other sites

6 minutes ago, akapowl said:

One more thing:

 

When chaining the tweens up one after the other as tightly as that, I saw, that you will eventually still get jumps, when using

 


scrub: 1

 

and scrolling too fast, because the scrubbed value is yet to be applied when the next tween already starts.

 

So I guess, either just use scrub: true and lose that nice effect, or make sure to implement some "buffer-zones" in your markup to give it enough air to breathe, when you want to keep that nice effect, the scrub: 1 delivers.

 

Someone please correct me if I am wrong or if there is another way to avoid that.

 

 

I was going to say, I was just experiencing a little issue with the scrolling where the object has some trouble flicking about a bit haha

Not so sure how to implement buffer-zones though. Would these be like, additional divs to end on? I'm a bit new haha

Link to comment
Share on other sites

25 minutes ago, Tonycre8 said:

Not so sure how to implement buffer-zones though. Would these be like, additional divs to end on?

Sure that's one way. Or using margin to push content down. 

 

32 minutes ago, akapowl said:

Someone please correct me if I am wrong or if there is another way to avoid that.

If you made all of the tweens parts of a larger timeline and applied the ScrollTrigger to the timeline instead of the tweens in the timeline then it would avoid jumping.

  • Like 2
Link to comment
Share on other sites

On 8/4/2020 at 2:48 PM, ZachSaucier said:

If you made all of the tweens parts of a larger timeline and applied the ScrollTrigger to the timeline instead of the tweens in the timeline then it would avoid jumping.

 

Like so then, right?

 

See the Pen d84ca98a04e21d5fcc63d0f9d5d43d3d by akapowl (@akapowl) on CodePen

 

Works like a charm! Thanks a lot @ZachSaucier

 

  • Like 1
Link to comment
Share on other sites

Okay so with that example then, how would I get it to properly stagger between slides? I can't get the start times to quite align.

For example, I've copied a bit of what akapowl has and tried messing with it, but the animations trigger too quickly. Where I want one option to be tweened later on, it tweens it straight away.
mq9y8y9.png

As an example in this image, the car should only be rotated fully like this when the pink is fully visible. I.e. and entire slide covers the view.

76B50Ra.png

But it's already at this point by the time I get to the second slide. Which is odd, considering I told it to **start** at the second slide, but it's now ending it's animation on there.

Here's a quick snippet:

let car_anim = gsap.timeline({
  scrollTrigger: {
    trigger: "#slide2",
    endTrigger: "#slide5",
    start: "top top",
    end: "+=1000",
    scrub: 1
  }
})

ScrollTrigger.defaults({
  immediateRender: false,
  ease: Power1.inOut
})

car_anim
  // Slide 2
  .to(scene.rotation, {y: -1.6}, 0)
  .to(camera.position, {x: -0.1}, 0) 
  // Slide 3
  .to(scene.rotation, {z: 1.6}) // This happens too quickly!

 

Any advice on how to get that to work? So that it only triggers when it hits the next slide?

Link to comment
Share on other sites

I think you should read about how scrub works in the docs (the section "How does duration work with scrub: true?"). It will explain how it works. Short answer: add spacing between your tweens.

  • Like 2
Link to comment
Share on other sites

Okay I finally got it! I'll drop a video here to just to show you guys.
Just had to tweet how I was doing things, and adding labels too to keep the timeline going. Here's a snippet of what's changed:

let car_anim = gsap.timeline({
    duration: 1,
    scrollTrigger: {
        trigger: "#slide2",
        endTrigger: "#slide4",
        start: "top top",
        end: "+=3000",
        scrub: 1,
        markers: true
    }
})
ScrollTrigger.defaults({
    immediateRender: false,
    ease: Power1.inOut
})


// Slide 2
car_anim
    .add("slide2", 0)
    .add("slide3", 10)
    .add("slide4", 20)
car_anim
    .to(scene.rotation, {y: -1.55, duration: 4}, "slide2")
    .to(camera.position, {x: -0.1, duration: 4}, "slide2") 
// Slide 3
    .to(scene.rotation, {z: 1.6, duration: 4}, "slide3")
// Slide 4
    .to(camera.position, {x: -.15, z: 4.2, duration: 4}, "slide4")
    .to(scene.rotation, {z: .035, y: -3.15, duration: 4}, "slide4")

And then here's the video:

Thanks a lot you guys for the help!

-Tony

  • Like 4
Link to comment
Share on other sites

Nice job! Thanks for sharing.

  • Like 1
Link to comment
Share on other sites

  • 1 year later...

Hello experts, I am also experiencing this issue in my Typescript setup with react-three-fiber and gsap ScrollTrigger. I can't seem to apply the fix in my code:

import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";

gsap.registerPlugin(ScrollTrigger);

// this doesn't work
ScrollTrigger.defaults({
  immediateRender: false,
  ease: "power1.inOut",
  scrub: true,
} as any);


function AnimationWrapper() {
  const { scene, camera } = useThree();
  const tl = gsap.timeline();
  useEffect(() => {
    scene.rotation.set(0, 3.1, 0);

    tl
      .to(scene.rotation, {
        y: 1.65,
        scrollTrigger: {
          trigger: ".section-two",
          // this won't work either
          immediateRender: false,
          scrub: true,
          end: "top top",
        } as any,
      })
      .to(scene.rotation, {
        y: 3.1,
        scrollTrigger: {
          trigger: ".section-three",
          // this won't work either
          immediateRender: false,
          scrub: true,
          end: "top top",
        } as any,
      });
  }, []);
  return null;
}

Is this solution still viable?

Link to comment
Share on other sites

Hey @thvu,

 

I am not familiar with react whatsoever, so in case this is a react problem, someone else would have to jump in.

 

But I notice you are making one of the most common ScrollTrigger mistakes there, nesting ScrollTriggers inside of individual tweens of a timeline.

That won't work like that.

 

 

 

 

For timelines, ScrollTriggers are supposed to be declared in the timeline object only and not on individual tweens.

 

If you need to trigger things at different trigger-points, you have the options to set up individual tweens with individual ScrollTriggers or create one longer timeline and space things out correctly via durations and/or the position parameter.

 

Both options are to be seen in the examples above.

 

If changing that doesn't help you resolve your issues, it would be awesome if you could create a minimal demo for us to tinker with.

 

Welcome to the GSAP forums :) 

 

  • Like 4
Link to comment
Share on other sites

Hi @akapowl,

Thanks for pointing out my rookie mistake. I finally got it working by applying your suggestion. Interestingly, I don't need to use `ScrollTrigger.defaults()` with `immediateRender`. It just works.

For anyone having the same issue, here's what it look like (Hopefully I can find sometime to add a working codepen later):

import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";

gsap.registerPlugin(ScrollTrigger);

function AnimationWrapper() {
  const { scene, camera } = useThree();
  const tl = gsap.timeline({
    scrollTrigger: {
      trigger: ".section-two",
      endTrigger: ".section-five",
      scrub: true,
    },
  });
  useEffect(() => {
    scene.rotation.set(0, 3.1, 0);

    tl
      .to(scene.rotation, { y: 1.65 })
      .to(scene.rotation, { y: 3.1 });
  }, []);
  return null;
}

 

Link to comment
Share on other sites

34 minutes ago, thvu said:

`ScrollTrigger.defaults()` with `immediateRender`. It just works.

 

immediateRender isn't a default. That's why you had to use any.

 

interface Vars {
    anticipatePin?: number;
    end?: string | number | StartEndFunc;
    endTrigger?: gsap.DOMTarget;
    horizontal?: boolean;
    id?: string;
    invalidateOnRefresh?: boolean;
    markers?: boolean | MarkersVars;
    once?: boolean;
    onEnter?: Callback;
    onEnterBack?: Callback;
    onLeave?: Callback;
    onLeaveBack?: Callback;
    onRefresh?: Callback;
    onRefreshInit?: Callback;
    onSnapComplete?: Callback;
    onScrubComplete?: Callback;
    onUpdate?: Callback;
    onToggle?: Callback;
    pin?: boolean | gsap.DOMTarget;
    pinnedContainer?: gsap.DOMTarget;
    pinReparent?: boolean;
    pinSpacing?: boolean | string;
    pinType?: "fixed" | "transform";
    refreshPriority?: number;
    scroller?: gsap.DOMTarget | Window;
    scrub?: boolean | number;
    snap?: number | number[] | "labels" | "labelsDirectional" | SnapFunc | SnapVars;
    start?: string | number | StartEndFunc;
    toggleActions?: string;
    toggleClass?: string | ToggleClassVars;
    trigger?: gsap.DOMTarget;
  }

 

Link to comment
Share on other sites

6 minutes ago, OSUblake said:

immediateRender isn't a default. That's why you had to use any.

 

@GreenSock are immediateRender and ease supposed to be a valid ScrollTrigger vars?

Link to comment
Share on other sites

3 hours ago, OSUblake said:

 

@GreenSock are immediateRender and ease supposed to be a valid ScrollTrigger vars?

Technically ScrollTrigger will recognize if you put immediateRender in the ScrollTrigger vars or the tween's vars object. By default, ScrollTrigger always immediately renders its animation in order to maximize performance and avoid a few edge case annoyances, but you can set immediateRender: false if you prefer (in either place). ease, however, does NOT belong inside the ScrollTrigger vars object. That's tween-specific. 

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

Here's a working example of react-three-fiber and gsap ScrollTrigger. I basically took @akapowl's pen and rewrite it:

See the Pen rNwLKOj by thanghuuvu (@thanghuuvu) on CodePen

Edit: I left out the opacity animation. Somehow adding it causes unwanted flickering.

  • Like 1
Link to comment
Share on other sites

You should never create timelines/tweens outside of a hook. Once the state updates, it's going to create a brand new timeline and scroll trigger, messing everything up. I broke it for you 😉

 

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

 

Be sure to check out our React Guide and the Creating and Controlling Timelines section.

 

 

  • Like 4
Link to comment
Share on other sites

@OSUblake perhaps that's why I experienced some flickering if I add the opacity animation in @akapowl's pen. I fixed the pen, thank you for being so helpful!

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