greensock / GSAP

GSAP (GreenSock Animation Platform), a JavaScript animation library for the modern web
https://gsap.com
19.56k stars 1.72k forks source link

Understanding timelines #218

Closed pixelass closed 7 years ago

pixelass commented 7 years ago

I consider myself a smart guy but after 2 years of trying over and over I guess I have to give up on understanding how GSAPs timeline tools work.

I don't even know where to look.
The documentation seems unclear and are missing links to examples.
The examples I can find on codepen are often not commented and have no links to the docs for that feature. They are also written in such a compact way that I have to pen each one and format the code before being able to read what's going on.

I fail at creating the most simple timelines because I simply don't understand how it works. I'm actually not sure if I'm just to dumb or if I'm not the only with this problem

E.g. how are these different when I simply use tl.progress() to animate the timeline?

tl.from(slide, 1, {x: '-100%'}) // should be at 0
  .to(slide, 1, {x: '0%'}) // should be at 0.5
  .to(slide, 1, {x: '100%'}) // should be at 1
tl.from(slide, 1, {x: '-100%'}) // is 0
  // 0.5 => x: '0%'
  .to(slide, 1, {x: '100%'}) // is 1

I have never seen a more complicated API sorry.

OSUblake commented 7 years ago

@pixelass I'm really surprised to hear this as I'm quite familiar with your work, and would also consider you to be very smart.

Does this demo correctly illustrate the problem above? https://codepen.io/osublake/pen/5bcce1e5fcb99b038ddb4bfa011e4a62?editors=0010

That's how a timeline will work in pretty much any animation library. I think you might have a fundamental misunderstanding of what a timeline is. There really isn't an equivalent of a timeline in CSS. A timeline is a container to group and sequence different animations together. The sequencing may look like it's creating keyframes, but it's not.

To create multiple keyframes, you would need to use something like the BezierPlugin to do the interpolation.

var tl = new TimelineMax({ paused: true })
  .to(slide, 1, {
    bezier: {
      values: [
        { xPercent: -100 },
        { xPercent: 0 },
        { xPercent: 100 }
      ]
    }
  });

So in the demo above, the first timeline has 3 different animations that will play as a sequence. If you were to plot the ease of the animation, it would appear as a sawtooth like this.

pixelass commented 7 years ago

I am working on some timeline based libraries and am quite familiar with timelines. I just don't understand the API of gasp timelines.

I wanted to include a demo for gasp in one of my libraries. I've never used gasp before since every time I give it a try it ends in frustration. Maybe all that duration and delay stuff is too spaghetti for me to understand. Maybe I expected it to work differently and am unable to adjust. Who knows.

My libraries just expose a timeline [0,1] which I feed into .progress(). Not mit to get wrong thereπŸ€“

jackdoyle commented 7 years ago

Howdy @pixelass. I appreciate you taking the time to ask about this and share your experience/confusion. I'm bummed to hear about the negative experience you've had digging into GSAP's API.

I must admit that we typically hear the opposite - people often tell us that the API is really nice, clean, and has good developer ergonomics. But I realize that various people come from different backgrounds and what makes sense to some is baffling to others. We never want anyone feeling like they're "dumb". I bet others may have shared your sentiments but been afraid to voice them, so thanks for having the guts to step out.

Before we go further, I'd strongly recommend watching the videos at https://greensock.com/sequence-video and https://greensock.com/position-parameter, as they might make a lot of things fall into place quickly.

Let me take a shot at explaining things from the 10,000-foot view (I'm sure you already know much of this, but bear with me)...

TWEENS

Everything boils down to individual "tweens". A tween has starting and ending values, and it just interpolates between them (and applies them to the appropriate properties). Like element.width from 100 to 200 over the course of 1 second.

GSAP is very object-oriented. So each tween is an instance. A single tween can control multiple properties of course, but its timing and easing are instance-specific.

Many animation engines require that you feed in both the starting values and the ending values for tweens/animations. Sometimes that's useful. GSAP has .fromTo() methods that allow that kind of thing. But more often it's annoying to have to always define all those values. Many times you don't even know what they are (like if animations are based on dynamic user interaction). Animators often just care about one end of the animation, like "animate TO width:200" (from whatever it is currently). That's what a .to() tween is. GSAP will dynamically figure out the current values and use those as the starting ones.

Sometimes animators are starting with the end state and they want to animate FROM other values (to whatever they are now). Perfect for intro animations. Set your page up the way it should look at the end of the animation, and just say .from(...{width:0}) for example.

So the only difference between to(), from() and fromTo() tweens is which values you're feeding in (end, start, or both).

TIMELINES

Timelines are just containers for tween instances (and/or other timelines). They solve a few problems:

In the very old days, you'd create tween instances and then put them into timelines wherever you want, like:

//old (long, cumbersome) way...
var tween1 = TweenMax.to(...);
var tween2 = TweenMax.to(...);
var timeline = new TimelineLite();
timeline.insert(tween1, 0); //put it at the start (time of 0)
timeline.append(tween2); //put it at the end (right after tween1)

But eventually we added "convenience" methods to the timelines to streamline the code:

var timeline = new TimelineLite();
timeline.to(..., 0); //create a to() tween and put it at a time of 0
timeline.to(...); //the default is to add the tween to the end of the timeline, so we don't even need to define the position parameter

Under the hood, it's basically the same thing - 2 tweens are placed into a timeline at certain spots.

KEY CONCEPT

Every tween and every timeline has a playhead which basically controls how/where it renders the values. So if you put the playhead halfway through, it'll render the values accordingly. A timeline's playhead sweeps over its child tweens/timelines and that's what controls the playhead of all of its children. Think of it like it cascades down. So visually, if you arranged all the tweens in a timeline like horizontal boxes with various start times (time moves from left to right), you'd draw a vertical line representing the playhead. On every tick, that playhead moves and things render accordingly.

                        PLAYHEAD
|--------------timeline-----|-----------|
|--tween1--|                |
           |-----tween2-----|-----------|

GSAP's API lets you control virtually anything - the playhead position of the parent timeline, the startTime of any child, you can play/pause/reverse or alter the timeScale, etc.

A lot of this is laid out in the "getting started" guide - have you seen that? https://greensock.com/get-started-js/

YOUR EXAMPLE

It almost looks like you're trying to think in terms of "keyframes". Is that right? If you think in terms of tweens instead, where you're defining the to and/or from values and a duration, you could simplify your example to a single line of code:

tl.fromTo(slide, 3, {xPercent:-100}, {xPercent:100, ease:Linear.easeNone});

//then, you could set its progress to halfway through: 
tl.progress(0.5); //xPercent = 0.

Let me narrate your original example:

//animate translateX from -100 to whatever it currently is (0 in this example) over the course of 1 second
tl.from(slide, 1, {x: '-100%'})

  //next (from 1-2 seconds on the timeline), animate translateX from whatever it is at the start of the tween (0 in this case) to 0 over 1 second...which means nothing will happen because you're tweening from 0 to 0
  .to(slide, 1, {x: '0%'}) // should be at 0.5

  //next (from 2-3 seconds on the timeline), animate translateX from whatever it is at the start of this tween (0 in this case) to 100%
  .to(slide, 1, {x: '100%'}) // should be at 1

So you built a 3-second long animation. In the middle, you've got a tween that doesn't really do anything (you're animating from 0 to 0 for 1 second).

Make more sense now? I think what might have thrown you off is the from() tween. Don't feel bad, though, because from() tweens can be tricky to conceptualize at first.

If you have any suggestions for API improvement, we're all ears. I really think that once you get over the initial learning curve, you'll fall in love with the API. At least I hope so. From asking around, that's what usually happens. Again, sorry it's been frustrating for you though.

pixelass commented 7 years ago

@jackdoyle thx for the looong message. I will read through it later (I just woke up and skimmed it)

Let me explain what I understand and what I don't understand.

The drawing below shows a timeline. If I jump to 1s My starting point would be when sequence 1 stopped sequence 2 is starting and sequence 3 is running.

Considering a total animation time of 10s I could also jump to that point by passing 0.1 into the timeline, which just says that I am at 10% of the animation. Using a value from 0 to 1 is amazing because we can just run it through various easing functions before sending it to the timeline.

0s        1s        2s        3s
┏┅┅┅┅┅┅┅┅┅┻┅┅┅┅┅┅┅┅┅┻┅┅┅┅┅┅┅┅┅┻
┣━━━━━━━━━┫         ┣━━━━━━━━━┫
┃         ┣━━━━━━━━━━━━━━━━━━━┫
┃    ┣━━━━━━━━━┫

To me the most obvious API would be something like this. (just a very declarative way of expressing the timeline)

const tl = new Timeline()
tl.seq([{
  start: 0,
  end: 1,      
  from: {x: 50}],
  to: {x: 100 }
}],
[{
  start: 2,
  end: 3,      
  from: {x: 100}],
  to: {x: 50 }
}])

When I look at the code above I immediately know what's going on. I know that sequence 1 startst at 0 seconds and stops at 1 seconds, then remains at that point until it starts again at 2 seconds...

With GSAP (And I will get this example wrong)

const tl = new Timeline()
tl.from(seq1, ??? {})

Sorry I don't get it. Why do I have to pass a duration into from. This is as far as I get with all my attempts. It makes no sense whatsoever. Whenever I look at GSAP timeline examples I feel the same. Just from reading the code I have no idea what's going on.

I guess what I need is being able to set start and stop points to make sense of it.

Adds a TweenLite.from() tween to the end of the timeline (or elsewhere using the "position" parameter)

Ok let's give it another try

const tl = new Timeline()
tl.from(seq1, 1 {x: 50}).to(seq1, 1 {x: 100})

I'm pretty sure the example above is wrong and I still don't get the duration parameter of from.

I think it should be

const tl = new Timeline()
tl.from(seq1, 0 {x: 50}).to(seq1, 1 {x: 100})

So it turns out chaning plays a role. (Still writing code that is most probably 100% wrong.)

const tl = new Timeline()
tl
  .from(seq1, 1 {x: 50})
  .from(seq2, 1 {y: 10}, -0.5) // subtract 0.5 to make it start 0.5 seconds before the other one ends
  .to(seq1, 1 {x: 100}) // So if I have to continue I have one big spaghetti of timeline logic
const tl = new Timeline()
tl
  .from(seq1, 1 {x: 50})
  .to(seq1, 1 {x: 100}) // where am I now? at 1s or 2s
  .from(seq2, 1 {y: 10}, -0.5) // subtract 0.5 to make it start 0.5 seconds before the other one ends

Argghhh, just thinking about this makes me furious, I feel a big knot tightening in my head and feel like banging my head against the wall. I don't even want to write code like that because I have problems understanding or setting it up in the first place, so I know exactly that I will never be able to modify or maintain the animation later. My brain stops me right there, preventing me from writing the code in the first place.

I'm not sure if all of that makes any sense. And it might just be the way my brain works. I wiill never figure it out and I am pretty sure that I won't fall in love with it. We're talking about 2 years of trying to understand this but I learned the gist of fractal math in one evening ;).

pixelass commented 7 years ago

I also played around with fromTo because the name sounds like it might be doing what I want but it turns out it didn't. It was just another method that I WILL NOT understand EVER.

jackdoyle commented 7 years ago

I think I see what's causing the confusion - it's a fundamental misunderstanding of "from()". Once you read my super long response above, it'll probably make more sense. If not, let me know and I'll try to explain it a different way.

Here's a way to get EXACTLY what you were asking for in the API (although your example didn't have any way of defining the target, so I added that):

TimelineLite.prototype.seq = function(data) {
  var v, i;
  for (i = 0; i < data.length; i++) {
    v = data[i];
    v.to.immediateRender = false; //by default, fromTo() renders the starting values immediately, but we don't want that here.
    this.fromTo(v.target, v.end - v.start, v.from, v.to, v.start);
  }
  return this;
}

That allows you to do this:

var tl = new TimelineLite();
tl.seq([{
    target: "#box1",
    start: 0,
    end: 1,
    from: {x: 50},
    to: {x: 100}
  },
  {
    target: "#box1",
    start: 2,
    end: 3,
    from: {x: 100},
    to: {x: 50}
  }
]);

Codepen: https://codepen.io/GreenSock/pen/c580876205b86f1ccfc84b017a3d304b?editors=0010

Does that help?

While you may find that API more intuitive, I'll warn you that when you start building more complex stuff, it'll get cumbersome. Imagine coding a timeline with 30 animations and then the client is like "please make the first one 0.5 seconds longer". Doh! Now you've gotta adjust the "start" and "end" values for all 30 of them. Ouch.

We designed the GSAP API to empower a fluid, experimental workflow (after all, most animators constantly play with timings and eases). Once you grasp the basics, hopefully it'll become apparent why we made the choices we did. Carl is working on a video right now that'll demonstrate a particular workflow technique that might totally change your perspective. It simply wouldn't be possible with an API like you suggested (no offense intended - I totally see why you found that API intuitive for the simple example).

By the way, did you know you can insert things at absolute times in the timeline OR values that are relative to the end? Example:

tl.to(seq1, 1, {x:100}, "-=0.5"); //starts 0.5 seconds before the end of the previous one (relative)
tl.to(seq1, 1, {x:100}, 1.25); //starts at EXACTLY 1.25 seconds from the start of the timeline

You can even dynamically grab the most recently-added child and use its endTime to position things:

tl.to(seq1, 1, {x:100}, tl.recent().endTime()); //starts at exactly where the most recently-added child ends

I'm curious - did you already watch all 3 videos I suggested? I really think those might clear things up. And again, I apologize if the API seems obtuse initially.

It sounds like maybe you made up your mind, though, that GSAP's API is poorly designed and won't be learnable by you which makes me sad but I recognize that it's not a perfect tool for everyone and it's okay if you prefer to avoid GSAP. Hopefully some of this dialog will lead to some pleasant surprises.

jackdoyle commented 7 years ago

I know you really wanted to be able to just pass a value between 0-1 and have the timeline render its progress accordingly, so I wanted to make sure you know how easy that is with GSAP:

tl.progress(0.5); //renders halfway through.
tl.progress(1); //renders finished.
console.log( tl.progress() ); //reports the current progress (getter). 

In fact, you can even tween the progress! Yeah, literally use one tween to animate the playhead of another tween or timeline.

var tl = new TimelineLite({paused:true});
tl.to(...).to(...); //populate it however you want...
//now animate the playhead from halfway to the very end over the course of 1 second using another tween:
TweenLite.fromTo(tl, 1, {progress:0.5}, {progress:1, ease:Linear.easeNone});

Or if you prefer to get/set specific time (in seconds) rather than a 0-1 progress, it's easy:

tl.time(1.25); //jumps to a time of 1.25 seconds
jackdoyle commented 7 years ago

[bump]

@pixelass any feedback? I spent a lot of time trying to clarify things.

pixelass commented 7 years ago

Way to much time and thank you a lot. πŸ€— I haven't gotten around to play around with the info yet. I'm currently on a break.

pixelass commented 7 years ago

@jackdoyle I carefully read all your suggestions and they helped a lot. I mean a whole lot. Thanks for all the detail. Sadly I came to the conclusion that I won't be using GSAP since I still feel very uncomfortable with the API.

That being said I still can't wrap my head around it and it seems too complex for my tiny brain.

I'm sorry if this is not the expected outcome. I know you spent a lot of time on explaining this.

jackdoyle commented 7 years ago

@pixelass no problem. Thanks for circling back and giving things a read. I appreciate the openness and candor. I still think that if you played with it for a while (after gaining the new understanding), you'd probably dig it and realize it's not as obtuse as you originally thought, but that's okay - GSAP isn't a perfect fit for everyone.

Thanks again!

DennisSmolek commented 7 years ago

Just wanted to add that this is probably the most thorough reply to a user's frustration vs. a bug I've ever seen on any repo... Most just point to SO.. Also, great reading on how the api is working under the hood

OpherV commented 6 years ago

despite the OP not being appeased I found the info in this issue enlightening and super useful!

jangir-ritik commented 9 months ago

@jackdoyle You've been so considerate and positive despite OP being a total jerk.

pixelass commented 9 months ago

I wasn't being a jerk. I was having several personal issues which then resulted in a a burn-out.

I'm not always proud of my actions. The time when this happened was one of the worst in my life and I'm happy to have made it through.

Thank you for understanding.

jackdoyle commented 9 months ago

@pixelass thanks for sharing that. Yeah, sometimes life kicks us in the teeth and our hurt/frustration leaks into other areas. I've gone through some very difficult stuff in my personal life that had a profound effect on my ability to even process work-related things.

No judgement here. All good.

I'm glad you made it through too. Hopefully you're in a better place now and have totally jumped on board with GSAP 😊

pixelass commented 9 months ago

A heart emoji cannot justify the love I wanna give back to you @jackdoyle

Thanks for the kind words.