Jump to content
Search Community

Understanding TimelineMax callback and scope

Johan ⚡️ Nerdmanship test
Moderator Tag

Go to solution Solved by OSUblake,

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

I often use the call() method in my timelines.

tl.call(func, ["param1"], this, 2);

A typical situation looks like this (code below), where I wanna iterate through an array and for each item in the array I wanna create a timeline and then also run another function which use the same elements as in the current iteration. If I just run the function someplace inside the loop, that works fine, but I don't have the timing control.

 

And if I run it as a callback in timeline I can't reference the elements in the current iteration. So I solve that by passing i as a parameter and then I repeat myself and declare "var elem = myArray" again in the callback function, which seems like a hack.

var myArray = [ myElem, myOtherElem ];

function myFunction() {

  for (var i = 0; i < myArray.length; i++) {
    
    var elem = myArray[i]; // I want to access this element in my callback

    var tl = new TimelineMax();

    tl.to(elem, 1, { autoAlpha: 0})
      .call(myOtherFunction) // This guy can't access the "elem" in the current iteration
      .to(elem, 1, { autoAlpha: 1 });
    
    myOtherFunction(); // But this guy can
  }
}

Is there a way to write the call() method so the "myOtherFunction" can use the variable "elem" in the current iteration without passing it as a parameter??

  • Like 1
Link to comment
Share on other sites

Thanks Jonathan!

 

Yes, I've read it. It only says that the scope is the scope :)

 

scope : *

(default = null) — The scope in which the callback should be called (basically, what "this" refers to in the function). NOTE: this parameter only exists in the JavaScript and AS2 versions.

 

I want to set the scope in the TimelineMax.call() so that the callback function can access the scope of the calling function and the current iteration (able to reference variables $box and i).

 

It's a general question about how to set the scope in TimelineMax.call() method, but I've made a pen about it as simple as I could:

See the Pen faceacc19dde87ebaca7e49e703ddecd by nerdmanship (@nerdmanship) on CodePen

// I want the callback function to have access to the variables i and $box
// I assume I can achieve that by setting the scope in the TimelineMax.call()


// ---

// #fruits box only wants fruits
// #animals box only wants animals

var $boxes = $("[data-box]"); // $boxes[0] & $boxes[1]

// Array of two arrays of items
var arr = [
  ["Apple", "Banana", "Citrus"],
  ["Dog", "Elephant", "Ferret"]
];

function newItems() {
  
  // Iterate thru $boxes
  for (var i = 0; i < $boxes.length; i++) {
    
    $box = $($boxes[i]);
    
    var tl = new TimelineMax();

    // Fade it out, inject new random item, fade back in
    tl.to($box, 1, { autoAlpha: 0, delay: i*0.2 })
      .call(getRandomItem, i, newItems)
      .to($box, 1, { autoAlpha: 1 });
  }
}

// Injects a random fruit item to fruit box and animal item to animal box
function getRandomItem(i) {
  // Sets newItem to i.e. arr[1][0] which is "dog"
  var newItem = arr[i][Math.floor(Math.random()*arr[i].length)]; // i from for loop in newItems()
  $box.html(newItem); // $box from newItems()
}

// Renew items in boxes every 4 seconds
setInterval(newItems, 4000);

Thanks!

Link to comment
Share on other sites

Thanks for the demo. As I understand it, this isn't a scope issue. You just weren't passing the parameter i into getRandomItem() function properly.

 

First try logging out the value of i in getRandomItem()

 

function getRandomItem(i) {
  console.log(i) // undefined
  // Sets newItem to i.e. arr[1][0] which is "dog"
  var newItem = arr[Math.floor(Math.random()*arr.length)]; // i from for loop in newItems()
  $box.html(newItem); // $box from newItems()
}
 
you will see that it is undefined and you get an error like:
 
pen.js:38 Uncaught TypeError: Cannot read property 'length' of undefined
 
Going back to the docs, you will see that the second parameter in call() is an array for parameters.
 
.call( callback:Function, params:Array, scope:*, position:* ) : *
 
It needs to be an array so that the function you are calling can take multiple parameters.
 
If you put in i in an array when you do call() it will work for you
 

tl.to($box, 1, { autoAlpha: 0, delay: i*0.2 })
      .call(getRandomItem, )
      .to($box, 1, { autoAlpha: 1 });
 
When I made that change I would see new words each time the text faded in. 
 
It seems your pen is private which prevents me from providing a working fork, but that one change should be all you need.
 
 
  • Like 4
Link to comment
Share on other sites

Thanks Carl!

 

Sorry for the private settings, I wasn't aware it was limiting in that sense. It's update and public here:

See the Pen bBbbwE by nerdmanship (@nerdmanship) on CodePen

 

The brackets did do the trick with passing the i. It was sloppy of me and I haven't had this issue in other projects.

 

I discovered that the callback was working only because $box was leaking to the global object, since I forgot to declare it with "var". I don't want that, I want to keep the variable in the local scope.

 

So I'm still wondering about scope.

 

By logging this from the callback function I can see that the scope is changed successfully from the timeline to the newItems function by writing like so:

.call(getRandomItem, [i], newItems)

However, the $box variable in the callback still throws a reference error. Which is my initial problem and query.

 

If I would declare the callback function inside newItems function then $box is referenced properly. This is what I want to achieve, but I want to declare the function outside that scope to make it accessible to other functions too.

 

If this is too much of a general Js question I could turn to Stack Overflow and then come back here and share any results.

 

---

 

Just some context to clarify why I'm asking...

 

- I want each function to have one job (as long as it makes sense)

- I want to be able to reuse functions in other functions

- I don't want to repeat myself and declare the same variable multiple times

 

This is why I run into challenges regarding scope a lot. I'm practising and researching it a lot, but I still have some missing knowledge it seems.

 

In this case I could solve this by re-assigning $box = $boxes in the callback function, but it seems like bad practice. Especially if you'd have a bunch of variables. The code eventually gets bloated, harder to overview and manage, and more prone to bugs.

 

If this problem is solved there will still be a timing issue. The loop will perform all of its iterations before the first timeline has reached the first callback. All callbacks will reference the same $box – the last item of the $boxes array.

 

Edit: Removed jQuery

Link to comment
Share on other sites

Damn I wish I could come up with self-chalenges like that, they sound fun.

 

If I may interject here...

 

Yes, the answer to (what I understand to be) your question is more general JS related than it is GSAP but I don't think you will be crucified by that. If anything, you've taught me a new trick today.

 

Your issue is scope but not in the way I read in your description. The scope does not need to be changed in this particular case. What is missing is for the 'box' variable to exist inside the callback function. Just pass it in with the arguments and you're golden.

 

Like so: 

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

 

I would only not advise on this setup because if you leave the tab that you currently have the animation in and come back a while later, the animation will jump forward in time looking like you're fast forwarding it. I will assume its a side effect of using a set timeout to create a bunch of timelines that affect the same set of elements.

 

A better idea, would be to create one timeline that repeats itself but updates the contents of the boxes at a specific point in time.

  • Like 3
Link to comment
Share on other sites

  • Solution

This is probably what you're after...

See the Pen eBOPwr?editors=0010 by osublake (@osublake) on CodePen

 

It might be easier starting out using closures instead of messing around with scopes. Most of the function-based demos I posted here use closures to capture value(s).

http://greensock.com/forums/topic/14790-function-based-values-question/?p=63599

 

Simple example...

tl.call(addValues(1, 2, 3)) // => 6

function addValues(a, b, c) {
  return function() {
    console.log(a + b + c);
  }
}

.

  • Like 3
Link to comment
Share on other sites

  • 2 weeks later...

Thank you so much for your thoughts and pens, guys!

 

[...]

 

I would only not advise on this setup because if you leave the tab that you currently have the animation in and come back a while later, the animation will jump forward in time looking like you're fast forwarding it. I will assume its a side effect of using a set timeout to create a bunch of timelines that affect the same set of elements.

 

A better idea, would be to create one timeline that repeats itself but updates the contents of the boxes at a specific point in time.

 

Great advice, Dipscom – thanks!

 

Of course I ran in to this problem and my solution was to kill the interval on window.blur and recreated it on window.focus. It works well, but it doesn't feel robust enough.

 

This (code below) was what I could imagine from your suggestion, but I realised that it probably would end up out of sync in the tab use case. It also kinda does the same as the previous version, only looping thru the array differently. What did you have in mind?

var boxes = [];
var dur = 2;

// create one timeline that repeats itself
tl.to(boxes, dur, {
	bezier: [
		{ autoAlpha: 0 },
		{ autoAlpha: 1 },
		{ autoAlpha: 0 }
	],
	repeat: -1
});

// updates the contents of the boxes at a specific point in time
setInterval(function() {
	// update stuff
}, (dur*1000)/2) // Half way thru timeline duration

- - 

 

Thanks Blake – always great advice and so useful resources!

 

Thanks for both offering me what I was looking for and also a better way to approach it. 

Link to comment
Share on other sites

It would still behave as the first one, yes. Because, you're still using the a setInterval to trigger change.

 

What I meant was something like:

var boxes = [];
var dur = 2;

// create one timeline that repeats itself
var tl = new TimelineMax({repeat:-1});

tl.to(boxes, dur, {
    bezier: [
        { autoAlpha: 0 },
        { autoAlpha: 1 },
        { autoAlpha: 0 }
    ]
});
tl.call(function() {
    // update stuff if you wanted to trigger it somewhere in the middle of the timeline
    // Otherwise, just use the onComplete param of the timeline itself.
});
tl.to(boxes, dur, {x:100});
  • 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...