Jump to content

Rodrigo last won the day on May 24

Rodrigo had the most liked content!


  • Posts

  • Joined

  • Last visited

  • Days Won


Everything posted by Rodrigo

  1. Hi, The best option I can think of is using the Observer Plugin in the particular container: https://greensock.com/docs/v3/Plugins/Observer This seems to work the way you expect: useIsomorphicLayoutEffect(() => { let ctx = gsap.context(() => { let restartTimer; let t = gsap.to(scrollContainerRef.current, { scrollTo: { y: "max", autoKill: true }, duration: 10, }); Observer.create({ target: scrollContainerRef.current, type: "wheel,touch", onChange: () => { restartTimer && restartTimer.kill(); restartTimer = gsap.delayedCall(1, () => { const maxScroll = scrollContainerRef.current.scrollHeight - scrollContainerRef.current.clientHeight; const currentScroll = scrollContainerRef.current.scrollTop; t = gsap.to(scrollContainerRef.current, { scrollTo: { y: "max", autoKill: true }, duration: 10 * (1 - currentScroll / maxScroll), }); }); }, }); }, comp); return () => ctx.revert(); }, []); Here is a fork of your demo: https://codesandbox.io/p/sandbox/laughing-pasteur-dtnkxf?file=%2Fcomponents%2FHero.jsx%3A13%2C18 Hopefully this helps. Happy Tweening!
  2. Hi Marc, Sorry to hear about the troubles 😞 Unfortunately without a minimal demo there is not much we can do. We understand also that given the unique situation you're dealing with a demo is a bit hard to create. There are a few things that caught my attention from your post though: I don't really follow this. You mean that the content you're getting from the server response has both HTML and JS in it, with some GSAP instances in the JS? Or you're just getting the HTML and the animation code is on a JS file that already is loaded in the client? In order to tell ScrollTrigger that new content has been added and that it has to run it's calculations again, calling ScrollTrigger.refresh() is the way to go. This should be executed after the response from the server and after the DOM has been updated. You should find a way to know that the DOM is updated with the new content before calling the refresh method. Perhaps the new content you're getting has images that are not loaded yet when you call the refresh method? If that's the case either give those images a fix height or add some code to check when all the images are loaded and then call ScrollTrigger.refresh(). This is the closest I can get to a working example of this approach: https://codepen.io/GreenSock/pen/YzJLMaX Hopefully this helps. Happy Tweening!
  3. Hi, This should work: useIsomorphicLayoutEffect(() => { let sections = gsap.utils.toArray(".dark-section"); let ctx = gsap.context(() => { const logo = logoRef.current; const menu = menuRef.current; tl.current = gsap .timeline({ paused: true, }) .to(logo, { color: "#ffffff", }) .to(menu.children, { backgroundColor: "#ffffff", }, "<"); sections.forEach((section) => { ScrollTrigger.create({ trigger: section, onEnter: () => tl.current.play(), onEnterBack: () => tl.current.play(), onLeave: () => tl.current.reverse(), onLeaveBack: () => tl.current.reverse(), start: "top 70", end: "bottom 85", markers: true, }); }); }, headerRef); return () => ctx.revert(); }, []); A couple of notes: You had the gsap.utils.toArray() method outside the effect hook. That's not going to work because at that point the DOM is not rendered yet. You had paused: true on each instance of the timeline. Just pause the timeline and not individual instances. While it's possible is far more difficult to manage correctly if you just want to play/reverse the entire timeline. Hopefully this helps. Happy Tweening!
  4. Hi @LizF and welcome to the GreenSock forums! Just add an extra set of ScrollTrigger instances that take care of the container's pinning: ScrollTrigger.create({ trigger: ".card_2", start: "top top", end: "+=200", pin: true, }); gsap .timeline({ scrollTrigger: { trigger: ".card_2", start: "top top+=200", end: "+=200", scrub: true, markers: true, id: "2", toggleActions: "restart pause reverse pause" } }) .from(".block_2", { opacity: 0, duration: 1 }); ScrollTrigger.create({ trigger: ".card_3", start: "top top", end: "+=200", pin: true, }); gsap .timeline({ scrollTrigger: { trigger: ".card_3", start: "top top+=200", end: "+=200", scrub: true, markers: {indent: 150}, id: "3", toggleActions: "restart pause reverse pause" } }) .from(".block_3", { x: 500, duration: 1 }); Here is a fork of your codepen example: https://codepen.io/GreenSock/pen/rNqvzBv Hopefully this helps. Happy Tweening!
  5. Hi, I'm not sure I understand correctly what you're trying to do, but maybe is something like this: https://codepen.io/GreenSock/pen/Poyejgq This is mostly about the proper layout and HTML markup. Also is worth noticing that you were using very old versions of GSAP and ScrollTrigger, is better to always use the latest ones. Hopefully this helps. Happy Tweening!
  6. Hi @Moritz L and welcome to the GreenSock forums! First, thanks for being a Club GreenSock member and supporting GreenSock! The problem here is that on the first scroll up, the value you're passing to the progress property is negative, but only the first time it doesn't work. On successive events scrolling up a negative value works as expected. What you can do is create some simple conditional logic that runs on that first event in order to check if the value is negative and use GSAP Wrap Util function to pass it correctly: onChange: (self) => { const delta = Math.abs(self.deltaX) > Math.abs(self.deltaY) ? -self.deltaX : -self.deltaY; let myProgress = snapProgress(loop.progress() + mapRange(delta) * 10); if (!hasChanged && myProgress < 0) { myProgress = gsap.utils.wrap(0, 1, myProgress); } hasChanged = true; const scrub = gsap.to(loop, { progress: myProgress, duration: 1, ease: "power4.out" }); scrub.invalidate().restart(); } That conditional check will be truthy only on the first event, then it'll always be falsy. Here is a fork of your codepen: https://codepen.io/GreenSock/pen/ExdLXmb Hopefully this helps. Happy Tweening!
  7. Hi @dcarrano72 and welcome to the GreenSock forums! I think you have a confusion regarding how the start and end point work in ScrollTrigger. From the ScrollTrigger Docs: start String | Number | Function - Determines the starting position of the ScrollTrigger. It can be any of the following: String - Describes a place on the trigger and a place on the scroller that must meet in order to start the ScrollTrigger. So, for example, "top center" means "when the top of the trigger hits the center of the scroller" (and the scroller is the viewport by default). "bottom 80%" means "when the bottom of the trigger hits 80% down from the top of the viewport" (assuming vertical scroll). You can use keywords like "top", "bottom", "center" (or "left" and "right" if horizontal: true) or percentages like "80%" or pixel values like "100px". Percentages and pixels are always relative to the top/left of the element/scroller. You can even use a complex relative value like "top bottom-=100px" which means "when the top of the trigger hits 100px above the bottom of the viewport/scroller" Number - An exact scroll value, so 200 would trigger when the viewport/scroller scrolls by exactly 200 pixels. Function - A function that gets called whenever the ScrollTrigger calculates its positions (typically upon creation and any time the scroller resizes). It should return a String or Number, as described above. This makes it easy to dynamically calculate values. Like all callbacks, the function receives the ScrollTrigger instance itself as the only parameter, so you can, for example, base the position on the previous ScrollTrigger's end like start: self => self.previous().end This is a static position that is calculated when the ScrollTrigger is created and when the scroller is resized, based on where things are in the normal document flow. It is not constantly recalculated, so for example if you animate the trigger/endTrigger, it won't constantly update the start/end values accordingly because ScrollTrigger is highly optimized for performance. You can call ScrollTrigger.refresh() to force things to be recalculated. The default is "top bottom" unless pin: true is set in which case the default value is "top top". Maybe you're looking for something like this: gsap.to(".left-column", { backgroundColor: "orange", scrollTrigger: { trigger: ".heading2", start: "top botom+=400", toggleActions: "play none none reverse", } }); Here is a fork of your codepen: https://codepen.io/GreenSock/pen/rNqvwNW Finally always use markers when developing your app/site. It makes finding and understanding these issues and how ScrollTrigger works far easier. Hopefully this helps. Happy Tweening!
  8. Hi, Is really hard for us to debug issues without a minimal demo. You can fork one of the starter templates in our Vue collection here: https://stackblitz.com/@GreenSockLearning/collections/gsap-vue3-starters This is one that uses ScrollTrigger: https://stackblitz.com/edit/vitejs-vite-t1nmfa The only thing I can see is weird from your code is the fact that you are creating a GSAP instance that is controlled by ScrollTrigger and an extra ScrollTrigger instance that pins each card. Any particular reason for that? Is not an option to pin each card in the same GSAP instance? If you want to keep the cards pinned longer than the animation, then you can create a timeline and add and extra empty .to() instance to it: cards.forEach((card, i) => { const tl = gsap.timeline({ scrollTrigger: { trigger: card, start: "top-=" + 40 * i + " 40%", end: "center 20%", scrub: true, }, }) .to(card, { scale: () => 0.8 + i * 0.035, ease: "none", }) .to({}, {duration: 0.25}); // half the duration of the previous instance }) If you keep having issues, please include a minimal demo. Happy Tweening!
  9. Hi, First IDK if the CDN links are useful or not since you're importing GSAP and ScrollTrigger in your script section, but definitely that particular version (3.6) didn't had MatchMedia built into it. MatchMedia was added in version 3.11. The issue I see is that both code blocks are doing the exact same thing. Here is the small screen code: gsap.set('.stats-card', { yPercent: 100 }); gsap.to('.stats-card', { scrollTrigger: { trigger: '.stats-card', start: 'top 98%', end: 'top 75%', scrub: 3, markers: true, id: "small", }, yPercent: 0, stagger: { each: 0.11 } }); So before the animation is created you're setting the position of the cards to be 100% on the Y axis, then they are animated with a stagger to yPercent: 0. This is the code in your large screen: gsap.from('.stats-card', { scrollTrigger: { trigger: '.stats', start: 'top 70%', end: 'top 75%', scrub: 3, markers: true, id: "large", }, yPercent: () => 100, stagger: { each: 0.11 } }); Here you use a from instance that applies the styles you pass in the config object and then animates the elements to their natural position. So in this case the elements are animated from y 100% to 0%, just like the previous animation. So you do have different GSAP instances, but they're doing the same. See the problem? Hopefully this clear things up. Happy Tweening!
  10. Hi @Garavani and welcome to the GreenSock forums! Running some custom logic, even a small and simple one as you have in your snippet, it's always a bit expensive and wasteful in the long run. In this case I'd recommend you to create a timeline instead of a tween and add a call method half way through that, something like this: https://codepen.io/GreenSock/pen/eYPrWVN Hopefully this helps. Happy Tweening!
  11. Hi, I've been looking around your las link for a bit and I can't find a simple solution for this. As @Cassie mentions this most likely has to do with the way you're handling state changes. You have multiple effect hooks in your components checking for different properties. Also you have this in a component: const isMobile = 'sfddsf'; useEffect(() => { setAnimLength(trackRef.current.offsetWidth); console.log(height); const context = gsap.context(() => { }); return () => context.revert(); }, [projectsData, isMobile]); You're passing a constant as a dependency in the array of your hook. That doesn't make a lot of sense. Also we always recommend using useLayoutEffect to ensure that the DOM has been changed and rendered when running the code. I created an ultra simplified example that shows that running ScrollTrigger.refresh() should have the effect you're looking for: https://codepen.io/GreenSock/pen/gOBzLro Unfortunately we don't have the time resources to comb through an entire codebase trying to find the problem. Even if you already reduced it to it's bare minimum, it's still a lot. Sorry I can't be of more assistance. Happy Tweening!
  12. Hi, The issue is that the calculations ScrollTrigger is making here when creating the linkST instances: let menu = gsap.utils.toArray(".nav a"); menu.forEach((a) => { let element = document.querySelector(a.getAttribute("href")), linkST = ScrollTrigger.create({ trigger: element, start: "top top" }); a.addEventListener("click", (e) => { e.preventDefault(); gsap.to(window, { duration: 1, scrollTo: linkST.start, overwrite: "auto" }); }); }); Are being affected by the ScrollTrigger instances created in the panels loop. Since you know that the home section is at the top of the document just use zero there: a.addEventListener("click", (e) => { e.preventDefault(); const target = e.target.getAttribute("href") === "#home" ? 0 : linkST.start; gsap.to(window, { duration: 1, scrollTo: target, overwrite: "auto", }); }); Finally is worth noticing that pinType : "sticky" is not an option. It's either "fixed" or "transform" pinType "fixed" | "transform" - by default, position: fixed is used for pinning only if the scroller is the <body>, otherwise transforms are used (because position: fixed won't work in various nested scenarios), but you can force ScrollTrigger to use position: fixed by setting pinType: "fixed". Typically this isn't necessary or helpful. Beware that if you set the CSS property will-change: transform, browsers treat it just like having a transform applied, breaking position: fixed elements (this is unrelated to ScrollTrigger/GSAP). Hopefully this helps. Happy Tweening!
  13. Hi, The issue is that you're missing the empty dependencies array in the effect hook: useLayoutEffect(() => { let ctx = gsap.context(() => { /* ... */ }, component); return () => ctx.revert(); }, []); So basically what happens here is that everytime that the state is updated that entire layout effect is executed again causing that behaviour you're describing. Using an empty dependencies array ensures that the layout effect hook runs once when the component is first mounted. Hopefully this clear things up. Happy Tweening!
  14. Hi @Houseofdreams and welcome to the GreenSock forums! You could try with an arrow function in the onComplete callback: GSAP.to(this.shadow.material.uniforms.uAlpha, { duration: 1, delay: duration - .25, value: 1, onComplete: () => this.experience.world.level2.show(), }) Or execute an anonymous function: GSAP.to(this.shadow.material.uniforms.uAlpha, { duration: 1, delay: duration - .25, value: 1, onComplete: function() { this.experience.world.level2.show(); }, }); Or use an arrow function when defining show method (if that is a possibility): const show = () => { console.log(this); }; Hopefully this helps. If you keep having issues, please provide a minimal demo so we can take a better look at it. Happy Tweening!
  15. Hi @thomasbosc and welcome to the GreenSock forums! The issue here is the repeat: -1 you have in your timeline. So this is what's happening: You create a timeline that is repeating endlessly, not going to stop until the end of times, with an infinite duration. Then you tell ScrollTrigger: "Here is this animation that lasts forever, I want you to scrub this animation's progress in this amount of scroll". See where this goes wrong? ScrollTrigger, as great as it is, it can't do that because is logically impossible. ScrollTrigger does is best, which ends up with the animation going crazy fast. You can set a number of repeats for your timeline that looks nice and is enough repetitions to achieve the effect you want. I used this in your codepen and it looks good and has good speed: const tl = gsap.timeline({ repeat: 4, }); Hopefully this helps. Happy Tweening!
  16. Hi, Here is the updated link: https://stackblitz.com/edit/nextjs-ep1rbj?file=pages%2Fscroll.js Although it seems that something is up with Stackblitz right now. But I can assure you that the code there works as expected on my local machine, so the code should be a good reference. Happy Tweening!
  17. Hi @Tony Geek and welcome to the GreenSock forums! Most likely this has to do with the way you're queuing your scripts. Your global functions file should have ScrollTrigger as a dependency in the array: // Include GSAP javascript function enqueue_gsap() { wp_enqueue_script( 'gsap-js', 'https://cdnjs.cloudflare.com/ajax/libs/gsap/3.11.5/gsap.min.js', array( ), false, true ); wp_enqueue_script( 'gsap-st', 'https://cdnjs.cloudflare.com/ajax/libs/gsap/3.11.5/ScrollTrigger.min.js', array( 'gsap-js' ), false, true ); } function enqueue_global_functions() { wp_enqueue_script( 'global-functions', get_stylesheet_directory() . 'js/global-functions.js', array( 'jquery, gsap-st' ), '1.0.0', true ); } That should be enough to have that file added after GSAP and ScrollTrigger. Hopefully this helps. Happy Tweening!
  18. Hi @dadostudio and welcome to the GreenSock forums! First thanks for being a Club GreenSock member and supporting GreenSock! For this you can easily use the ScrollTrigger instance for the horizontal animation and get it start and end points: const anchors = gsap.utils.toArray(".anchor"); anchors.forEach((anchor, i) => { anchor.addEventListener("click", (e) => { e.preventDefault(); if (i === 0) { gsap.to(window, { scrollTo: { y: scrollTween.scrollTrigger.end }, duration: 0.35 }); } if (i === anchors.length - 1) { gsap.to(window, { scrollTo: { y: scrollTween.scrollTrigger.start }, duration: 0.35 }); } }); }); Here is a fork of your codepen: https://codepen.io/GreenSock/pen/KKGRdqg Here is another example that uses different links for each section, but the approach is basically the same: https://codepen.io/GreenSock/pen/jOezPLN Hopefully this helps. Happy Tweening!
  19. Hi, There are a few issues with your GSAP config. You're setting the from values in the to object: tl .fromTo(chars, { opacity: 0, }, { duration: 0.8, opacity: 1, scale: 0, y: 80, rotationX: 180, transformOrigin: "0% 50% -50", ease: "back", stagger: 0.01 } ); }; You're basically telling GSAP animate the elements from opacity 0, to: opacity: 1, scale: 0 y: 80 rotationX: 180 So basically that scale 0 will mean that the elements will not be visible. Also your quote element still has an opacity of 0. This seems to work: document.getElementById("show").onclick = function () { gsap.set("#quote", { opacity: 1 }); tl.fromTo( chars, { opacity: 0, scale: 0, y: 80, rotationX: 180, }, { duration: 0.8, opacity: 1, scale: 1, y: 0, rotationX: 0, transformOrigin: "0% 50% -50", ease: "back", stagger: 0.01 } ); }; Hopefully this helps. Happy Tweening!
  20. Hi everyone! @DennisDev, thanks for sharing your solution with the community 💚 There is one comment though about the code you posted: You're not using GSAP Context in your effect hook. We strongly recommend using GSAP Context in react environments to solve the issues that arise due to the double call in the effect hooks since version 18 of React. https://greensock.com/docs/v3/GSAP/gsap.context() As @DennisDev showed, this shouldn't be a complicated endeavor. I just created a super simple example of this that uses GSAP Context: https://stackblitz.com/edit/nextjs-qq3fvh?file=pages%2Fscroll.js Hopefully this helps. Happy Tweening!
  21. Hi @drpantelic and welcome to the GreenSock forums! That is not something that might be extremely trivial to create and make it 100% dynamic and is a bit beyond what we can do in these free forums. This is the best I can do with the time I have right now: https://codepen.io/GreenSock/pen/OJBvBjB Hopefully this is enough to get you started. Happy Tweening!
  22. Hi, Just in case here are a few examples: https://codepen.io/GreenSock/pen/BaRqeGb https://codepen.io/GreenSock/pen/ZEXYrJR https://codepen.io/GreenSock/pen/jOezPLN Happy Tweening!
  23. Hi, You could create a timeline and animate the entire container first and then the sections: let sections = gsap.utils.toArray(".container .panel"); gsap.set(".container", { x: () => ((window.innerWidth / 2) - sections[0].clientWidth / 2) }); gsap.timeline({ scrollTrigger: { trigger: ".side-scrolling-wrapper", start: "top top", markers: true, scrub: 0.5, snap: 0.1, end: () => "+=" + document.querySelector(".container").offsetWidth } }) .to(".container", { x: 0, duration: 0.5 / sections.length, ease: "none", }) .to(sections, { xPercent: -180 * (sections.length - 1), ease: "none", opacity: 1, }); Here is a fork of your codepen: https://codepen.io/GreenSock/pen/rNqddwY Hopefully this helps. Happy Tweening!
  24. Hi, In this cases is better to use the Horizontal Loop helper function: https://greensock.com/docs/v3/HelperFunctions#loop https://codepen.io/GreenSock/pen/ZELPxWW Here is a fork of your demo that uses it: https://stackblitz.com/edit/stackblitz-starters-wzfb58?file=src%2Fhelpers%2FhorizontalLoop.js,src%2FSlider.js&title=GSAP Starter Hopefully this helps. Happy Tweening!
  25. Hi, That's a lot of code (over 700 lines). Also it caughts my attention that you have a bunch of different <canvas> tags. Why not pu everything inside a single canvas element? Right now you're animating a bunch of different HTML Nodes. You'll get far better performance animating everything inside canvas. This is an example I made a looong time ago. It uses PIXI to create a proof of concept that emulates the Jetlag site: https://jetlag.photos/ https://codepen.io/rhernando/pen/xrmXgE/fd2f9d0937490197a87f18210d71ac64 Even in a mid-range android phone works without any issues. Finally I don't know what you mean with menu bar here: Are you referring to the browser's url address bar? If so you should try using ScrollTrigger.normalizeScroll(): https://greensock.com/docs/v3/Plugins/ScrollTrigger/static.normalizeScroll() Hopefully this helps. Happy Tweening!