Tulip781 Posted December 1, 2023 Share Posted December 1, 2023 Hi community, I wanted to know the best practice for using GSAP context for cleanup with JS classes in Nuxt 3. I've seen the Stack Blitz demo, but I'm struggling to wrap my head around how it would be used with JS classes, where the GSAP logic is spread between different classes. Would I wrap all my JS class definitions inside a GSAP context? Any guidance would be greatly appreciated. Here is the file I’d like to adapt to use GSAP Context, so that I can easily clean up all animations onUnmounted. Thank you. <template> <div class="flex h-screen w-screen items-center justify-center <img alt="" class="absolute left-0 top-0 h-56 select-none opacity-0" /> </div> </template> <script setup> import gsap from 'gsap'; import { useMouse, useWindowSize } from '@vueuse/core'; const { x: mouseX, y: mouseY } = useMouse(); const images = ref([]); // Array to store Image instances const { width, height } = useWindowSize(); const lastMousePos = ref({ x: 0, y: 0 }); const cacheMousePos = ref({ x: 0, y: 0 }); let animationFrameId; let trail; let tl; let ctx; const cloudsRef = ref([]); const MathUtils = { // linear interpolation lerp: (a, b, n) => (1 - n) * a + n * b, // distance between two points distance: (x1, y1, x2, y2) => Math.hypot(x2 - x1, y2 - y1), }; watch( () => ({ x: mouseX.value, y: mouseY.value }), (newPos) => { cacheMousePos.value = { ...lastMousePos.value }; lastMousePos.value = newPos; }, { deep: true }, ); const getMouseDistance = () => { return Math.hypot(lastMousePos.value.x - cacheMousePos.value.x, lastMousePos.value.y - cacheMousePos.value.y); }; class Image { constructor(el) { this.DOM = { el: ref(el) }; this.defaultStyle = { scale: 1, x: 0, y: 0, opacity: 0 }; this.getRect(); } resize() { gsap.set(this.DOM.el.value, this.defaultStyle); this.getRect(); } getRect() { this.rect = this.DOM.el.value.getBoundingClientRect(); } isActive() { return gsap.isTweening(this.DOM.el) || this.DOM.el.style.opacity != 0; } } class ImageTrail { constructor(images) { this.DOM = { content: document.querySelector('.content') }; this.images = images; this.imagesTotal = this.images.length; this.imgPosition = 0; this.zIndexVal = 1; this.threshold = 25; requestAnimationFrame(() => this.render()); } render() { const distance = getMouseDistance(); cacheMousePos.value.x = MathUtils.lerp(cacheMousePos.value.x || mouseX.value, mouseX.value, 0.1); cacheMousePos.value.y = MathUtils.lerp(cacheMousePos.value.y || mouseY.value, mouseY.value, 0.1); if (distance > this.threshold) { this.showNextImage(); ++this.zIndexVal; this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0; } let isIdle = true; for (const img of this.images) { if (img.isActive()) { isIdle = false; break; } } if (isIdle && this.zIndexVal !== 1) { this.zIndexVal = 1; } animationFrameId = requestAnimationFrame(() => this.render()); } stop() { if (animationFrameId) { cancelAnimationFrame(animationFrameId); } this.images.forEach((img) => gsap.killTweensOf(img.DOM.el)); } showNextImage() { const img = this.images[this.imgPosition]; gsap.killTweensOf(img.DOM.el); tl = gsap.timeline(); tl.set(img.DOM.el, { opacity: 1, scale: 1, zIndex: this.zIndexVal, x: cacheMousePos.value.x - img.rect.width / 2, y: cacheMousePos.value.y - img.rect.height / 2, }); tl.to(img.DOM.el, { duration: 0.9, scale: 3, ease: 'expo.out', x: mouseX.value - img.rect.width / 2, y: mouseY.value - img.rect.height / 2, }) .to( img.DOM.el, { duration: 1, ease: 'power1.out', opacity: 0, }, 0.4, ) .to( img.DOM.el, { duration: 1, ease: 'quint.out', scale: 0.2, }, 0.4, ); } } onMounted(() => { images.value = Array.from(cloudsRef.value).map((el) => new Image(el)); trail = new ImageTrail(images.value); }); onUnmounted(() => { trail.stop(); }); </script> Link to comment Share on other sites More sharing options...
Rodrigo Posted December 1, 2023 Share Posted December 1, 2023 Hi, In your code snippet I see that you are creating a ctx variable but that never gets anything assigned to it. What I would do would be something like this: const ctx = gsap.context(() => {}); class MyClass { myMethod() { ctx.add(() => { gsap.to(element, { x: 100 }); }); } } onMounted(() => { const instanceOne = new MyClass(); }); onUnmounted(() => { ctx && ctx.revert(); }); https://gsap.com/docs/v3/GSAP/gsap.context()#adding-to-a-context Hopefully this helps. If you keep having issues, please create a minimal demo that clearly illustrates the problem you're facing. Happy Tweening! Link to comment Share on other sites More sharing options...
Tulip781 Posted December 1, 2023 Author Share Posted December 1, 2023 Hi Rodrigo, Thanks for much for this example, is it really useful. Would calling myMethod() in your example actually execute the gsap.to tween, or just add the tween the the context each time? How would I wrap the return value from isActive method in a GSAP context? Thanks for the help. class Image { constructor(el) { this.DOM = { el: ref(el) }; this.defaultStyle = { scale: 1, x: 0, y: 0, opacity: 0 }; this.getRect(); } resize() { gsap.set(this.DOM.el.value, this.defaultStyle); this.getRect(); } isActive() { return gsap.isTweening(this.DOM.el) || this.DOM.el.style.opacity != 0; } } Link to comment Share on other sites More sharing options...
Rodrigo Posted December 1, 2023 Share Posted December 1, 2023 Hi, Just use the add() method from the GSAP Context instance you create, as shown in the code I posted before: const ctx = gsap.context(() => {}); class Image { constructor(el) { this.DOM = { el: ref(el) }; this.defaultStyle = { scale: 1, x: 0, y: 0, opacity: 0 }; this.getRect(); } resize() { gsap.set(this.DOM.el.value, this.defaultStyle); this.getRect(); } isActive() { let isActive; ctx.add(() => { isActive = gsap.isTweening(this.DOM.el) || this.DOM.el.style.opacity != 0; }); return isaActive; } } Although that is a method that returns a boolean and not a GSAP instance so that most likely is not really necessary, so there shouldn't be a real need for that: https://gsap.com/docs/v3/GSAP/gsap.isTweening() Happy Tweening! Link to comment Share on other sites More sharing options...
GreenSock Posted December 1, 2023 Share Posted December 1, 2023 Yeah, there's no need to add that to the context because you're not actually creating any GSAP animations, ScrollTriggers, etc. The whole point of the context is to give you an easy way to revert() a bunch of tweens/timelines/ScrollTriggers/Observers/Draggables/SplitTexts and/or to scope your selector text. 👍 Link to comment Share on other sites More sharing options...
Recommended Posts
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 accountSign in
Already have an account? Sign in here.
Sign In Now