// // Nenya library - tools for developing networked games // Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved // https://github.com/threerings/nenya // // This library is free software; you can redistribute it and/or modify it // under the terms of the GNU Lesser General Public License as published // by the Free Software Foundation; either version 2.1 of the License, or // (at your option) any later version. // // This library is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // Lesser General Public License for more details. // // You should have received a copy of the GNU Lesser General Public // License along with this library; if not, write to the Free Software // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA package com.threerings.media.animation; import java.util.ArrayList; import java.awt.Graphics2D; import java.awt.Rectangle; import com.google.common.collect.Lists; import com.samskivert.util.StringUtil; import static com.threerings.media.Log.log; /** * An animation that provides facilities for adding a sequence of animations that are fired after * a fixed time interval has elapsed or after previous animations in the sequence have completed. * Facilities are also provided for running code upon the completion of animations in the * sequence. */ public class AnimationSequencer extends Animation { /** * Constructs an animation sequencer with the expectation that animations will be added via * subsequent calls to {@link #addAnimation}. * * @param animmgr the animation manager to which to add our animations when they are ready to * start. */ public AnimationSequencer (AnimationManager animmgr) { super(new Rectangle()); _animmgr = animmgr; } /** * Adds the supplied animation to the sequence with the given parameters. Note that care * should be taken if this is called after the animation sequence has begun firing animations. * Do not add new animations after the final animation in the sequence has been started or you * run the risk of attempting to add an animation to the sequence after it thinks that it has * finished (in which case this method will fail). * * @param anim the animation to be sequenced, or null if the completion action should be run * immediately when this "animation" is ready to fired. * @param delta the number of milliseconds following the <em>start</em> of the previous * animation in the queue that this animation should be started; 0 if it should be started * simultaneously with its predecessor in the queue; -1 if it should be started when its * predecessor has completed. * @param completionAction a runnable to be executed when this animation completes. */ public void addAnimation (Animation anim, long delta, Runnable completionAction) { // sanity check if (_finished) { throw new IllegalStateException("Animation added to finished sequencer"); } // if this guy is triggering on a previous animation, grab that // good fellow here AnimRecord trigger = null; if (delta == -1) { if (_queued.size() > 0) { // if there are queued animations we want the most // recently queued animation trigger = _queued.get(_queued.size()-1); } else if (_running.size() > 0) { // otherwise, if there are running animations, we want the // last one in that list trigger = _running.get(_running.size()-1); } // otherwise we have no trigger, we'll just start ASAP } AnimRecord arec = new AnimRecord(anim, delta, trigger, completionAction); // Log.info("Queued " + arec + "."); _queued.add(arec); } /** * Convenience wrapper to add an animation to run at the same time as the previous one. */ public void addAnimation (Animation anim) { addAnimation(anim, 0, null); } /** * Convenience wrapper to add an animation to run after the previous one. */ public void appendAnimation (Animation anim) { addAnimation(anim, -1, null); } /** * Clears out the animations being managed by this sequencer. */ public void clear () { _queued.clear(); _lastStamp = 0; } @Override public void tick (long tickStamp) { if (_lastStamp == 0) { _lastStamp = tickStamp; } // add all animations whose time has come while (_queued.size() > 0) { AnimRecord arec = _queued.get(0); if (!arec.readyToFire(tickStamp, _lastStamp)) { // if it's not time to add this animation, all subsequent // animations must surely wait as well break; } // remove it from queued and put it on the running list _queued.remove(0); _running.add(arec); // note that we've advanced to the next animation _lastStamp = tickStamp; // fire in the hole! arec.fire(tickStamp); } // we're done when both lists are empty // boolean finished = _finished; _finished = ((_queued.size() + _running.size()) == 0); // if (!finished && _finished) { // Log.info("Finishing sequence at " + (tickStamp%10000) + "."); // } } @Override public void paint (Graphics2D gfx) { // don't care } @Override public void fastForward (long timeDelta) { _lastStamp += timeDelta; } @Override public void viewLocationDidChange (int dx, int dy) { super.viewLocationDidChange(dx, dy); // track cumulative view location changes so that we can adjust our // animations before we start them _vdx += dx; _vdy += dy; } /** * Called when the time comes to start an animation. Derived classes may override this method * and pass the animation on to their animation manager and do whatever else they need to do * with operating animations. The default implementation simply adds them to the media panel * supplied when we were constructed. * * @param anim the animation to be displayed. * @param tickStamp the timestamp at which this animation was fired. */ protected void startAnimation (Animation anim, long tickStamp) { // account for any view scrolling that happened before this animation // was actually added to the view if (_vdx != 0 || _vdy != 0) { anim.viewLocationDidChange(_vdx, _vdy); } _animmgr.registerAnimation(anim); } protected class AnimRecord extends AnimationAdapter { public AnimRecord ( Animation anim, long delta, AnimRecord trigger, Runnable completionAction) { _anim = anim; _delta = delta; _trigger = trigger; _completionAction = completionAction; } public boolean readyToFire (long now, long lastStamp) { if (_delta == -1) { // if we have no trigger, that means we should start immediately, otherwise we wait // until our trigger is no longer running (they are guaranteed not to be still // queued at this point because readyToFire is only called on an animation after // all animations previous in the queue have been started) return (_trigger == null) ? true : !_running.contains(_trigger); } else { return (lastStamp + _delta <= now); } } public void fire (long when) { // Log.info("Firing " + this + " at " + (when%10000) + "."); // if we have an animation, start it up and await its completion if (_anim != null) { startAnimation(_anim, when); _anim.addAnimationObserver(this); } else { // since there's no animation, we need to fire our // completion routine immediately fireCompletion(when); } } public void fireCompletion (long when) { // Log.info("Completing " + this + " at " + (when%10000) + "."); // call the completion action, if there is one if (_completionAction != null) { try { _completionAction.run(); } catch (Throwable t) { log.warning(t); } } // make a note that this animation is complete _running.remove(this); // kids, don't try this at home; we call tick() immediately so that any animations // triggered on the completion of previous animations can trigger on the completion of // this animation rather than having to wait until the next tick to do so tick(when); } @Override public void animationCompleted (Animation anim, long when) { fireCompletion(when); } @Override public String toString () { return "[anim=" + StringUtil.shortClassName(_anim) + ((_anim == null) ? "" : ("/" + _anim.hashCode())) + ", action=" + _completionAction + ", delta=" + _delta + ", trig=" + (_trigger != null) + "]"; } protected Animation _anim; protected Runnable _completionAction; protected long _delta; protected AnimRecord _trigger; } /** The animation manager in which we run animations. */ protected AnimationManager _animmgr; /** Animations that have not been fired. */ protected ArrayList<AnimRecord> _queued = Lists.newArrayList(); /** Animations that are currently running. */ protected ArrayList<AnimRecord> _running = Lists.newArrayList(); /** The timestamp at which we fired the last animation. */ protected long _lastStamp; /** Used to track view scrolling while animations are in limbo. */ protected int _vdx, _vdy; }