Jump to content
Search Community

Changing x: random value on device width - matchmedia or window.innerWidth

kabocreative test
Moderator Tag

Go to solution Solved by GreenSock,

Recommended Posts

Hello!

I'm using match media to change the x value in two tweens in the t1 timeline based on screen width, from: x: "random(-100, 100)" to x: "random(-300, 300)" as commented in the codepen.

The Problem: on screen resize the only way to load the correct x values is to reload the screen. I've seen posts on killing and restarting, but I've no idea how to do that in my case.

 

Ideal fix: I loved the idea of changing x pixel values to:

 

x: "random(window.innerWidth -40%, window.innerWidth 40%)"

This would avoid repeating so much code in the media queries, but I could not make that work.


Alternative fix: I looked at ScrollTrigger's match media as it would solve the resizing issue, but couldn't as one instance of the x value is in a loop.
 

I feel my code is a big behemoth and there must be a neater way of changing the x value in those two places by media query than to repeat the whole thing again... or if that is the best way any hints on how to kill/restart the animation on screen resize is greatly appreciated!

I've hit up the forums 3 times in 3 days, noted that's quite cheeky. Promised last post on this animation which is pretty much complete then I'm moving on to MUCH simpler ones while I pick up the ropes.
 

See the Pen GRxENyy by kabocreative (@kabocreative) on CodePen

Link to comment
Share on other sites

Are you using a functional value?
 

x: () => {
	const random = gsap.utils.random(smallestVal, largestVal)
	return (random - window.innerWidth * 0.4)
}


Also, if you want your scrollTrigger to reset on resize provide it invalidateOnRefresh: true though this may have some side effects if you didn't design your inner animations for it.

  • Like 1
Link to comment
Share on other sites

Another thing to consider: 

 

Instead of an infinitely looping animation that has repeatRefresh, you can just use a recursive function like this: 

// OLD (not responsive)
let loop = gsap.to(el, {x: "random(-100, 100)",  y: "random(-50, 50)", ease: "none", duration: 2.5, repeat: -1, repeatRefresh: true});

// NEW
let random = gsap.utils.random;
function wander(el) {
  gsap.to(el, {
    x: random(window.innerWidth * -0.4, window.innerWidth * 0.4),  
    y: random(-50, 50), 
    ease: "none", 
    duration: random(2, 2.8),
    onComplete: () => wander(el)
  });
}
gsap.utils.toArray(".class").forEach(wander);

Not only does this make things responsive (at least on the next iteration of each animation), but it also lets you randomize the duration so that things look more fluid (all the circles aren't changing direction at the same time). You could randomize a little delay in there too quite easily. 

 

There are ways to force the timeline to use new values when the screen resizes, but I suspect you'll find the solution above preferable. If you still need help forcing the timeline to use new values, let me know. 

  • Like 1
Link to comment
Share on other sites

  • 3 weeks later...

Hi again! Finally had time to return to this.

Jack - big thanks for your help, I much prefer the idea you've presented. I'm having two issues with implementation which no amount of googling, docs and forum searching has resolved.

 

Issue #1

See the Pen jOzpxaG by kabocreative (@kabocreative) on CodePen

 

I can't work out how to make the new function play at the end of the timeline. With the loop I added it at the end of the timeline, but this function seems to play immediately regardless of where I place it.

 

Issue #2

See the Pen poLZVWd by kabocreative (@kabocreative) on CodePen

 

When I add the function in place of the loop in the full project I'm having this odd effect causing it to look very 'jumpy' or abrupt in direction change.

This doesn't apply in my simplified codepen above (which is much smoother in direction change), so it seems to be related either to having the second timeline or having the mouseenter function, but I can't identify what it is causing it.

Again thanks so much for your help!!

Link to comment
Share on other sites

16 hours ago, kabocreative said:

I can't work out how to make the new function play at the end of the timeline. With the loop I added it at the end of the timeline, but this function seems to play immediately regardless of where I place it.

That's because you did call it immediately :) Just because the code is below your timeline declaration doesn't mean that it'll wait to execute that code until the timeline completely finishes playing. Think of all your timeline code like an initial setup which happens immediately, and then it'll start running over the course of the next many requestAnimationFrames. 

 

So all you need to do is toss it in an onComplete on the timeline: 

onComplete: () => gsap.utils.toArray(".animation-container").forEach(wander)

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

 

17 hours ago, kabocreative said:

When I add the function in place of the loop in the full project I'm having this odd effect causing it to look very 'jumpy' or abrupt in direction change.

That's because you're looping through all 5 ".animation-container" instances and EACH time through that loop, you're doing this: 

gsap.utils.toArray(".animation-container").forEach(wander);

Which loops again through all 5 of them, starting an independent recursive wander routine. So you're creating a ton of conflicting tweens. Instead of creating one for each, you're creating 5 for each. So the jerking is caused by the fact that the animations keep getting interrupted by each other (they're all fighting for control). 

 

I think you meant to do this:

// OLD
gsap.utils.toArray(".animation-container").forEach(wander);

// NEW
wander(animationContainer.querySelector(".circle"))

But slap that into an onComplete as discussed above. Also, you could reduce your code a lot by just creating a variable at the top for the circle instead of using that querySelector() every time, and you don't have to recreate the wander function in each loop: 

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

 

Does that help?

  • Like 3
Link to comment
Share on other sites

Hi Jack,

 

Again massive thanks. That does help a lot. 90% of it makes sense, and the remaining 10% shows that I've still got some learning to do! :)

I've almost got it working perfectly now... in your codepen above it works beautifully until you scroll down and back up again.

 

At that point the ScrollTrigger does exactly what it should pauses, restarts and plays the timeline again, but the Wander function keeps running as it isn't part of the timeline.

 

I want the onComplete to respect the ScrollTrigger rules - i.e. play pause restart pause

I've tried for example:

 

 scrollTrigger: {
        trigger: ".container",
        markers: true,
        start: "top 75%",
        end: "bottom 25%",
        toggleActions: "play pause restart pause",
        onLeave: () => wander.pause()
      },
      onComplete: () => wander(animationContainer.querySelector(".circle"))

The idea was to pause Wander within ScrollTrigger, but no joys.

I've been looking at eventCallback, i.e. "onComplete", "onUpdate", "onStart", "onReverseComplete" but there's no 'on leave' style option here to pause that animation.

I also looked at wander.kill but again I don't have an option for 'on leave' to use it.

Is there a way to make wander be part of the t1 timeline so it can respect the toggle actions?

Link to comment
Share on other sites

Heya!

 

So sometimes it's good to step back from the syntax and the tech and just think about the logic of what you're trying to achieve. (at least it is for me!) Adding an infinite animation to a scrollTriggered animation that has a finite distance to play in isn't going to work - that's a logical impossibility.

  • You need a way to pause the 'wander' animation at certain toggle points. 
  • In order to do that you need to be able to access and control the tween that is controlling the wandering.  
  • To do that, the tween needs to have a name so you can tell it to do things.


Right now you just have an unnamed tween inside the wander function. You can't call pause() on that function - you have to call pause on the tween itself.

If you move the wander function inside the circles loop, you can create a named tween for each circle and then control it. (There's likely a way to do this while keeping that function global but I'm not quite sure how.)

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



I also noticed when you 'get rid' of the circles in the timeline at the end, the animation keeps playing, even though you can't see it. That's going to be bad for performance so I recommend killing the wander tween at that point.

FYI - This is a quick 'rough' idea - I would suggest going through and logging out wanderTween at different steps and thinking through the logic. There are lots of different things happening to these dots and they could happen in different orders,

e.g. What happens if the hover happens before the wander tween has been created?

You'll need to work out what combinations of steps could exist and then make sure you have conditional logic in place to handle it! It helps me to get a pen and paper out and draw/write out the logic for stuff like this.

 

  • Like 3
Link to comment
Share on other sites

  • Solution

One little tip in addition to all of Cassie's great advice: 

You can just add a property to the element itself to store a reference to the wander tween, sorta like: 

function wander(el) {
  el.wanderTween = gsap.to(el, {
    x: random(window.innerWidth * -0.4, window.innerWidth * 0.4),
    y: random(-50, 50),
    ease: "none",
    duration: random(2, 2.8),
    onComplete: () => wander(el)
  });
}

So that you can easily control it anytime, for example: 

circle.wanderTween.pause();

I hope that helps.

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

Jack and Cassie thank you!

Cassie - really good points especially on drawing out the processes. I started that way and made great progress, then I seemed to get a bit lost... Taking that step back would have been sensible! I'll try to remember this next time I'm falling down a rabbit hole.

Jack - this bit is EXACTLY what I wanted to do but had zero idea how to get there:

 

let t1 = gsap
      .timeline({
        delay: 0.8,
        scrollTrigger: {
          trigger: ".container",
          start: "top 75%",
          end: "bottom 75%",
          toggleActions: "play pause restart pause",
          markers: true,
          onToggle: (self) => {
            if(!self.isActive && typeof wanderTween !== "undefined") {
               wanderTween.pause()
            }
          }
        },
        onComplete: () => wander(animationContainer.querySelector(".circle"))
      })

 

I completely missed onToggle when searching through the docs for answers. In honesty even if I'd found it I wouldn't have been able to write something as neat and succinct as you have to get it all together, but now I understand how you've solved it I'm sure this will help me next time I hit a challenge like this.

Big thanks again. The final animation is now on the staging site working perfectly. And I'm working on much simpler animations elsewhere while I learn the ropes. This one was a doozy :D

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