Jump to content
Search Community

Debouncing / Throttling / rAF with GSAP

Acccent test
Moderator Tag

Warning: Please note

This thread was started before GSAP 3 was released. Some information, especially the syntax, may be out of date for GSAP 3. Please see the GSAP 3 migration guide and release notes for more information about how to update the code to GSAP 3's syntax. 

Recommended Posts

Hello everyone,

 

I'm about to implement some debouncing mechanisms into my website and was wondering what was the best approach to take.

 

I've seen this snippet by @OSUblake:

var requestId = null;

window.addEventListener("resize", function() {
  if (!requestId) {
    requestId = requestAnimationFrame(update);
  }
});

function update() {
  
  // run your update
  
  requestId = null;
}

 

Which is a (beautiful) GSAP-independent implementation of requestAnimationFrame. Considering GSAP uses rAF internally, is there a way to leverage this? (edit: removed awful faulty code)

 

I feel like if there was a way, it would make it easier to subsequently use that tween, or at the very least generate new ones like it, while modifying some things like the delay between each update call etc.

 

*edit* to be clear, I understand the code above would be for throttling, not debouncing. I'm thinking about both and focusing on the former for now. :)

 

Here is the latest version of what I came up with!

function stipple(f, ...args) {
  const opts = {},
        tw = new TweenLite({}, 0.2, { onComplete: end, paused: true });
  let isStarting = true,
      lastArgs;

  setOptions(args.length === 1 ? args[0] : {});

  function dot(...thisArgs) {
    lastArgs = thisArgs;
    if ( isStarting ) {
      isStarting = false;
      if ( opts.leading ) { run(); }
      tw.restart(true, true);
    }
    else if ( !opts.throttle ) { tw.restart(true, true); }
  }

  function end() { isStarting = true; if ( !opts.leading ) { run(); } }

  function run() { opts.func.forEach(f => { new TweenLite({}, 0, { onComplete: f, onCompleteParams: opts.params || lastArgs || [] }); }); }

  Object.defineProperties(dot, {
    tween: { get: () => { return tw; }},
    options: { get: () => { return opts; }, set: v => { setOptions(v); }},
    func: { get: () => { return opts.func; }, set: v => { opts.func = makeArray(v); }},
    params: { get: () => { return opts.params; }, set: v => { opts.params = v ? makeArray(v) : null; }},
    throttle: { get: () => { return opts.throttle; }, set: v => { opts.throttle = v; }},
    leading: { get: () => { return opts.leading; }, set: v => { opts.leading = v; }},
    delay: { get: () => { return opts.delay; }, set: v => {
      v = Math.max(v, 0.001); if ( !isNaN(v) && v !== opts.delay ) { tw.duration(v); opts.delay = v; }
    }}
  });

  return dot;

  function makeArray(e) { return Array.isArray(e) ? e : [e]; }

  function setOptions(obj) {
    opts.func = makeArray(obj.func || opts.func || f);
    opts.delay = Math.max(obj.delay || opts.delay || ( typeof args[0] === 'number' ? args[0] : 0.2 ), 0.001);
    opts.params = obj.params || opts.params || args[1];
    opts.params = opts.params ? makeArray(opts.params) : null;
    opts.throttle = obj.throttle || opts.throttle || args[2] || false;
    opts.leading = obj.leading || opts.leading || args[3] || false;

    tw.duration(opts.delay);
  }
}

 

Link to comment
Share on other sites

If you want to fire a function 1 second after resizing has stopped you could do

console.clear();
var tween = TweenLite.delayedCall(1, update).pause();

window.addEventListener("resize", function() {
  console.log("resizing")
    
      tween.restart(true);
    
});

function update() {
  console.log("resize stopped 1 second ago so let's update")
}

 

open the console:

 

See the Pen ?editors=0011 by GreenSock (@GreenSock) on CodePen

 

  • Like 3
Link to comment
Share on other sites

That's great! What does the isActive() check achieve in your pasted snippet? It's not in the codepen. Just limits the number of times you call restart()?

Also, this is for debouncing, but I feel like it could work for throttling as well with just a small modification — I tried this:

window.addEventListener("resize", function() {
  if(!tween.isTweening()){
    tween.restart(true);
  }
});

 

...thinking it would restart the tween each time it reached completion (calling update every time), but it didn't work.

 

Another thing I found just after posting the initial thread is how the documentation for .ticker 

https://greensock.com/docs/TweenLite/static.ticker basically has a throttling example in it, as long as you're willing to work with frames instead of seconds... which might be preferable, actually. (Would it work with a fps that's lower than 1?)

Link to comment
Share on other sites

Yeah, sorry, I realized the isActive() wasn't necessary.

 

After posting that example I dipped my toes into a throttle and debounce combo solution.

Basically a tween will have an onComplete callback that does the thing you want to do.

As you resize you check to see if the progress() of that tween is past 0.5 (50%). If yes you force the tween to the end (making the onComplete callback fire) then restart the tween (which is really a timer) again.

 

The end result is that the custom onComplete callback will fire basically every 0.5 seconds while you resizing (much less frequent than the actual resize events) and then again after you are done resizing.

 

In the demo below the body will change background color to illustrate when he custom onComplete callback (update) is firing.

 

Please understand, this is just me messing around with the idea, not some official, GreenSock-endorsed, way of doing these things ;)

 

Use and tweak as you see fit

console.clear();
var tween = TweenLite.to({}, 1, {onComplete:update}).progress(1)

window.addEventListener("resize", function() {
  console.log("resizing")
  if(tween.progress() > 0.5){
    console.log("change color and restart()");
    tween.progress(1).restart(true, true);
  }
});



function update() {
  TweenLite.set("body", {backgroundColor:"hsla(" + Math.random() * 360 + ", 50%, 50%, 1)"})
}

 

See the Pen zRoZEj?editors=1011 by GreenSock (@GreenSock) on CodePen

 

 

 

 

  • Like 1
Link to comment
Share on other sites

haha I absolutely created this thread to encourage some unofficial messing-around :D

I usually get a custom build of lodash with just debounce and throttle, and thought "I'm sure GSAP has enough under the hood to replace that entirely, possibly more efficiently too". So here I am, asking about it. :) Maybe we can come across an all-encompassing solution!

 

Your last solution is great because you can remove .progress(1) and you end up with the previous solution, ie. the debouncing functionality. I'm making a codepen with a reusable function, I'll post it when I'm finished – posting this comment here first because I accidentally made my browser crash and am guessing it may happen again, haha.

Link to comment
Share on other sites

There it is :)

 

const dots = {};

function stipple(id, func, delay, throttle = false, leading = false) {
  if ( !dots.hasOwnProperty(id) ) {
    dots[id] = TweenLite
      .to({}, delay * ( throttle ? 2 : 1 ), {})
      .eventCallback(( leading ? "onStart" : "onComplete" ), func);
  }
  
  if ( dots[id].progress() === 1 ) { dots[id].restart(true, false); }
  
  if ( dots[id].progress() > 0.5 ) {
    if ( leading ) {
      
      if ( throttle ) { dots[id].restart(true, false); }
      else { dots[id].progress(0.01, true); }
      
    } else {
      
      if ( throttle ) { dots[id].progress(1); }
      dots[id].restart(true, true);
      
    }
  }
}

 

And the codepen:

See the Pen NybgyV?editors=0011 by Acccent (@Acccent) on CodePen

 

There's a slight issue with the timing at the end of the non-immediate throttling version, I'm not sure if it's an easy fix and if it even really matters. Also, I had to use .progress(0.01, true); – using .restart() with suppressEvents = true seems to still call onStart? Maybe I'm missing something.

 

This isn't the end of the thread btw, it would be great if others had suggestions on how to improve on this!

  • Like 1
Link to comment
Share on other sites

So, I simplified it quite a bit, and made it return something so you can remove the event listener. Here's the updated code:

function stipple(id, func, delay, throttle = false, leading = false) {
  let stippled = function() {
    if ( !dots.hasOwnProperty(id) ) {
      dots[id] = TweenLite
        .to({}, delay, {})
        .eventCallback(( leading ? "onStart" : "onComplete" ), func);
    }

    if ( dots[id].progress() === 1 ) { dots[id].restart(true, false); }

    if ( !throttle && dots[id].progress() > 0.01 ) {
        dots[id].progress(0.01, true);
    }
  };
  
  return stippled;
}

 

The codepen in my post above is the updated one, I forgot to fork, oops.

 

I don't understand how lodash and underscore manage to do this without storing any external references. Here are the sources, if anyone's interested:

https://github.com/jashkenas/underscore/blob/master/underscore.js

https://github.com/lodash/lodash/blob/master/debounce.js

 

I'll go with the solution above for now, but please feel free to keep improving on it, and if someone sees a performance issue or the like, by all means share it :D

  • Like 1
Link to comment
Share on other sites

ok, I got inspired by Diaco's examples and made a new version. Also I get now how we don't need to store anything outside of the function, so my understanding of JS as a whole improved thanks to this!

 

See the Pen MQpggE?editors=0011 by Acccent (@Acccent) on CodePen

 

This accepts either objects and parameters, and can be used for several functions at once. Have a look at how the 4 events are set up. (By the way, maybe that wasn't clear in the previous pen, but each square is a checkbox that you can turn on or off.)

  • Like 1
Link to comment
Share on other sites

3 minutes ago, OSUblake said:

 

I knew that, but I tried to avoid really thinking about it for as long as I could... hehe.

In my mind, the variables inside the function were going to be reinitialised every time the function was called... I didn't realise that since the function was returning another function, the context stayed the same every time that returned function was called. (I'm probably not making much sense! ? )

  • Haha 1
Link to comment
Share on other sites

8 minutes ago, Acccent said:

In my mind, the variables inside the function were going to be reinitialised every time the function was called... I didn't realise that since the function was returning another function, the context stayed the same every time that returned function was called. (I'm probably not making much sense! ? )

 

Hehe. I totally know what you mean. I think pretty much everybody views a function call like that when they start out with JavaScript. In that MDN article, they even say it seems unintuitive that local variables still exist after running the function, so you're not alone.

  • Like 1
Link to comment
Share on other sites

Going back to improving the solution, do we know why dot.restart(true, true); still calls the onStart callback? I assumed it wouldn't since suppressEvents if set to true. Is that by design?

 

(If it is, maybe there's a more efficient alternative to if { dot.progress() > 0.01 ) { dot.progress(0.01, true); } ?)

Link to comment
Share on other sites

do we know why dot.restart(true, true); still calls the onStart callback? 

 

You are basically saying "wait some amount of time before dot starts playing again". The amount of time for the delay is not part of the tween. The engine just knows to wait x amount of seconds before the tweens playhead starts moving forward. When it starts moving forward, the onStart is fired. 

 

when you suppressEvents you are only suppressing events that occur when the playhead is jumping past those events.

  • Like 2
Link to comment
Share on other sites

22 hours ago, Carl said:

when you suppressEvents you are only suppressing events that occur when the playhead is jumping past those events.

 

Ah, that makes sense, thanks!

 

I pretty heavily refactored the codepen above (taking a bit more inspiration from @Diaco) so that the timing is now more precise and also, most importantly, you can access and change the functions, delay, leading and throttle parameters of a given stipple on the fly. :)

Link to comment
Share on other sites

I updated the pen (and the code in the OP) because I was annoyed that you could only ever pass "static" parameters to your debounced function. So now if you don't set the params property (or set it to null later), the function is passed the event's data, which allows you to test against mouse position, window size, key code etc.

 

Check out the update codepen above for a cool moving box :)

  • Like 1
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...