Jump to content
Search Community

how to use a timeline as a tween group

eigenface test
Moderator Tag

Recommended Posts

I've included a simplified sample of some code from a project I'm working on.  The user initiates a tween by clicking a button.  I want to throw this tween in a timeline with a bunch of other user-initiated tweens (not included in the sample), so I can pause/resume and kill them all en masse.  Because these tweens are user-initiated, they can be created and started at any time, with other tweens already in progress.

 

You can run the sample code to see how this is supposed to look: every time you press the button, the circle starts fading out, and you can press the button repeatedly to reset the fade and start it over.

 

Then you can try uncommenting the last line of code (adding the tweens to the timeline.)  My intention is for this to make no difference to how the circle behaves when you press the button - nothing should change.

 

As you'll see, the behavior is actually quite different with the last line uncommented.  I've tried with and without the second parameter to timeline.add, and I've tried adding overwrite:"all" to the tween (instead of killing it manually), all with different, unsatisfactory results.  Are these behaviors intended?

 

Is there a way to do this, that is, add new tweens to a timeline at arbitrary future times, without affecting the behavior of the tweens?

.

package
{
    import com.greensock.TweenMax;
    import com.greensock.TimelineMax;
    import com.greensock.easing.*;
    
    import flash.display.SimpleButton;
    import flash.display.Sprite;
    
    import flash.events.MouseEvent;
    import flash.events.Event;
    
    public class Main extends Sprite
    {
        protected var tween:TweenMax;
        protected var timeline:TimelineMax;
        protected var circle:Sprite;
        
        public function Main()
        {
            addEventListener(Event.ADDED_TO_STAGE, addedToStageHandler, false, 0, true);
        }
        
        protected function addedToStageHandler(event:Event = null):void
        {
            timeline = new TimelineMax();
            
            circle = new Sprite();
            circle.graphics.beginFill(0);
            circle.graphics.drawCircle(320, 340, 100);
            circle.graphics.endFill();
            addChild(circle);
            
            var upState:Sprite = new Sprite();
            upState.graphics.beginFill(0xFF0000);
            upState.graphics.drawRect(270, 140, 100, 100);
            upState.graphics.endFill();
            
            var button:SimpleButton = new SimpleButton(upState, upState, upState, upState);
            addChild(button);
            
            button.addEventListener(MouseEvent.MOUSE_DOWN, mouseDownHandler, false, 0, true);
        }
        
        protected function mouseDownHandler(event:MouseEvent):void
        {
            if (tween != null) {
                
                tween.kill();
            }
            circle.alpha = 1;
            tween = TweenMax.to(circle, 3, { alpha:0, ease:Quint.easeOut });
            
            //timeline.add(tween, timeline.totalTime());
        }
    }
}
Link to comment
Share on other sites

Please try this:

 

protected function mouseDownHandler(event:MouseEvent):void
        {         
            tween = TweenMax.fromTo(circle, 3, {alpha:1}, { alpha:0, ease:Quint.easeOut});
            timeline.add(tween, timeline.time()) //insert tween in timeline at a time of now / current time().
        }

Be sure to use latest version of GSAP (12.1.2)

http://www.greensock.com/?download=GSAP-AS3

Link to comment
Share on other sites

Thanks, Carl!  Your mouseDownHandler almost works correctly.  The first click jumps to somewhere in the middle of an alpha fade already in progress, depending on how long you wait before clicking the first time.  Subsequent clicks have the desired effect.  I have been using the latest version of GSAP (12.1.2) - are you seeing this first-click problem also?

 

In addition, I just want to get confirmation that I'm not crazy - my original mouseDownHandler looks like it should work, right?  (It exhibits the same weirdness it always did if I switch totalTime to time.)  My mouseDownHandler looks like a different way of writing your mouseDownHandler, but the visible behavior should be the same.  The fact they exhibit different behavior seems buggy.  Or am I missing some subtlety of the API?

Link to comment
Share on other sites

Oh snap. Yeah, I was totally not paying attention to the first tween. I see the jump you are talking about if you wait to click the first time. 

 

What is happening is you are creating a timeline that isn't paused as soon as your app starts. That timeline gets placed on the root timeline (the engines internal master timeline) as soon as it gets created. So lets just say that your timeline has a startTime of 0. If you wait 1 second to add your first tween it gets placed at a time of 0 in your timeline, but timeline's start time in relationship to the root timeline's playhead is now 1 second in the past... so you will jump 1/3 into your first tween's progress. 

 

If that sounds confusing don't worry too much as

 

  • its something we are going address in the next update and this behavior, although not a bug, will work like you were expecting.
  • I have a fix you can use now.

 

When you create your timeline, just set paused:true in the constructor like so:

 

protected function addedToStageHandler(event:Event = null):void
        {
            timeline = new TimelineMax({paused:true});


...

when you add() a new tween, be sure to play the timeline

 

 protected function mouseDownHandler(event:MouseEvent):void
        {         
            tween = TweenMax.fromTo(circle, 3, {alpha:1}, { alpha:0, ease:Quint.easeOut});
            timeline.add(tween, timeline.time()).play()
        }

 

---

 

When you keep the timeline paused initially it will automatically move its start time to the current time of the root timeline when you add the first tween and play it, so you will see the first tween play from the beginning.

 

Again, in the update you won't have to pause initially or force play().

 

I'm quite sure the behavior you were experiencing was all related to this.

Link to comment
Share on other sites

Thanks again, Carl!  Your new mouseDownHandler with the play() call works as expected.

 

However, I still think I've found a bug or inconsistency, which you may want to address in the update.  I still get weird behavior if I add the play() call to my original mouseDownHandler (and pass { paused:true } to the timeline constructor.)  In that case, the first click has the desired effect, but subsequent clicks do nothing at all.  Here is the code to reproduce this apparent bug:

 

package
{
    import com.greensock.TweenMax;
    import com.greensock.TimelineMax;
    import com.greensock.easing.*;
    
    import flash.display.SimpleButton;
    import flash.display.Sprite;
    
    import flash.events.MouseEvent;
    import flash.events.Event;
    
    public class Main extends Sprite
    {
        protected var tween:TweenMax;
        protected var timeline:TimelineMax;
        protected var circle:Sprite;
        
        public function Main()
        {
            addEventListener(Event.ADDED_TO_STAGE, addedToStageHandler, false, 0, true);
        }
        
        protected function addedToStageHandler(event:Event = null):void
        {
            removeEventListener(Event.ADDED_TO_STAGE, addedToStageHandler);
            
            timeline = new TimelineMax({ paused:true });
            
            circle = new Sprite();
            circle.graphics.beginFill(0);
            circle.graphics.drawCircle(320, 340, 100);
            circle.graphics.endFill();
            addChild(circle);
            
            var upState:Sprite = new Sprite();
            upState.graphics.beginFill(0xFF0000);
            upState.graphics.drawRect(270, 140, 100, 100);
            upState.graphics.endFill();
            
            var button:SimpleButton = new SimpleButton(upState, upState, upState, upState);
            addChild(button);
            
            button.addEventListener(MouseEvent.MOUSE_DOWN, mouseDownHandler, false, 0, true);
        }
        
        protected function mouseDownHandler(event:MouseEvent):void
        {
            if (tween != null) {
                
                tween.kill();
            }
            circle.alpha = 1;
            tween = TweenMax.to(circle, 3, { alpha:0, ease:Quint.easeOut });
            
            timeline.add(tween, timeline.time()).play();
        }
    }
}

 

Is this behavior a bug?  I mean, according to the API, the above mouseDownHandler should produce the same result as your new mouseDownHandler, right?  Or no?

Link to comment
Share on other sites

Eigenface,

 

Please test the following code with Jack's new files:

 

package
{
    import com.greensock.TweenMax;
    import com.greensock.TimelineMax;
    import com.greensock.easing.*;
    
    import flash.display.SimpleButton;
    import flash.display.Sprite;
    
    import flash.events.MouseEvent;
    import flash.events.Event;
    
    public class Main extends Sprite
    {
        protected var tween:TweenMax;
        protected var timeline:TimelineMax;
        protected var circle:Sprite;
        
        public function Main()
        {
            addEventListener(Event.ADDED_TO_STAGE, addedToStageHandler, false, 0, true);
        }
        
        protected function addedToStageHandler(event:Event = null):void
        {
            removeEventListener(Event.ADDED_TO_STAGE, addedToStageHandler);
            
            timeline = new TimelineMax();
            
            circle = new Sprite();
            circle.graphics.beginFill(0);
            circle.graphics.drawCircle(320, 340, 100);
            circle.graphics.endFill();
            addChild(circle);
            
            var upState:Sprite = new Sprite();
            upState.graphics.beginFill(0xFF0000);
            upState.graphics.drawRect(270, 140, 100, 100);
            upState.graphics.endFill();
            
            var button:SimpleButton = new SimpleButton(upState, upState, upState, upState);
            addChild(button);
            
            button.addEventListener(MouseEvent.MOUSE_DOWN, mouseDownHandler, false, 0, true);
        }
        
        protected function mouseDownHandler(event:MouseEvent):void
        {
            if (tween != null) {
                
                tween.kill();
            }
            circle.alpha = 1;
            tween = TweenMax.to(circle, 3, { alpha:0, ease:Quint.easeOut });
            
            timeline.add(tween, timeline.time());
        }
    }
}

You will notice that in addition to it working:

  • You no longer need to start the timeline paused
  • You don't need to chain the play() call onto the add()

Let us know if things work for you now, and thanks also for your diligence in questioning the odd behavior.

Link to comment
Share on other sites

With GSAP 12.1.3, the above simplified example works correctly, but when I try it with my project, I still get weird behavior.  I've included another sample that demonstrates the problem.  All I've done is add a parent timeline.

 



package
{
    import com.greensock.TweenMax;
    import com.greensock.TimelineMax;
    import com.greensock.easing.*;
    
    import flash.display.SimpleButton;
    import flash.display.Sprite;
    
    import flash.events.MouseEvent;
    import flash.events.Event;
    
    public class Main extends Sprite
    {
        protected var tween:TweenMax;
        protected var timeline:TimelineMax;
        protected var parentTimeline:TimelineMax;
        protected var circle:Sprite;
        
        public function Main()
        {
            addEventListener(Event.ADDED_TO_STAGE, addedToStageHandler, false, 0, true);
        }
        
        protected function addedToStageHandler(event:Event = null):void
        {
            removeEventListener(Event.ADDED_TO_STAGE, addedToStageHandler);
            
            timeline = new TimelineMax();
            parentTimeline = new TimelineMax();
            parentTimeline.add(timeline);
            
            circle = new Sprite();
            circle.graphics.beginFill(0);
            circle.graphics.drawCircle(320, 340, 100);
            circle.graphics.endFill();
            addChild(circle);
            
            var upState:Sprite = new Sprite();
            upState.graphics.beginFill(0xFF0000);
            upState.graphics.drawRect(270, 140, 100, 100);
            upState.graphics.endFill();
            
            var button:SimpleButton = new SimpleButton(upState, upState, upState, upState);
            addChild(button);
            
            button.addEventListener(MouseEvent.MOUSE_DOWN, mouseDownHandler, false, 0, true);
        }
        
        protected function mouseDownHandler(event:MouseEvent):void
        {
            if (tween != null) {
                
                tween.kill();
            }
            circle.alpha = 1;
            tween = TweenMax.to(circle, 3, { alpha:0, ease:Quint.easeOut });
            
            timeline.add(tween, timeline.time());
        }
    }
}


 

Now, only the first click behaves as expected.  Subsequent clicks do nothing while the alpha fade is in progress.  If I click after the fade is complete, the circle appears at full alpha, and once again subsequent clicks do nothing.

 

Is this behavior correct?  Is there a way to make this work?

Link to comment
Share on other sites

Yep, that's actually the correct behavior (not a bug) - let me explain...

 

...actually, first let me give you the short answer and then I'll give you the "why"...

 

Just add smoothChildTiming:true to your timeline:

parentTimeline = new TimelineMax({smoothChildTiming:true});

Okay, now for the "why"...

 

Every animation sits on a timeline (by default it's the root timeline), so every animation has a startTime which is simply where it's positioned on its parent timeline. The virtual playhead moves along that root timeline and as it intersects with tweens/timelines, it renders them accordingly. This is an extremely powerful and flexible system. Things can be nested as deeply as you want, each timeline can have its own timeScale, etc. and the playhead is what controls where everything renders. You can seek() a timeline or tween which controls where its playhead is, but keep in mind that there's a master playhead on the root timeline, so when you seek() a tween (or timeline) and you want it to continue to play from that spot naturally, it must essentially pick up that tween/timeline and align its virtual playhead with the root's, thus affecting its startTime (position on the root timeline). The entire system remains perfectly synchronized this way. 

 

All timelines have a "smoothChildTiming" property. If it is "true", it allows children to automatically adjust their positions in order to align the playhead on-the-fly. For example, if you reverse() a tween, it'll flip it around and ensure that the startTime is adjusted so that the playhead is exactly aligned with the root one and it'll continue playing from there. If the parent timeline's "smoothChildTiming" is false, the tween's startTime would be locked-in, so the reverse would simply change the tween's orientation.

 

smoothChildTiming is "true" for the root timeline, but "false" by default for TimelineLites and TimelineMaxes. This is very intentional because most people want to have very tight control of the position of things inside timelines that they create, but for the root the preferred behavior is for things to shift around as needed in order to prioritize seamless playback. 

 

If none of that made sense, don't worry. Just set smoothChildTiming to true and it'll work as you were expecting. I'd probably need to draw some visuals and maybe even walk through the concepts in a video to make it crystal clear, but trust me - the behavior you saw was indeed the accurate one and there are good reasons :) 

  • Like 1
Link to comment
Share on other sites

Thanks so much for this detailed explanation.  My previous example works correctly if I add smoothChildTiming:true to the parentTimeline as you've indicated.

 

To be honest, I didn't fully understand the explanation.  I have a few questions, to make sure I'm setting things up correctly in my project.

 

In my project, I want to put user-generated tweens in several different bottom-level timelines so I can pause/kill them by category (carsTimeline, trainsTimeline...), as well as add the timelines as children of a common parent timeline so I can pause/kill everything easily.  Also, I want the bottom-level timelines to have autoRemoveChildren:true, so I won't accumulate completed tweens (which are never used again.)

 

1. Do I need to add smoothChildTiming:true to every ancestor timeline, except the bottom-level timeline which contains the tweens, like so?

 



carsTimeline = new TimelineMax({ autoRemoveChildren:true });
parentTimeline = new TimelineMax({ smoothChildTiming:true });
grandParentTimeline = new TimelineMax({ smoothChildTiming:true, paused:true });
...
carsTimeline.add(TweenMax.delayedCall(carDelay, spawnCar), carsTimeline.time());
...
parentTimeline.add(carsTimeline);
grandParentTimeline.add(parentTimeline);
...
grandParentTimeline.play()


 

As you can see, I also have paused:true in the top-level timeline, so I can add some initial tweens (in fact, delayed calls) and start them later.

 

2. I don't need to use totalTime(), right?  (time() honors delays, just not repeatDelays)

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...