2

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();
}
Zach Saucier
  • 24,871
  • 12
  • 85
  • 147
Nish
  • 81
  • 6
  • 1
    Are you planning on using GSAP or not? If you are planning on using GSAP, use it for the animation (instead of manually updating the time values and such yourself) then use its onComplete. If not, you'll have to measure the progress value and check if it's less than a threshold. – Zach Saucier Oct 15 '20 at 19:04
  • Yes! I am using GSAP. I just wasn't sure how to use it in this scenario with Curtains to achieve the wobble and damping effect. I am more familiar with GSAPs API and would prefer that solution. – Nish Oct 15 '20 at 22:08
  • I'm actually writing an article about using Curtains + GSAP so this is helpful in understanding what people have trouble with. – Zach Saucier Oct 16 '20 at 01:06
  • Also I used one of your pens as an example in my [animating efficiently](https://www.brighttalk.com/webcast/11505/440587/creating-animations-efficiently-with-gsap) talk this past Wednesday :) – Zach Saucier Oct 16 '20 at 01:28

4 Answers4

3

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.

Zach Saucier
  • 24,871
  • 12
  • 85
  • 147
  • Animating the utime value with gsap works well, however in case the comp runs lower than 60fps then the animation doesn't render properly (ie it freezes/ends before damping completely). I believe this is due to the fact the calculations are based on the frame rate. This also happens despite running it through the GSAP ticker. What would be a way around this? how would I measure the prog value to see if falls below a threshold? Inside curtains OnRender event or somewhere else? – Nish Jan 30 '21 at 05:03
  • @Nish You're saying the demo that with I provided the animation doesn't render properly when the computer is not running at at least 60 fps? Does the onComplete function not ever run in that case? – Zach Saucier Jan 30 '21 at 14:09
  • the onComplete function IS called but it gets called before the damping has completed. A bit earlier due to the reduced frame rate. Is there a better way to monitor the prog value through the curtains onRender method? My threshold solution below is erratic (doesn't get called on hard refresh). I'm actually trying to disable the drawing when the damping effect completes. – Nish Feb 24 '21 at 08:25
  • In that case I would set the end values (and/or disable the drawing) inside of the onComplete. – Zach Saucier Feb 24 '21 at 13:35
  • Unfortunately, for comps with lower fps, setting the end values, and then disabling the drawing inside GSAP onComplete, causes the wobble animation to disable before completely damping. That's why i'm trying to find an "onComplete" / threshold solution that doesn't rely on FPS. – Nish Feb 25 '21 at 07:45
  • The above approach *does not rely on the FPS*. It is perfectly synced no matter the FPS. You can't both try to show every frame of an animation *and* make it end at the right time if the client is dropping frames. That doesn't make logical sense. So you have to either choose: Do I want a more smooth animation that takes longer or do I want an animation that ends at the right time but might be jerky if the client is dropping frames? – Zach Saucier Feb 25 '21 at 15:20
1

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();
  }
SavvyShah
  • 57
  • 7
  • OnRender() is called 60 times per second, which is why the "on complete" function must be called from onReady(). Check linked codepen for reference. – Nish Oct 15 '20 at 08:04
  • I've updated the codepen with your solution. The event is called once at the end of the animation - however, it only works 50% of the time. I tried it many times and results are very erratic. Any ideas why? – Nish Oct 15 '20 at 10:31
1

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")}
})
Nish
  • 81
  • 6
  • Just noting, the author of this answer says this approach [is erratic](https://stackoverflow.com/questions/64364972/run-a-function-when-animation-completes-using-curtains-js/64381599?noredirect=1#comment117295949_64381599). – Zach Saucier Feb 24 '21 at 13:36
0

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");
        });
Nish
  • 81
  • 6