Jump to content
Search Community

How to use GSAP context with JS classes in Nuxt 3

Tulip781 test
Moderator Tag

Recommended Posts

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

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

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

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

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

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