Jump to content
Search Community

Missing Something w/ Horizontal ScrollTrigger ๐Ÿ˜…

jh3y test
Moderator Tag

Go to solution Solved by GreenSock,

Recommended Posts

@jh3yย I've been slammed with other stuff, but I started looking into this and I know what the issue was in my demo that caused the oscillating in that one scenario. I'll aim to circle back with solution for you late tomorrow (it's almost 3am and my brain is powering down) :)ย Thanks for your patience.ย 

  • Like 1
Link to comment
Share on other sites

3 hours ago, GreenSock said:

@jh3yย I've been slammed with other stuff, but I started looking into this and I know what the issue was in my demo that caused the oscillating in that one scenario. I'll aim to circle back with solution for you late tomorrow (it's almost 3am and my brain is powering down) :)ย Thanks for your patience.ย 

Hey @GreenSockย ๐Ÿ‘‹

ย 

That's fine! Thank you for the heads up. It's more relief that you can see a solution ๐Ÿ˜…

I'll happily wait on that haha. I'm interested to see how I've broken the navigation buttons too. I think it's linked to this. But the way I've tried making them has perhaps made it more prominent.

ย 

I have some other GSAP content to make in the mean time ๐Ÿ‘

Link to comment
Share on other sites

  • 2 weeks later...

Alright,ย @jh3yย here's an updated demo that simplifies several pieces:ย 

See the Pen BaQXjWw?editors=0010 by GreenSock (@GreenSock) on CodePen

ย 

It uses your "build 3 full cycles of animation and then just scrub the middle one" approach which is easier for people to visualize/understand than my versionย that focused more on minimizing the number of animations for performance sake. The reality is that nobody is gonna notice any difference in a real-world scenario, and I know you're trying to help people understand the approach so it may be better to just go the simpler route.ย 

ย 

I also made theย buildSeamlessLoop() function more useful by letting you pass in a function that gets called for each element, and you just return an animation (timeline) and it handles dropping it into a master timeline. This the methods more modular - you don't have to parse through the wholeย buildSeamlessLoop() method code to find where to put your animations.ย 

ย 

It handles all 3 iterations by simply doing:ย 

items.concat(items).concat(items).forEach((item, i) => { ... });

Furthermore, I reserved the first and last 1px of scroll as a way to sense when the user is scrolling all the way up or down, when we'll wrap. The ScrollTrigger is unchanged - it's just that when I calculate where to actually set the scroll position according to where we're snapping or wrapping, I won't allow it to sit at exactly 0 or at the end - I offset it by 1px. I think it's a more bulletproof approach.ย 

ย 

Go ahead and take a peek and let me know if anything is unclear or doesn't translate to your demo well. I'm pretty sure you'll find this new version cleaner/easier and it doesn't have the bug where you could get it to rock at the very start when crossing the start/end boundary.

  • Like 4
Link to comment
Share on other sites

Hey @GreenSockย ๐Ÿ‘‹

ย 

Appreciate this a lot! ๐Ÿ™ย 

ย 

I'll make some time to go through it and swing my article in this direction ๐Ÿ‘ย 

I had built it up around the idea of taking three timelines and tacking them onto a main timeline. But, that can get confusing and looping over the elements three times is more intuitive once that timeline concept is in place. That's how I'll swing the direction shift I think ๐Ÿ˜

ย 

Again. I appreciate this a lot. Looking forward to getting this one written up and sent off.

ย 

Thank you for all your help.

ย 

I still have a feeling I'll be posting in this thread again though ๐Ÿ˜…

ย 

I did have one idea which I have only pseudo-coded in my head. Would it be possible to use a Draggable proxy and drag the time? I imagine so, right? That would be cool. I imagine it will be a case of putting a threshold on how much distance correlates with the time scrub ๐Ÿค”

  • Like 1
Link to comment
Share on other sites

On 3/20/2021 at 8:18 AM, jh3y said:

I did have one idea which I have only pseudo-coded in my head. Would it be possible to use a Draggable proxy and drag the time? I imagine so, right? That would be cool. I imagine it will be a case of putting a threshold on how much distance correlates with the time scrub ๐Ÿค”

Sure, but it may be simpler to just use some vanilla JS for this because you won't need to have an extra element that obscures pointer events (like if you needed to make things clickable, like those buttons):ย 

let startX = 0,
    startOffset = 0,
    getEvent = event => (event.changedTouches && event.changedTouches[0]) || event, // to make it touch-safe
    events =  document.body.onpointerdown ? ["pointerdown","pointermove","pointerup","pointercancel"] : document.body.ontouchstart ? ["touchstart","touchmove","touchend","touchcancel"] : ["mousedown","mousemove","mouseup","pointercancel"];

// when the user presses on anything except buttons, start a drag...
document.addEventListener(events[0], event => {
  const e = getEvent(event);
  if (e.target.tagName.toLowerCase() !== "button") {
    document.addEventListener(events[1], onPointerMove);
    document.addEventListener(events[2], onPointerUp);
    document.addEventListener(events[3], onPointerUp);
    startX = e.pageX;
    startOffset = scrub.vars.offset;
  }
});
function onPointerMove(event) {
  scrub.vars.offset = startOffset + (startX - getEvent(event).pageX) * 0.001;
  scrub.invalidate().restart(); // same thing as we do in the ScrollTrigger's onUpdate
  event.preventDefault();
}
function onPointerUp(event) {
  document.removeEventListener(events[1], onPointerMove);
  document.removeEventListener(events[2], onPointerUp);
  document.removeEventListener(events[3], onPointerUp);
  scrollToOffset(scrub.vars.offset)
}

Here's an updated demo (fork):ย 

See the Pen RwKwLWK?editors=0010 by GreenSock (@GreenSock) on CodePen

ย 

Is that what you had in mind?ย 

ย 

So now you can scroll, drag, or click the "next/previous" buttons to infinitely scroll. Fun!

  • Like 4
Link to comment
Share on other sites

@jh3yย The night I posted that version withย horizontal dragging enabled, I realized it wouldn't work right on touch devices. I also thought of a way to simplify it into a Draggable call:ย 

See the Pen RwKwLWK?editors=0010 by GreenSock (@GreenSock) on CodePen

ย 

Also, I noticed a problem in your demo:ย 

// incorrect
const SNAP = gsap.utils.snap(1 / BOXES.length)

// correct
const SNAP = gsap.utils.snap(STAGGER)

Here's a fork of your demo with the tweaks in place:ย 

See the Pen xxgVJWo?editors=0010 by GreenSock (@GreenSock) on CodePen

ย 

Does that work well for you?ย 

Link to comment
Share on other sites

6 hours ago, GreenSock said:

@jh3yย The night I posted that version withย horizontal dragging enabled, I realized it wouldn't work right on touch devices. I also thought of a way to simplify it into a Draggable call:ย 

ย 

ย 

ย 

Also, I noticed a problem in your demo:ย 


// incorrect
const SNAP = gsap.utils.snap(1 / BOXES.length)

// correct
const SNAP = gsap.utils.snap(STAGGER)

Here's a fork of your demo with the tweaks in place:ย 

ย 

ย 

ย 

Does that work well for you?ย 

ย 

Hey @GreenSock

ย 

Ahh cool. I did wonder if something with a proxy might work. But, my Draggable experience is limited to a few demos. Thanks for that.

ย 

Erm. I'm not sure that is an error/problem. That only works because the spacing and number of cards align "perfectly". But, as soon as you change the number of cards or changing the spacing(STAGGER), the snapping will no longer work. If you try either changing the count to 50 cards or changing the spacing to 0.25, the demo will break. Whereas in my version, the snapping will continue to work with either value changed.

ย 

There was one thing I had a little bit of trouble working out. But, I think that's because I've been looking at it for some time at this point. Calculating the offset of a clicked card from the center. If I click a card that's 2 to the left, how do I get `-2` from that to move the timeline ๐Ÿค” Still messing with that. I'm sure there's a Math way with wrap to normalize the center point and the indexes somehow based on the current position. I think I've written out what I need to do there ๐Ÿ˜…

Link to comment
Share on other sites

6 hours ago, jh3y said:

Calculating the offset of a clicked card from the center. If I click a card that's 2 to the left, how do I get `-2` from that to move the timeline ๐Ÿค” Still messing with that. I'm sure there's a Math way with wrap to normalize the center point and the indexes somehow based on the current position.

Here's how I'd do it:ย 

// OLD
document.querySelector('.boxes').addEventListener('click', e => {
  const BOX = e.target.closest('.box')
  if (BOX) {
    // Get the current index
    const IDX = gsap.utils.wrap(0, 10, Math.floor(SCRUB.vars.position * 10))
    const TARGET = BOXES.indexOf(BOX)
    // Can only hit three values
    // -0.2, -0.1, 0, 0.1, 0.2
    // Base it on the current index and then the positions around it.
    // In most cases the bump will be TARGET - IDX
    let bump = TARGET - IDX
    // If we're in the top half and need to wrap around
    if (IDX >= BOXES.length - 2 && TARGET < IDX && TARGET < 2) bump = 2
    if (IDX >= BOXES.length - 1 && TARGET === 0) bump = 1
    // If we're in the lowers and need to wrap back
    if (IDX <= 2 && TARGET > IDX && TARGET >= BOXES.length - 2) bump = -2
    if (IDX < 1 && TARGET === BOXES.length - 1) bump = -1
    if (Math.abs(bump) <= 2)
      scrollToPosition(SCRUB.vars.position + (1 / BOXES.length) * bump)
  }
});

// NEW
document.querySelector('.boxes').addEventListener('click', e => {
  const BOX = e.target.closest('.box')
  if (BOX) {
    const event = e.changedTouches ? e.changedTouches[0] : e;
    let position = iteration + (1 / BOXES.length) * BOXES.indexOf(BOX);
    if (event.pageX < window.innerWidth / 2 && position > SCRUB.vars.position) {
      position -= 1;
    } else if (event.pageX > window.innerWidth / 2 && position < SCRUB.vars.position) {
      position += 1;
    }
    scrollToPosition(position);
  }
});

Basically I calculate the natural position (factoring in the current iteration), and then just check to see if it's going in the correct direction. In other words, if the user clicks on the left half of the screen, the position should always be LESS than the current position, and if they click on the right side of the screen, it should be GREATER than the current position. This should work with any number of slots, not just 1,ย 2, or 3.ย 

ย 

Is that what you're looking for?ย 

  • Like 2
Link to comment
Share on other sites

10 minutes ago, GreenSock said:

Here's how I'd do it:ย 


// OLD
document.querySelector('.boxes').addEventListener('click', e => {
  const BOX = e.target.closest('.box')
  if (BOX) {
    // Get the current index
    const IDX = gsap.utils.wrap(0, 10, Math.floor(SCRUB.vars.position * 10))
    const TARGET = BOXES.indexOf(BOX)
    // Can only hit three values
    // -0.2, -0.1, 0, 0.1, 0.2
    // Base it on the current index and then the positions around it.
    // In most cases the bump will be TARGET - IDX
    let bump = TARGET - IDX
    // If we're in the top half and need to wrap around
    if (IDX >= BOXES.length - 2 && TARGET < IDX && TARGET < 2) bump = 2
    if (IDX >= BOXES.length - 1 && TARGET === 0) bump = 1
    // If we're in the lowers and need to wrap back
    if (IDX <= 2 && TARGET > IDX && TARGET >= BOXES.length - 2) bump = -2
    if (IDX < 1 && TARGET === BOXES.length - 1) bump = -1
    if (Math.abs(bump) <= 2)
      scrollToPosition(SCRUB.vars.position + (1 / BOXES.length) * bump)
  }
});

// NEW
document.querySelector('.boxes').addEventListener('click', e => {
  const BOX = e.target.closest('.box')
  if (BOX) {
    const event = e.changedTouches ? e.changedTouches[0] : e;
    let position = iteration + (1 / BOXES.length) * BOXES.indexOf(BOX);
    if (event.pageX < window.innerWidth / 2 && position > SCRUB.vars.position) {
      position -= 1;
    } else if (event.pageX > window.innerWidth / 2 && position < SCRUB.vars.position) {
      position += 1;
    }
    scrollToPosition(position);
  }
});

Basically I calculate the natural position (factoring in the current iteration), and then just check to see if it's going in the correct direction. In other words, if the user clicks on the left half of the screen, the position should always be LESS than the current position, and if they click on the right side of the screen, it should be GREATER than the current position. This should work with any number of slots, not just 1,ย 2, or 3.ย 

ย 

Is that what you're looking for?ย 

ย 

Yeah, that was kinda what I was looking for and wanted to avoid doing the "Am I clicking the left/right half of the track" part ๐Ÿ˜…

ย 

I came up with this in the end. It seems to scale well when I change the number of boxes, etc.

ย 

document.querySelector('.boxes').addEventListener('click', e => {
  const BOX = e.target.closest('.box')
  if (BOX) {
    let TARGET = BOXES.indexOf(BOX)
    let CURRENT = gsap.utils.wrap(
      0,
      BOXES.length,
      Math.floor(BOXES.length * SCRUB.vars.position)
    )
    let BUMP = TARGET - CURRENT
    if (TARGET > CURRENT && TARGET - CURRENT > BOXES.length * 0.5) {
      BUMP = (BOXES.length - BUMP) * -1
    }
    if (CURRENT > TARGET && CURRENT - TARGET > BOXES.length * 0.5) {
      BUMP = BOXES.length + BUMP
    }
    scrollToPosition(SCRUB.vars.position + BUMP * (1 / BOXES.length))
  }
})

Love that Draggable way of doing it! The proxy method popped up before when I made the light bulb tug and wondered if something similar could be done here ๐Ÿ‘ That works nice on mobile and desktop ๐Ÿ™Œย 

ย 

Thanks!

ย 

  • Like 1
Link to comment
Share on other sites

  • 1 year later...

Howdy y'all! ๐Ÿ‘‹

ย 

Had a comment on CSS Tricks today about the article for this technique:ย https://css-tricks.com/going-meta-gsap-the-quest-for-perfect-infinite-scrolling/

ย 

Has something changed dramatically between versions to introduce these breaks? Or is there a property I need to update or add potentially?

ย 

The other solution I could see is pinning the GSAP version in all the demos for that article to ~3.7.0 I think ๐Ÿค”

Link to comment
Share on other sites

Hi Jhey,

ย 

There have been some issues in 3.11 that the team has recently ironed out.

Please try this latest beta of 3.11.2. I'm sure the team will be interested to hear the results.

ย 

ย 

https://assets.codepen.io/16327/gsap-latest-beta.min.js

ย 

ย 

If there are still problems, probably best to lock it in to version 3.10 or whatever works best.

ย 

That latest beta url should only be used as a short term fix as it could potentially break sometime down the road.

ย 

Pretty sure the plan is to get 3.11.2 out as soon as possible

  • Like 1
Link to comment
Share on other sites

Yep, very sorry about any confusion there, @jh3y! We made some big changes under the hood for the fancy new gsap.matchMedia(), gsap.context(), and revert() functionality in 3.11 which caused a couple of unintended regressions. Those should be resolved in the next releaseย which you can preview at:ย 

Hoping to push 3.11.2 out ASAP. Just wrapping up some tests and final tweaks, but let us know if you notice any odd behavior with those preview files.ย 

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