Run a function when animation completes using Curtains.js

470 Views Asked by At

I'm looking to call a function right at the end of the wobble effect.

That is, at the end of the damping effect (when the wobble stops), I'd like to execute a GSAP timeline function. I'd assume this type of "onComplete" function would need to be called inside the onReady() of Curtains and perhaps by tracking the damping effect. I'm only familiar with GSAP's onComplete function, but don't know how I would implement it here. Maybe something that checks if deltas.applied is less than 0.001, then the function is called?

Below is the code snippet (without the fragment and vertex shaders). Full working code here: CodePen

class Img {
  constructor() {
    const curtain = new Curtains({
        container: "canvas",
         watchScroll: false,
    });
    
    const params = {
        vertexShader,
        fragmentShader,
        uniforms: {
          time: {
            name: "uTime",
            type: "1f",
            value: 0,
          },
          prog: {
            name: "uProg",
            type: "1f",
            value: 0,
          }
        }
      }

    const planeElements = document.getElementsByClassName("plane")[0];  
    
    this.plane = curtain.addPlane(planeElements, params);

    if (this.plane) {
      this.plane
        .onReady(() => {
            this.introAnim();
        })
        .onRender(() => {
          this.plane.uniforms.time.value++;
          deltas.applied += (deltas.max - deltas.applied) * 0.05;
          deltas.max += (0 - deltas.max) * 0.07;
          this.plane.uniforms.prog.value = deltas.applied 
        })
    }

    // error handling
    curtain.onError(function() {
      document.body.classList.add("no-curtains");
    });
  }
  
  introAnim() {
    deltas.max = 6;
    //console.log("complete") <-- need an onComplete type function~!
  }
}

window.onload = function() {
  const img = new Img();
}
4

There are 4 best solutions below

2
SavvyShah On

This won't be the best answer possible but you can take some ideas and insights from it.

Open the console and see that when the animation gets completed it gets fired only once.


//Fire an onComplete event and listen for that
const event = new Event('onComplete');

class Img {
  constructor() {
    // Added a instance variable for encapsulation
    this.animComplete = {anim1: false}
    //Changed code above
    const curtain = new Curtains({
        container: "canvas",
         watchScroll: false,
    });
    const params = {
        vertexShader,
        fragmentShader,
        uniforms: {
          time: {
            name: "uTime",
            type: "1f",
            value: 0,
          },
          prog: {
            name: "uProg",
            type: "1f",
            value: 0,
          }
        }
      }

    const planeElements = document.getElementsByClassName("plane")[0];  
    
    this.plane = curtain.addPlane(planeElements, params);

    if (this.plane) {
      this.plane
        .onReady(() => {
            this.introAnim();
        
        document.addEventListener('onComplete', ()=>{
          //Do damping effects here
          console.log('complete')
        })
        })
        .onRender(() => {
          this.plane.uniforms.time.value++;
          deltas.applied += (deltas.max - deltas.applied) * 0.05;
          deltas.max += (0 - deltas.max) * 0.07;
          this.plane.uniforms.prog.value = deltas.applied 
          if(deltas.applied<0.001 && !this.animComplete.anim1){
            document.dispatchEvent(event)
            this.animComplete.anim1 = true
          }
        })
    }

    // error handling
    curtain.onError(function() {
      document.body.classList.add("no-curtains");
    });
  }
  
  introAnim() {
    deltas.max = 6;
  }
}

window.onload = function() {
    const img = new Img();
  }
6
Zach Saucier On

What you could use is some algebra :)

First off, you should simplify your deltas.max function like so:

deltas.max += (0 - deltas.max) * 0.07;
// Simplifies to
deltas.max -= deltas.max * 0.07;
// Rewrite to
deltas.max = deltas.max - deltas.max * 0.07;
// Rewrite to
deltas.max = deltas.max * (1 - 0.07); 
// Simplifies to
deltas.max *= 0.93; // Much nicer :)

That is actually pretty important to do because it makes our work of calculating the end value of our time variable and the duration of our animation significantly Easier:

// Given deltas.max *= 0.93, need to calculate end time value
// endVal = startVal * reductionFactor^n
// Rewrite as 
// n = ln(endVal / startVal) / ln(reductionFactor) // for more see https://www.purplemath.com/modules/solvexpo2.htm
// n = ln(0.001 / 8) / ln(0.93)
const n = 123.84;
        
// Assuming 60fps normally: n / 60
const dur = 2.064;

Once we have those values all we have to do is create a linear tween animating our time to that value with that duration and update the max and prog values in the onUpdate:

gsap.to(this.plane.uniforms.time, {
  value: n,
  duration: dur,
  ease: "none",
  onUpdate: () => {
    this.deltas.applied += (this.deltas.max - this.deltas.applied) * 0.05;
    this.deltas.max *= 0.93;
    this.plane.uniforms.prog.value = this.deltas.applied;
  },
  onComplete: () => console.log("complete!")
});

Then you get "complete!" when the animation finishes!

To make sure that your Curtains animations run at the proper rate even with monitors with high refresh rates (even the ones not directly animated with GSAP) it's also a good idea to turn off Curtain's autoRendering and use GSAP's ticker instead:

const curtains = new Curtains({ container: "canvas", autoRender: false });
// Use a single rAF for both GSAP and Curtains
function renderScene() {
  curtains.render();
}
gsap.ticker.add(renderScene);

Altogether you get this demo.

1
Nish On

I've found a solution to call a function at the end of the damping (wobble) effect, that doesn't use GSAP, but uses the Curtains onRender method. Because the uTime value goes up infinitely and the uProg value approaches 0, By tracking both the uTime and uProg values inside the Curtains onRender method we can find a point (2 thresholds) at which the damping effect has essentially completed. Not sure if this is the most efficient way, but it seems to work.

.onRender(() => {
if (this.plane.uniforms.prog.value < 0.008 && this.plane.uniforms.time.value > 50) { console.log("complete")}
})
0
Nish On

Thanks to the Curtains docs re asynchronous-textures, I was able to better control the timing of the wobble effect with the desired result every time. That is, on computers with lower FPS, the entire damping effect takes place smoothly, with an onComplete function called at the end, as well as on comps with higher frame rates.

Although, as mentioned there is less control over the length of the effect, as we are not using GSAP to control the Utime values. Thanks @Zach! However, using a "threshold check" inside the curtains onRender this way, means the damping wobble effect is never compromised, if we were to disable the drawing at the on complete call.

By enabling the drawing at the same time the image is loaded we avoid any erratic behaviour. The following works now with hard refresh as well.

export default class Img {
  constructor() {
    this.deltas = {
      max: 0,
      applied: 0,
    };

    this.curtain = new Curtains({
      container: "canvas",
      watchScroll: false,
      pixelRatio: Math.min(1.5, window.devicePixelRatio),
    });

    this.params = {
      vertexShader,
      fragmentShader,
      uniforms: {
        time: {
          name: "uTime",
          type: "1f",
          value: 0,
        },
        prog: {
          name: "uProg",
          type: "1f",
          value: 0,
        },
      },
    };

    this.planeElements = document.getElementsByClassName("plane")[0];

    this.curtain.onError(() => document.body.classList.add("no-curtains"));
    this.curtain.disableDrawing(); // disable drawing to begin with to prevent erratic timing issues
    this.init();
  }

  init() {
    this.plane = new Plane(this.curtain, this.planeElements, this.params);
    this.playWobble();
  }

  loaded() {
    return new Promise((resolve) => {
      // load image and enable drawing as soon as it's ready
      const asyncImgElements = document
        .getElementById("async-textures-wrapper")
        .getElementsByTagName("img");

      // track image loading
      let imagesLoaded = 0;
      const imagesToLoad = asyncImgElements.length;

      // load the images
      this.plane.loadImages(asyncImgElements, {
        // textures options
        // improve texture rendering on small screens with LINEAR_MIPMAP_NEAREST minFilter
        minFilter: this.curtain.gl.LINEAR_MIPMAP_NEAREST,
      });

      this.plane.onLoading(() => {
        imagesLoaded++;
        if (imagesLoaded === imagesToLoad) {
          console.log("loaded");
          // everything is ready, we need to render at least one frame
          this.curtain.needRender();

          // if window has been resized between plane creation and image loading, we need to trigger a resize
          this.plane.resize();
          // show our plane now
          this.plane.visible = true;

          this.curtain.enableDrawing();

          resolve();
        }
      });
    });
  }


  playWobble() {
    if (this.plane) {
      this.plane
        .onReady(() => {
          this.deltas.max = 7; // 7
        })
        .onRender(() => {
          this.plane.uniforms.time.value++;
          this.deltas.applied += (this.deltas.max - this.deltas.applied) * 0.05;
          this.deltas.max += (0 - this.deltas.max) * 0.07;
          this.plane.uniforms.prog.value = this.deltas.applied;

          console.log(this.plane.uniforms.prog.value);

          // ----  "on complete" working!! ( even on hard refresh) -----//

          if (
            this.plane.uniforms.prog.value < 0.001 &&
            this.plane.uniforms.time.value > 50
          ) {
            console.log("complete");
            this.curtain.disableDrawing();
          }
        });
    }
  }

  destroy() {
    if (this.plane) {
      this.curtain.disableDrawing();
      this.curtain.dispose();
      this.plane.remove();
    }
  }
}

const img = new Img();

Promise.all([img.loaded()]).then(() => {
          console.log("animation started");
        });