Jump to content
GreenSock

Johan ⚡️ Nerdmanship

How to create unique sub-tls with loop and add to main-tl

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 have created a grid of rects in an SVG element and for each rect in the grid I'm trying to create a unique sub-timeline to add to a main timeline.

 

I haven't isolated the problem in the pen, but I will if necessary. Line 1–70 is just code for generating the grid, as far as I know, you can just ignore this.

 

The creation of timelines start at line 70. I have an array of 12 rects and I want to loop through them and create sub-timelines with random durations and add them to the main timeline.

var rects = selectAll(".rectangle"); // Array of rects
var mainTl = new TimelineMax(); // Main timeline

// Loop through the array and output a timeline for each rect in the array
function makeTls() {
  for(var i = 0; i<rects.length; i++){
    var tl = new TimelineMax(); // Create a sub-timeline
    var ranDur = randomBetween(0.5, 3); // Generate a random duration
    
    tl.from(rects[i], ranDur, { scale: 0 }); // Specify unique tween
    
    return tl; // Return a unique timeline 
  }
}

mainTl.add( makeTls() ); // How to add a dynamic number of tls to the main tl?

I understand why this doesn't work, but I can't figure out how to output as many unique timelines as I have rects in my array and add them to the main timeline. Can you point me in a direction?

 

I'm aware that I can use staggerFrom() and cycle the duration property with a randomising function, but I wish to explore the loop option for learning purposes.

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

Link to comment
Share on other sites

  • Solution

Using for loops can cause problems if you're not careful, like this thread. 

http://greensock.com/forums/topic/14645-for-loop-returning-incorrect-counter-number/

 

That's why I always show people how to use arrays to do stuff like building timelines. There's an array method called reduce that does a really nice job of reducing a bunch items into a single item, i.e. a bunch of timelines into a single timeline.

function makeTls(parentTL) {
  
  rects.reduce(function(parent, rect) {
    var tl = new TimelineMax();
    tl.from(rect, randomBetween(0.5, 3), { scale: 0 });
    return parent.add(tl);
  }, parentTL);
}

rects = Array.from(rects); // Convert to array
makeTls(mainTl);

It might be confusing if you've never seen that method, but once you learn it it's really convenient. The return statement is adding the new timeline to the parent timeline. Here's a thread where I explain it a bit better, and how it would look like as a for-loop.

http://greensock.com/forums/topic/14486-help-with-overlapping-items-on-repeat-timeline/?hl=reduce#entry61899

 

Running it your demo...

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

  • Like 5
Link to comment
Share on other sites

"Give a man a fish and you feed him for a day; teach a man to fish and you feed him for a lifetime."

 

Thanks a bunch, Blake – great answer! I feel like I got a fish, fishing equipment, fishing lessons and some fishing literature recommendations. I can see how this is a powerful method in combination with GSAP.

 

I'm new to the reduce() method (as I am to most methods   :oops: ), and yes, it's indeed a little difficult to wrap my head around. I'm gonna think out loud here and hopefully provide some value to others at my skill level.

 

The reduce() method pretty much take all my things and does whatever I specify with each one, then accumulates them into one thing and returns it. What's tricky to me are mainly two things, 1) what the "accumulation" means and 2) what the parameters that I input means.

 

1)

The purpose of the method is to reduce many things into one thing. So when it accumulates, it just means that the one thing is added to the other – each round it adds one unit to a whole. Like playing with Legos, you got a stack of a dozen pieces and you wanna build a tower. Each round you take one piece from the stack, you orient it properly, and then add it to the tower. The pieces are accumulated to a tower one piece at the time.

 

2)

In this case the method takes three parameters; previousValue ( parent ), currentValue ( rect ) and initialValue ( new TimelineMax() ).

  • previousValue is whatever has been accumulated so far and starts at either the first lego piece in the stack or whatever initialValue is specified as
  • currentValue is the lego piece you're working with right now and starts at either the first or the second piece depending on if previousValue starts at the first piece or initialValue
  • initialValue is optional and if specified it's what the method starts with (as previousValue), i.e. a base plate to build the lego tower upon
rects.reduce(function(parent, rect) { // array.reduce(function(previousValue, currentValue)
  var tl = new TimelineMax(); // do the next three lines of code to each value in my array...
  tl.from(rect, 1, { scale: 0 }); // "rect" is "currentValue", which in this case starts at rect[0] because an initialValue is provided
  return parent.add(tl); // "parent" is "previousValue", which in this case start at "new TimelineMax()" because the initialValue is provided
}, new TimelineMax() ); // this is the initialValue

So when running it, I guess this happens...

//first round returns
new TimelineMax() // this is previousValue as specified by initialValue
  .add(new TimelineMax().from(rect[0], 1, { scale: 0 })) // rect[0] is currentValue

//second round returns
new TimelineMax().add(new TimelineMax().from(rect[0], 1, { scale: 0 })) // this is previousValue from round 1
  .add(new TimelineMax().from(rect[1], 1, { scale: 0 })) // rect[1] is currentValue

//third round returns
// whatever was returned from round 2 is previousValue
// rect[2] is currentValue and is injected to the specified tl and is accumulated to previousValue

//nth round returns whatever was returned from the previous round plus the current round

//if my array contains 12 rect the reduce() method spits out something like this
new TimelineMax().add(new TimelineMax().from(rect[0], 1, { scale: 0 }))/* ... */.add(new TimelineMax().from(rect[11], 1, { scale: 0 }))

I learned a lot writing this, given it's correct. :mrgreen:  Please correct me if I screwed anything up!

 

Thanks again, Blake!

  • Like 2
Link to comment
Share on other sites

It sounds like you have a pretty good understanding of how it works. It can be really confusing at first because it's probably unlike any method you've ever worked with. Understanding how the accumulation works is definitely the hardest part because the accumulator can be anything, so there are no rules about how to use it, other than that you must return it.

 

I think the easiest way to understand how use array iterator methods like forEach, filter, map, reduce, etc, is to compare them to jQuery's .each() method. They all loop through a collection, but do something a little different. And except for reduce, they all have the same callback signature, which is slightly different than jQuery's. In jQuery, a callback is called with these arguments...

collection.each(moveRight);

function moveRight(i, element) {
  TweenLite.to(element, 1, { x: 100 });
}

An array callback uses these arguments...

collection.forEach(moveRight);

function moveRight(element, i, array) {
  TweenLite.to(element, 1, { x: 100 });
}

You can name them whatever you want, and you don't have to include what you aren't going to use. So for that example you really only need the element.

collection.forEach(moveRight);

function moveRight(element) {
  TweenLite.to(element, 1, { x: 100 });
}

The reduce callback is pretty much the same, except it has an extra argument, which is the start or previous value i.e. the accumulator, which can be anything. Here's an example of using an element as the accumulator to append new elements to.

[1,2,3,4].reduce(wrap, myElement);

function wrap(parent, num, index, array) {
  var element = document.createElement("div");
  element.textContent = num;
  parent.appendChild(element);
  return parent;
}

Here's a demo of that...

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

 

Just like in the forEach example above, you may not always need to include every argument. So for that example, you don't need to include the last two arguments. Most of the time you only need the first two, but here's an example that uses all four to create rainbow text. You can use the current index and length of the array to calculate a hue value. The accumulator is just an empty string "". If the syntax looks weird, it's because I'm using the new string template feature. 

var text = "Here's an example of rainbow text.".split("");

document.body.insertAdjacentHTML("beforeend", text.reduce(rainbowText, ""));

function rainbowText(result, letter, i, source) {
  var color = `hsl(${i / source.length * 360}, 100%, 50%)`;
  return result += `<span style="color: ${color};">${letter}</span>`;
}

And here's the what the rainbow text looks like...

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

 

Those are good examples of how to use reduce by itself, but the real power of reduce can be seen when you chain other array methods with it, particularly map and filter. This will allow you to compose your animations using small, reusable blocks of code.

 

Here's an example of how you can animate a grid in a checkerboard pattern using filter and reduce. Filter is called first on the collection, returning either even or odd elements. From there, a reduce method is called on the elements, creating a new set of animations. Notice how easy it would be to change out the filter or reduce calls, giving you a lot of flexibility. 

timeline
  .add(cells.filter(isEven).reduce(fadeIn, new TimelineMax()))
  .add(cells.filter(isOdd).reduce(fadeOut, new TimelineMax()), "+=0.5")

function fadeIn(tl, cell) {
  return tl.fromTo(cell, 0.75, { scale: 0.5, autoAlpha: 0 }, { scale: 1, autoAlpha: 1 }, 0);
}

function fadeOut(tl, cell) {
  return tl.fromTo(cell, 0.75, { scale: 1, autoAlpha: 1 }, { scale: 0.5, autoAlpha: 0 }, 0);
}

function isEven(cell, i) {
  return !(i % 2);
}

function isOdd(cell, i) {
  return !!(i % 2);
}

Here's the demo for that...

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

 

That's a pretty simple example, but it does a good job of showing how you can mix-and-match different methods together to dynamically create your animations.

  • Like 5
Link to comment
Share on other sites

You two are frying my brain.

 

I always thought of reduce as folding the array elements one by one with a check between the previous (or initial) value agains the current value.

 

Like:

[0,1,2,3,4,5].reduce( function(previous, current) {
 return previous + current;
})

// 15
Link to comment
Share on other sites

Yup. That's all it does. A recursive function that calls itself with a previous value. This makes it real easy to count or build strings like my

See the Pen 27d446d177645de8ca2c1e0ba5de9529 by osublake (@osublake) on CodePen

example. I guess you can call that a poor man's version of the SplitTextPlugin.

 

What makes reduce so powerful is that the previous value can be anything, and you can transform values without mutating the source. That's why reduce and other array methods are so common in reactive programming libraries like React and ReactiveX (Rx). 

[0,1.1,2.2,3.3,4.4,5.5].reduce( function(previous, current) {
 return previous + Math.round(current);
}, 100)

// 116

I noticed you made a comment about using functions to reduce the amount of repeated code in this post. That

See the Pen PzjdkK?editors=0010 by arunanthony (@arunanthony) on CodePen

would be perfect for reduce because most of the code follows a pattern. I'm not going to take the time to do this, but I'd be willing to be that you could replace 90% of that code with a few reduce calls. 

  • Like 2
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.
×