Jump to content

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


  • Posts

  • Joined

  • Last visited

Posts posted by jh3y

  1. On 4/2/2022 at 10:19 PM, GreenSock said:

    I'm not entirely sure what you mean by "...it would update the value and inertia would be triggered too..." but you could definitely use Draggable on a custom handle and just update the input.value in the onDrag, for example. And yes, you could have it track the inertia/velocity of the custom handle and do almost exactly the same thing we're doing in the simplified example, but you'd be animating the handle instead and then in the modifier you could apply the new value to the input.value. 


    Does that answer your question? 

    Yeah, I _think_ we mean the same thing 😅


    My terminology may be slightly off but I'm pretty sure we are on the same frequency there


    Thanks Jack!


    ʕ •ᴥ•ʔ/

  2. 19 hours ago, GreenSock said:

    Hey @jh3y. Just making sure everything was clear and helpful. Are you all set? There are many ways you could accomplish this - I was just providing the way that seemed clearest in my brain but that's just one option. If you need help with anything else, just let us know. 


    Lookin' forward to the article!

    Hey Jack 👋

    Sorry – I've taken a couple of days off before starting a new role this coming week. That solution is super clear and I love it! It's a great spin on the article too for showing learning process and how we've got to this solution. Kinda like the "Meta GSAP" article (Which could totally be redone with Observer now, right? Awesome!)


    I really like this solution because of it's simplicity. If I were to put another element such as a custom drag handle, and use Draggable for that bound to the input like I had originally, it would update the value and the Inertia would be triggered too, right? I think that's correct. I may have not worded it right but I'm pretty sure that works too, haha.


    Can't wait to finish off the article! \ʕ •ᴥ•ʔ/


    Thanks again for everything!

    • Like 1
  3. 59 minutes ago, Cassie said:

    It certainly won't be removed.

    It's actually part of the functionality of ScrollTrigger, we originally just planned to surface it because it's a bit too useful to be tucked away under the hood but folks asked if it could be it's own plugin. So now if people using ScrollTrigger want to grab some extra event/delta info while using without adding extra kb's they can - but if people aren't using ScrollTrigger they can load a separate plugin. 

    This makes perfect sense. I'll get that other demo fixed up to use the new API too


    Thanks! \ʕ •ᴥ•ʔ/

    • Like 1
  4. 12 hours ago, GreenSock said:

    This is actually a good use case for our brand new Observer tool (announcing it broadly tomorrow): 





    Nice and simple, right?

    This is great! And perfect timing 😅 And will be great for the article too!


    I could totally make this into an "Observe" demo. I'm assuming this is the tool that has been "renamed" from something else that I may have made a demo for?


    Thanks Jack! 🙏


    \ʕ •ᴥ•ʔ/

    • Like 2
  5. 9 minutes ago, GreenSock said:

    Hey @jh3y! Sorry about the delay - been swamped with launch stuff. Super excited about 3.10!


    Anyway, I think you could greatly simplify things, cutting probably 70%+ of the code 👍




    The main problem was that inertia tracking by its very nature is time-dependent, meaning it sorta keeps track of a certain amount of history so that it can do the calculations. You were creating a scenario where you inverted the velocity via a tween, but it took a little time for that to actually get reflected in the tracked velocity (as it should). So let's say it's moving super fast in one direction, so maybe 3000px/s and then you suddenly start moving it in the opposite direction at half the speed, but it's taking data points once every tick and must average them out - if it hits the other limit quickly enough, it'll still have some historical data from when it was going 3000px/s that offsets things. So in your case, that'd result in it still being a positive velocity and you were multiplying it by a negative, thus heading in the same direction as before.


    Sure, we could implement fancy workarounds to adjust for that but I thought it'd be much cleaner to simply do a single tween with a modifier that uses a wrapYoyo() utility function. No Draggable, no proxy, no checking for collisions on every tick. Faster, cleaner, and more succinct. Oh, and I changed the range step to 0.05 for more accuracy. You could easily just snap it in an onComplete or whatever if it's important to have a step of 1. 


    It's a similar approach to the one used in this old thread: 


    Does that clear things up? 

    Oh wow. That is much smaller. And I like that wrapYoyo! TIL.


    The only issues I see here are:

    - I want to plug into the "collision" so I can play a noise and bump the input itself from left to right based on the velocity too. Can you detect when a wrap happens inside "onUpdate"?

    - If I click the track, it applies the velocity which I wouldn't necessarily want as I want that to only happen after a "drag".


    With this original demo, it has the "correct" behavior. But, it has the drawback of me trying to keep that element in sync with the slider thumb. I was thinking with the "Draggable" way, I'd be able to easily keep a "faux" thumb in place by updating it's position based on the input value. But, that isn't an important part here. Just something I was thinking of doing.


    Thanks for teaching me something new! Again! 🙏 The best part being that I've kinda written an article using Draggable. But, it's kinda cool that I can steer it in a different direction. I love mentioning the forum powers!


    Jhey ʕ •ᴥ•ʔ

  6. Hey y'all! 👋


    I've been playing with this demo some more and attempting to create it without a "proxy" element in the DOM. It works for the "most" part but the velocity seems a little off with the Inertia compared to the one above.


    But, that's not the main issue I'm encountering. The issue is calculating the bounce back based on the value of the input. If I animate the value of the input or drag it so that it should bounce, it gets stuck on the end as if other tweens are hanging on if that makes sense? It's like it can't keep up with itself or the tracker velocity is hanging. For example, if you bounce it off one end where you expect it to bounce off the other, it hangs. Any ideas @OSUblake && @GreenSock


    See the Pen abEWPOo by jh3y (@jh3y) on CodePen


    Look forward to seeing what you think on this one. Could it be the way I'm using the `inertia`? Previously, this was set on `x` because I was moving an element. Would I instead need to map this to the input values perhaps?


    Jhey ʕ •ᴥ•ʔ

  7. On 3/11/2022 at 7:26 AM, OSUblake said:

    Hey Jhey,


    I would probably just put an event listener on the input and use startDrag.




    You might be able to do the same thing with Draggable by using the input as the trigger, but I just quickly threw this together. You're probably going to have to tweak it to work your sync function and whatnot, but it's just to give you an idea.






    Hey y'all! 👋


    Looks like `startDrag` was the piece of magic I was looking for 🙌 Thanks for sharing that one. I think this solves the issue of trying to align the slider proxy handle with the slider thumb because wherever you click, it will align, which is perfect


    On 3/11/2022 at 7:49 AM, GreenSock said:

    Hey @jh3y! Nice seeing you around the neighborhood. 🙌


    I'm not sure it needs all that code in the "pointerdown", Blake, although I very well may be missing something (it's super late and I'm brain-dead) - couldn't it be this simple?: 




    I didn't have time to pour over all your other code, @jh3y - were you asking for input about how to structure things differently? 


    Happy to be here! Thank you for both of your inputs. Love learning new parts of the API to solve these little challenges 💚 TIL "startDrag" 💪


    Now I can start writing this one up 😅


    Thanks again! \ʕ •ᴥ•ʔ/

    • Like 5
  8. Hey y'all!


    I've been thinking to write something up about this demo. But, I feel like there's a more robust way for me to handle dragging on the input regardless of where the slider thumb is 🤔


    I've tried putting in place a "Proxy" element (.slider__proxy) and I'm updating based on that. However, if you were to click the track somewhere away from the thumb, would it be possible to somehow trigger "drag" mode and move the slider thumb and proxy into place? I feel like I'm overcomplicating it somewhat seeing as a range input already has a drag handle kinda built in.


    But yeah, I was keen to know if there was a better approach to what I've done with it.


    Jhey ʕ •ᴥ•ʔ

    See the Pen podVRxw by jh3y (@jh3y) on CodePen

  9. Hey y'all!


    On 1/28/2022 at 4:34 PM, Cassie said:

    Always nice to see you in the forums Jhey.  ☺️

    Thank you @Cassie 🙏


    One more query on this demo 😁 I've got it pretty much there where I want it. There's one thing I can't seem to make work nicely. It's setting `aria-hidden` on the numbers as they slide in and out 🤔


    I've been trying this but it is a little inconsistent for me. Sometimes it works, sometimes it leaves some with the wrong values. Would it make sense to maybe do this outside of the timeline and work out which digits need the attribute applied when doing the scrub instead? (Maybe I've answered my own question there 😅)


    const DIGI_TIMELINE = gsap
          .set(DIGIT, {
            yPercent: 0,
            attr: {
              'aria-hidden': 'true',
          .to(DIGIT, {
            yPercent: 100,
            delay: i * COEFF,
            duration: 1,
            onStart: () => {
              gsap.set(DIGIT, {
                attr: {
                  'aria-hidden': 'false',
          .to(DIGIT, {
            delay: COEFF - 1,
            yPercent: 200,
            duration: 1,
            clearProps: 'all',
            onStart: () => {
              gsap.set(DIGIT, {
                attr: {
                  'aria-hidden': 'true',

    Anyways, hope you've all had an awesome weekend. Here's the demo! It's part of a series I'm doing where it's about debunking Instagram Reels/TikTok where someone shows how easy it is to make a design in Figma but it doesn't show the reality on the dev side of making said thing 😅


    See the Pen XWzmGad by jh3y (@jh3y) on CodePen




  10. 19 hours ago, GreenSock said:

    I love that you don't mind asking questions here, @jh3y! It shows that advanced users don't need to be bashful. 🙌


    Just a few thoughts/comments for what it's worth (maybe not much)

    1. You never need to set paused: false or delay: 0 on a timeline/tween (those are the defaults)
    2. Instead of using an onComplete to clearProps, you could just put that in your final tween. No biggie - maybe it was easier for you workflow-wise to use the onComplete.
    3. I'm not sure what you were trying to accomplish by using '- 0.5' as the position parameter. As it stands now, that's merely acting as a label (a very oddly-named one): 
      // since it's  a string...
      TL.add(DIGI_TIMELINE, '- 0.5')
      // ...it's acting the same as something like:
      TL.add(DIGI_TIMELINE, 'someLabel')
      // if you want it -0.5 seconds from the end:
      TL.add(DIGI_TIMELINE, "-=0.5")

      You could simplify things:

      // OLD
      const DIGI_TIMELINE = gsap.timeline({
        paused: false,
        onComplete: () => gsap.set(DIGIT, { clearProps: true })
        .set(DIGIT, {
        yPercent: 0,
        .to(DIGIT, {
        yPercent: 100,
        delay: i,
        duration: 1
        .to(DIGIT, {
        yPercent: 200,
        duration: 1
      TL.add(DIGI_TIMELINE, '- 0.5');
      // NEW
      const DIGI_TIMELINE = gsap.timeline()
        .set(DIGIT, {
          yPercent: 0,
        .to(DIGIT, {
          yPercent: 200, // since it's linear, there's no need for the prior tween to 100
          duration: 2,
          clearProps: "all"
        }, i)
      TL.add(DIGI_TIMELINE, 0);


    4. FYI, that tween wasn't rendering at the initial state because you had set paused: true on the timeline thus that's an indicator to GSAP that you don't want it rendering right now. But like Blake said, you can just .progress(1).progress(0) to force the playhead/render. Plus that also forces everything to get initialized up-front, so it'll be able to skip those tasks when it runs.

    We love seeing all the cool stuff you've been putting out there and tweeting about. Keep up the good work inspiring people!

    Not at all! I love some humble pie ☺️ It's the best way to learn. I'll always take suggestions and discussions.


    1. Totally! 💯 Sorry, the code is a little rough and I'd left them in as I was playing with variations trying to get it working with things like `delay: 2` etc. Sorry for that!

    2. Ahh yes, that would make sense too. I guess in my head I see each timeline like its own little block/animation component 😅

    3. Yes! Love this. Now the position param threw me to start with because I was in a bit of a rush. Without a label it broke and of course I needed `0` 😅. The simplified version is much nicer and again this was kind of my bad, sorry. I'd left it rough as I was playing with different eases for the effect I want. Ahh, I see that `clearProps` usage now and this is much nicer too!

    4. This is a rad tip. Kinda shows the magnitude of what GSAP covers because although I've used `progress` in the past, I'd completely forgotten about it haha. Trying to spend more time documenting things I make this year for this reason 😅


    Appreciate the kind words massively! I'll keep creating for sure. Love sharing and challenging ha. If I don't hear from you, have a great weekend!


    \ʕ •ᴥ•ʔ/

    • Like 3
  11. Think I've worked out a way.


    Needed another timeline to scrub the playhead of `MANIPULATOR`. Then I'm using a `delayedCall` to get the ball rolling


    const SCRUB = gsap.to(MANIPULATOR, {
      totalTime: 0,
      duration: 0,
      paused: true,
      ease: 'none'
    SCRUB.vars.totalTime = 4
    gsap.delayedCall(2, () => MANIPULATOR.play())

    There is likely a cleaner way to do this. The idea being that I'm going to be scrubbing a timeline composed of a few timelines and need to show the current state. I'll keep tinkering 🤓

  12. Hey y'all 👋


    Looping through a set of numbers infinitely by tweening `totalTime` on a timeline. But, how do I show the current state while it's `paused`?


    I'm setting `TL.totalTime(1)` here but nothing shows. Do I need to do something with `immediateRender`? I'm going to be scrubbing this timeline with another control so it will remain paused. I've tried the `invalidate().restart()` manoeuvre but I'm not having much luck...


    import gsap from 'https://cdn.skypack.dev/gsap'
    const TL = gsap.timeline({
      paused: true,
    const DIGITS = gsap.utils.toArray('.digit')
    const PADDED_DIGITS = [...DIGITS, DIGITS[0]]
    for (let i = 0; i < PADDED_DIGITS.length; i++) {
      const DIGIT = PADDED_DIGITS[i]
      const DIGI_TIMELINE = gsap.timeline({
        paused: false,
        onComplete: () => gsap.set(DIGIT, { clearProps: true })
        .set(DIGIT, {
          yPercent: 0,
        .to(DIGIT, {
          yPercent: 100,
          delay: i,
          duration: 1
        .to(DIGIT, {
          yPercent: 200,
          duration: 1
        TL.add(DIGI_TIMELINE, '- 0.5')
    const MANIPULATOR = gsap.timeline({
      delay: 2,
      paused: true,
      .fromTo(TL, {
        totalTime: 1,
      }, {
        totalTime: 11,
        duration: 4,
        ease: 'none',
        repeat: -1,

    Thanks in advance! Sure I'm missing something obvious.


    ʕ ᵒ ᴥ ᵒʔ


    Setting `MANIPULATOR` to `paused: false` plays the timeline as expected. But, I'd like to see the playhead state when it's paused.

    See the Pen OJOPdQw by jh3y (@jh3y) on CodePen

  13. 11 minutes ago, GreenSock said:

    Howdy, @jh3y! I always love seeing your ambitious endeavors. 


    Yeah, Flip.fit() instantly does its measurements - it's not as if it re-measures everything each time the tween plays or something. That'd kinda suck for performance, plus it wouldn't be a "true" replay if timelines shifted around their values and animated to new things each time they ran, you know? 


    You could, of course, rework your animation logic so that it's literally calling functions that run the Flip.fit() stuff at the right times (like in a timeline or whatever). Or (better yet) you could drop all your timeline-creation stuff into a simple function that runs on "resize" events, and basically reverts everything and re-creates all those Flip.fit() calls at that time. You could store the current progress() of the animation, then revert, rebuild, and jump to that recorded progress value all in the "resize" event handler. 


    If you still need some help, it'd be super duper amazingly helpful if you provided a minimal demo that only focuses on the particular challenge you're facing (the demo you provided has like 60+ flip calls, ScrollTrigger, and lots of other things going on). :)


    Happy tweening!


    Hey Jack @GreenSock


    Yep, I never assumed it would be recalculating on each replay 😅 It would work similar to other plugins, etc.


    My timelines were being generated in a function as were my flips 👍


    But, that's the part that had tripped me up. The window resizes, I kill the timeline, and regenerate it within a debounced handler.


    debounce(() => {
        // Timeline that was generated with a function
        // Clear all the Flip setting of initial positions
        gsap.set('li', { clearProps: 'all' })
        // Set them all again to the new positions of the things to Flip to.
      	// Regenerate the timeline.
        MAIN = genFlips()
        // ScrollTrigger stuffz
        window.scrollTo(0, 0)
        gsap.delayedCall(1, TRIGGER.enable)
    }, 250)

    Storing the progress wouldn't be necessary as ScrollTrigger's refresh would assign the scrub to the correct position on the timeline 👍


    But, what was missing was regenerating the loop head for ScrollTrigger to scrub the time. All I needed was to kill the loop head and regenerate it 👍




    See the Pen XWpvyxr by jh3y (@jh3y) on CodePen

    • Like 4
  14. Hey y'all!


    Quick one with this pen. Using ScrollTrigger to scrub a timeline that uses FLIP to shift in and out the different sections. On window resize, how can you reset the `FLIP` positions?


    I've tried a couple of different things but can't seem to find the one that should work and have left it as is with broken resizing 😅


    - The `resize` handling is at the bottom. 

    - `MAIN` is the timeline used for the FLIP transitions.

    - `li` are all the list items being FLIPped.


    I did think this was something with the ScrollTrigger. But, I think somewhere the FLIP settings get cached perhaps and I'm looking for some way to flush them perhaps?


    Timing and snapping are out of scope here. But, I did have a bit of an issue getting them nailed but I don't think I'm quite sure how I wanted this one to feel on scroll. It was purely an idea I wanted to see working with FLIP 😁


    See the Pen XWpvyxr by jh3y (@jh3y) on CodePen

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

    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(
          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 🙌 




    • Like 1
  16. 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 😅

  17. No worries. That was kinda what I was looking to find out with opening this thread 😁 "Was I missing something straightforward?". I haven't really played with nested scroll and ScrollTrigger 😁


    I don't think I knew that there were two different pin styles. I think I assumed that `position: fixed` was already being used, not that it was being kept in place by `transform` which I should've noticed really 😅


    As you say, `position: absolute` will do the trick and means I don't need to set anything on the child element 👍



  18. Hey @GreenSock 👋


    The use case was about being able to pin something in a container that can be moved around. For when there's nested scrolling and I want to pin something in the container with nested scroll.


    I'm relying on CSS here where elements fix relative to the container if it has a transform or position relative. The idea is that it wouldn't be unpinned in the container kinda like the infinite scroller.


    It was more a case of wondering how this might be done. For example, I anticipate the content scroller might want to be put in its own container and moved around without relying on body scroll. Didn't know if there was an appropriate approach. But, the pen I posted seems to work for my needs 👍

    • Like 1
  19. 15 minutes ago, mikel said:

    Hey @jh3y,


    Could this be an alternative?





    Happy scrolling ...



    Hey 👋


    Yeah, that's almost the same approach which confirms it to me 👍

    `position: relative` has the same effect as applying a `transform` on the container for `fixed` children.


    That demo you provided works with the `padding` removed and `height: 100%` set on the `#container`. In its current form, the green box will scroll up towards the end of the container 👍


    But, thank you, this confirmed my thinking in how to approach it 🙏 I have a sneaky suspicion there will be some requests to contain the scrolling demo I've been working on into portable containers and wanted to make sure I had the right idea for doing this before going forwards 👍


    Thanks again!



  20. Howdy Y'all! 👋


    This may be the way to do it or there may be a cleaner way 😅


    But, if you want to `pin` something inside a `scroller` element, is this the way to do it?


    const TRIGGER = ScrollTrigger.create({
      horizontal: false,
      trigger: '.scroller',
      pin: '.panel',
      end: '+=2000',                          
      scroller: '.scroller',
      onUpdate: self => {
        // Doing anything here for hypotheticals 😅
        gsap.set('.panel', {
          rotate: self.progress * 360,

    The "trick" I'm using is that if you use CSS `transform` on the container, that will scope `fixed` to the container. Or is there another API way of doing this?

    See the Pen eYgJJQg by jh3y (@jh3y) on CodePen