Jump to content
GreenSock

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

[solved] ScrollTrigger.matchMedia issues with multiple instances

Recommended Posts

Hi,

 

I have a question regarding using ScrollTrigger.matchMedia in multiple places within a web app (which will include using the same breakpoints in multiple places)

 

To paint the scenario for you, so you have some more context.

 

I have a Nuxt.js web app, that is using ScrollTrigger for various animations, which are setup within individual components throughout the app, which allows me to only create / destroy ScrollTrigger and gsap instances where needed to keep things nice and tidy.

 

I noticed in the video tutorial for ScrollTrigger.matchMedia, that declaring this object once seems to be the recommended way, and then using the media queries as keys - e.g '(min-width: 800px)' pointing to a function which would handle ALL the ScrollTrigger instances for each breakpoint.

 

My question is, is there a specific way that I should be using ScrollTrigger.matchMedia() within a component, and setting up the gsap / scrollTrigger animations only related to that component.

 

I have been playing around with this for the last few hours, and I keep running up against issues, as I am presumably using it incorrectly.

 

UPDATE: Small Codepen example in the below reply.

 

Apologies in advance as I haven't included any specific code in this post. I am just seeing if there is a simple way of achieving this with ScrollTrigger.matchMedia, or if I need to setup something a bit more custom to acheive this.

 

If its better for me to setup a small repo / codesandbox with a simple example showing exactly what I mean, let me know and I will reply to this post with it :)

 

P.S Just wanted to say that ScrollTrigger is the absolute bomb, and I've been using it since the day it launched!

 

Thanks in advance!

Share this post


Link to post
Share on other sites

See the Pen BajPRBQ by pxel (@pxel) on CodePen

 Here's a quick pen I did up to show you what I mean. I feel like this is incorrect usage of the ScrollTrigger.matchMedia() method, but this is essentially the result I am after... Being able to run multiple ScrollTrigger.matchMedia methods on 1 page (based on whichever components are loaded into a page)

 

I noticed on initial load Component 2's animation won't work, but resizing the window down (below 900) and back up again seems to make Component 2's animation work.

Thanks again!

Share this post


Link to post
Share on other sites

Yeah, you just do one matchMedia() call and put all your code in there. 

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

 

The problem with the way you're doing it is that there's one function per key, and you're basically duplicating keys. In other words, '(min-width: 900px)' is already associated with a setup function. I never even thought of someone trying to duplicate things like that. :)If there's enough demand for it, we could probably add more code to ScrollTrigger to accommodate that. 

  • Like 2

Share this post


Link to post
Share on other sites

Hey Jack - Thank you for your reply!

 

Ok, in saying that is there a way you would recommend implementing matchMedia() on a Single Page App (with javascript based routing) like Nuxt.js for example?
 

I can get my head around having one matchMedia() call, what I am trying to get my head around is having the contents within each breakpoint be dynamic or interchangable, or some kind of lifecycle where I can destroy and recreate it between route changes.

For example:

 

a )I setup matchMedia() (Global component)

b) My header may utilise ScrollTrigger, where matchMedia() is required (Global component)

c) My home page may have 3 'sections' utilising ScrollTrigger, where matchMedia() is required (Page component)

d) My about page may have 2 different 'sections' utilising ScrollTrigger, where matchMedia() is required (Page component)

 

So that means, on the home page matchMedia() would include animations for my header, and home page x 3 sections.

 

If i navigate to the about page, I would want to (destroy & rebuild / reinitialise) matchMedia() to include animations for my header, and about page x 2 sections.

 

I hope that makes sense!

If you think matchMedia() is the wrong tool for the job here, and something custom be required, that's totally understandable :)

 

Thanks again for the help, I really appreciate it!

 

 

Share this post


Link to post
Share on other sites

For anyone interested, I found a solution for this that seems to work quite well.

 

Here is the code from my vue component that acts like a basic event bus, I emit 'animations' that I would like to have be responsive, and then dynamically build out the matchMedia() method / refresh scrollTrigger whenever there are new animations detected.

 

Note that is is code from Nuxt JS / Vue JS:

 

Code for the component that is listening for the emitted animations:

 

// code snippet from '~/components/ScrollTriggerMatchMedia.vue' component
export default {
    data() {
        return {
            animations: []
        }
    },
    created() {
        // listen for global $emits for new animations
        this.$nuxt.$on('push-animations', (animations) => {
            this.addAnimations(animations)
        })
    },
    methods: {
        // build ScrollTrigger.matchMedia method
        rebuildAnimations() {
            let animations = this.animations
            // get unique key values - e.g:  '(max-width: 900px)', 'all', etc
            let breaks = [...new Set(animations.map(a => a.bp))]
            let breakpoints = {}
            // build out the object for matchMedia with each unique breakpoint as a key
            breaks.forEach(b => {
                breakpoints[b] = () => {
                    // loop through all animations with the same breakpoint
                    animations.filter(a => a.bp === b).forEach((fa, i) => {
                        fa['animation']()
                    })
                }
            })
            // run the ScrollTrigger.matchMedia() method, with our breakpoints object
            this.$ScrollTrigger.matchMedia(breakpoints)
            // refresh ScrollTrigger (using safe mode)
            this.$ScrollTrigger.refresh(true)
        },
        // push animations to component data
        addAnimations(animations) {
            this.animations.push(...animations)
        }
    },
    watch: {
        // rebuild triggered when there are any changes to the animations array inside the component's data
        animations() {
            this.rebuildAnimations()
        }
    }
}

 

Code for the component that is emitting the animation(s):

 

// code snippet from '~/components/Header.vue' component
export default {
    mounted() {
        this.$nuxt.$emit('push-animations', [
            {
                // this.gsapAnimation() returns a gsap.timeline
                animation: () => { this.gsapAnimation() },
                // breakpoint to be grouped into inside ScrollTrigger.matchMedia()
                bp: '(min-width: 900px)'
            },
            {
                // can also inline gsap timelines here
                animation: () => {
                    let tl = this.$gsap.timeline({
                        scrollTrigger: {
                            id: 'scrollTriggerHeader',
                            //trigger: this.$refs['header'],
                            scrub: 0.2,
                            start: 'top top-=30px',
                            end: 'top top-=100px',
                        }
                    })
                    tl.to(this.$refs['logo'], { y: -90, duration: 0.6, ease: "expo.inOut" })
                },
                bp: '(min-width: 900px)'
            },
        ])
    },
    destroyed() {
        // used to kill the ScrollTrigger instance for this component
        this.$ScrollTrigger.getById('scrollTriggerHeader').kill() 
    }
}

 

Some notes here, my setup has a custom plugin that initialises gsap and ScrollTrigger, and binds them to the global vue instance within nuxt. This is how I did that:

 

// code snippet from '~/plugins/gsap.js'
import Vue from 'vue'
import gsap from 'gsap'
import ScrollToPlugin from 'gsap/ScrollToPlugin'
import ScrollTrigger from 'gsap/ScrollTrigger'
gsap.registerPlugin(ScrollToPlugin)
gsap.registerPlugin(ScrollTrigger)

const GSAP = {
    install (Vue, options) {
        Vue.prototype.$gsap = gsap
        Vue.prototype.$ScrollTrigger = ScrollTrigger
    }
}

Vue.use(GSAP)


I hope this can help someone else on their adventures with using ScrollTrigger in a Javascript framework :)

  • Like 1

Share this post


Link to post
Share on other sites

Thanks for sharing your solution!

 

I rolled up my sleeves and worked on accommodating a use case like this, so you can add the same breakpoint multiple times and it should work in 3.4.1. Here's a preview: https://assets.codepen.io/16327/ScrollTrigger.min.js

 

Furthermore, you can optionally return a function that'll be called when the breakpoint is no longer active (teardown). I figured you might appreciate that. 

 

Better?

  • Like 6

Share this post


Link to post
Share on other sites

 

8 minutes ago, GreenSock said:

Thanks for sharing your solution!

 

I rolled up my sleeves and worked on accommodating a use case like this, so you can add the same breakpoint multiple times and it should work in 3.4.1. Here's a preview: https://assets.codepen.io/16327/ScrollTrigger.min.js

 

Furthermore, you can optionally return a function that'll be called when the breakpoint is no longer active (teardown). I figured you might appreciate that. 

 

Better?

 

Awesome!

I like the sound of that 🙌 😁 

 

Thanks so much, you're a true Superhero!

  • Thanks 1

Share this post


Link to post
Share on other sites

Hi @GreenSock  and  @pxel,

 

I found this thread using the search and think the solution would help in my case.

I tried the following with ScrollTrigger.matchMedia() :

 

ScrollTrigger.matchMedia({

    "(min-width: 1025px) and (min-aspect-ratio: 4/3) and (max-aspect-ratio: 2/1)": function() {
        document.documentElement.style.margin = 0;
        document.documentElement.style.height = "100%";
        document.body.classList.add("gsap");

        let tl = gsap.timeline({
            scrollTrigger: {
                trigger: "#container",
                start: "top top",
                end: "+=4000",
                scrub: true,
                pin: true,
                anticipatePin: 1
            }
        });

        let slides = gsap.utils.toArray(".imageText");

        slides.shift(); // remove first slide, because it should be the start slide

        slides.forEach((slide, i) => {
            tl.from(slide, { xPercent: 100, scale: 0.9 });
        });

    },

    "(max-width: 1024px)": function() {
        document.body.classList.remove("gsap");
        document.documentElement.style.margin = 0;
        document.documentElement.style.height = "auto";
    },

});

What I'm trying to do is based on Greensocks Sliding Panels demo, but the timeline and animations are working, thats not the problem, I have a question regarding the breakpoints.

 

As you can see in my example I try to set a certain aspect ratio where scrollTrigger is active.
Since the sliding panels always have 100% viewport height and I want to make sure that the content always fits into the viewport I use the aspect ratio in my media query and assign the needed styling for the slide in panels. Otherwise the panels are normally arranged one below the other.
 

The reason why I give the body the class gsap is that I control the styling of the elements with this class.
The two lines below, I'm adding the styles needed for the html element.
 

Now I want to delete the gsap class and the html stylings if this aspect ratio breakpoint isn't matched anymore.
And I do not know how I could do that. The All Key does not help me here, but maybe the teardown function?

Thanks in advance for your reply and this amazing set of tools. :)

 

Share this post


Link to post
Share on other sites
2 hours ago, emjay said:

Now I want to delete the gsap class and the html stylings if this aspect ratio breakpoint isn't matched anymore.
And I do not know how I could do that.

The above code works. What's your question?

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

Share this post


Link to post
Share on other sites

Hey @ZachSaucier :)

The problem is, we have 3 "states" but only two matching breakpoints. I need a way, to tell scrollTrigger the third state (if both breakpoints don't match:

 

State 1 ( all until 1024px viewport width) :

"(max-width: 1024px)"

State 2 (all up 1025px with a aspect ratio between 4/3 and 2/1) :

"(min-width: 1025px) and (min-aspect-ratio: 4/3) and (max-aspect-ratio: 2/1)"

Missing: State 3 (all up 1025px where the aspect ratio is not betweend 4/3 and 2/1)

Something like this (dummy Code):

ScrollTrigger.matchMedia({
    "(min-width: 1025px) and (min-aspect-ratio: 4/3) and (max-aspect-ratio: 2/1)": function() {
		
    },

    "not-matching": function() {
		// if no breakpoint is matching
    },

});

In this Case, I also wouldn't need State 2. :)

Thanks @ZachSaucier

Share this post


Link to post
Share on other sites

The reason for this is that there is no "else" functionality with media queries. 

 

Alternatively you could try setting things up with if/else using JS's .matchMedia but then you'll be responsible for killing off and recreating ScrollTriggers yourself, which is obviously not optimal.

Share this post


Link to post
Share on other sites
15 hours ago, ZachSaucier said:

You just need to create media queries that match the surrounding parts. Something like this:


Hello @ZachSaucier,

 

I already tried this, but with this solution we've many situations we're multiple breakpoints are matching (always if the aspect ratio = 4/3 or 2/1).


1032 * 774 = 4/3 = 1.33333 -> so 3 and 4 from your example will match both at this resolution, the next would be 1037 * 777, and so on...

 

1032 * 516 = 2/1 = 2 -> so 2 and 4 from your example will match, the next would be 1034 * 517, 1036 * 518, ...

I updated the codepen with some output:

See the Pen 0cb7376d2d2ff34bff65a1b1bd633faa?editors=0001 by emjay (@emjay) on CodePen



Hope you have another idea.

 

Thanks, Martin

Share this post


Link to post
Share on other sites
6 hours ago, emjay said:

Hope you have another idea.

Like I said, the alternative is to use JS's built in method of matchMedia and handle the killing and reconstructing yourself.

Share this post


Link to post
Share on other sites
On 7/14/2020 at 2:11 AM, GreenSock said:

Furthermore, you can optionally return a function that'll be called when the breakpoint is no longer active (teardown). I figured you might appreciate that. 

 

@ZachSaucier as you've said,  this "is obviously not optimal".

 

Would this teardown function, which was mentioned by @GreenSock help in my case? That was the reason for me to ask in this thread.

 

Thanks,

Martin

Share this post


Link to post
Share on other sites
6 hours ago, emjay said:

Would this teardown function, which was mentioned by @GreenSock help in my case? That was the reason for me to ask in this thread.

Sorry, what teardown function are you talking about?

Share this post


Link to post
Share on other sites
On 7/14/2020 at 2:11 AM, GreenSock said:

Thanks for sharing your solution!

 

I rolled up my sleeves and worked on accommodating a use case like this, so you can add the same breakpoint multiple times and it should work in 3.4.1. Here's a preview: https://assets.codepen.io/16327/ScrollTrigger.min.js

 

Furthermore, you can optionally return a function that'll be called when the breakpoint is no longer active (teardown). I figured you might appreciate that. 

 

Better?

@ZachSaucier I mean this post, #6 in this thread.

Share this post


Link to post
Share on other sites

@emjay What exactly do you want to happen? If there are multiple matching media queries and you only want ONE to apply at any given time, what logic do you want to use to decide which one? What do you want ScrollTrigger to do to help you that it's not currently doing? 

 

 

Share this post


Link to post
Share on other sites

I re-read things and I think I understand what you're asking for - a "none" option (not-matching). Here's a helper function I whipped up that should deliver that kind of thing - you'd use it just like ScrollTrigger.matchMedia():

function matchMediaOrNot(vars) {
  let queries = [],
      copy = {},
      isMatching,
      notMatchingFunc = vars.none,
      check = () => {
        let i = queries.length;
        while (i--) {
          if (queries[i].matches) {
            isMatching || ScrollTrigger.revert(false, "none");
            isMatching = true;
            return;
          }
        }
        isMatching = false;
        return notMatchingFunc();
      },
      wrap = func => () => {
        check();
        let result = func();
        return () => check() || result;
      },
      p;
  for (p in vars) {
    (p !== "none") && queries.push(window.matchMedia(p));
    copy[p] = wrap(vars[p]);
  }
  ScrollTrigger.matchMedia(copy);
  check();
}

Usage:

matchMediaOrNot({
    "(max-width: 1024px)": function() {
        console.log("1")
    },
    "(min-width: 1025px) and (min-aspect-ratio: 4/3) and (max-aspect-ratio: 2/1)": function() {
        console.log("2")
    },
    "(min-width: 1025px) and (max-aspect-ratio: 4/3)": function() {
        console.log("3")
    },
    "none": function() {
        console.log("not matching!");
    },
});

Does that deliver what you wanted? 

  • Like 2

Share this post


Link to post
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.

×