AnimationState.java

package swingtree.animation;

import org.slf4j.Logger;

import java.awt.event.ActionEvent;
import java.util.concurrent.TimeUnit;

/**
 * The state of an animation at a given point in time describing how far the animation has progressed
 * using a number between 0 and 1 (see {@link #progress()}).
 * Use the numbers exposed by the methods of this value based class to define how
 * your animation should progress over time.
 */
public final class AnimationState implements Progress
{
    private final static Logger log = org.slf4j.LoggerFactory.getLogger(AnimationState.class);

    public static AnimationState of( LifeSpan lifeSpan, Stride stride, ActionEvent event, long now ) {
        return _of(lifeSpan, stride, event, now, false);
    }

    public static AnimationState endOf(LifeSpan lifeSpan, Stride stride, ActionEvent event, long iteration) {
        return _of(lifeSpan, stride, event, lifeSpan.getEndTimeIn(TimeUnit.MILLISECONDS, iteration), true);
    }

    public static AnimationState startOf(LifeSpan lifeSpan, Stride stride, ActionEvent event) {
        return _of(lifeSpan, stride, event, lifeSpan.getStartTimeIn(TimeUnit.MILLISECONDS), false);
    }

    private static AnimationState _of( LifeSpan lifeSpan, Stride stride, ActionEvent event, long now, boolean isEnd ) {
        long duration = lifeSpan.lifeTime().getDurationIn(TimeUnit.MILLISECONDS);
        long interval = lifeSpan.lifeTime().getIntervalIn(TimeUnit.MILLISECONDS);
        long howLongIsRunning = Math.max(0, now - lifeSpan.getStartTimeIn(TimeUnit.MILLISECONDS));
        long howLongCurrentLoop = duration <= 0 ? 0 : howLongIsRunning % duration;
        if ( isEnd && howLongCurrentLoop == 0 )
            howLongCurrentLoop = duration;
        long howManyLoops      = duration <= 0 ? 0 : howLongIsRunning / duration;
        double progress;
        if ( duration <= 0 ) {
            howManyLoops = ( isEnd ? 1 : 0 );
        }
        switch ( stride ) {
            case PROGRESSIVE:
                if ( duration <= 0 )
                    progress     = ( isEnd ? 1 : 0 );
                else
                    progress = howLongCurrentLoop / (double) duration;
                break;
            case REGRESSIVE:
                if ( duration <= 0 )
                    progress     = ( isEnd ? 0 : 1 );
                else
                    progress = 1 - howLongCurrentLoop / (double) duration;
                break;
            default:
                progress = howLongCurrentLoop / (double) duration;
                log.warn("Unknown stride: {}", stride);
        }
        long steps = duration / interval;
        if ( steps > 0 )
            progress = Math.round( progress * steps ) / (double) steps;
        /*
            In the above line, we round the progress to the nearest step.
            This makes animations more deterministic and cache friendly.
        */
        return new AnimationState(progress, howManyLoops, lifeSpan, event);
    }


    private final double      progress;
    private final long        howManyLoops;
    private final LifeSpan    lifeSpan;
    private final ActionEvent event;


    private AnimationState( double progress, long howManyLoops, LifeSpan lifeSpan, ActionEvent event ) {
        this.progress     = progress;
        this.howManyLoops = howManyLoops;
        this.lifeSpan     = lifeSpan;
        this.event        = event;
    }

    /**
     *  Exposes the progress of the animation state, which is a number between 0 and 1
     *  that represents how far the animation has progressed between its start and end.
     *  Note that an animation may also regress, in which case the states will
     *  transition from 1 to 0 instead of from 0 to 1.
     *  See {@link Stride} for more information.
     *
     * @return The animation progress in terms of a number between 0 and 1,
     *         where 0.5 means the animation is halfway through, and 1 means the animation completed.
     */
    @Override
    public double progress() {
        return progress;
    }

    /**
     *  Slices the progress value of this animation state into a sub-{@link Progress} of the animation
     *  which starts with a value of {@code 0.0} when the animation reaches the progress value {@code from}
     *  and ends with a value of {@code 1.0} when the animation reaches the progress value {@code to}.
     *  If the {@code from} and {@code to} values are invalid, this method will correct them.
     *
     * @param from The progress value at which the sub-progress should start.
     *             This value must be between 0 and 1, otherwise it will be adjusted
     *             and a warning will be logged.
     * @param to The progress value at which the sub-progress should end.
     *           This value must be between 0 and 1, otherwise it will be adjusted
     *           and a warning will be logged.
     *
     * @return A {@link Progress} object that represents the sub-progress of the animation.
     *         This sub-progress will start with a value of {@code 0.0} when the animation reaches
     *         the progress value {@code from} and will end with a value of {@code 1.0} when the animation
     *         reaches the progress value {@code to}.
     */
    public Progress slice( double from, double to ) {
        if ( from == 0 && to == 1 ) {
            return this;
        }
        if ( from == to ) {
            log.warn("Invalid slice from '"+from+"' to '"+to+"'", new Throwable());
        }
        if ( from < 0 || from > 1 || to < 0 || to > 1 ) {
            log.warn("Invalid slice from '"+from+"' to '"+to+"'", new Throwable());
            from = Math.min(1, Math.max(0, from));
            to   = Math.min(1, Math.max(0, to));
        }
        if ( from > to ) {
            log.warn("Invalid slice from '"+from+"' to '"+to+"'", new Throwable());
            double tmp = from;
            from = to;
            to = tmp;
        }
        double subProgress = Math.max(from, Math.min(to, progress));
        return new Progress() {
            @Override
            public double progress() {
                return subProgress;
            }
        };
    }

    /**
     *  A single iteration of an animation consists of its progress going from 0 to 1
     *  in case of it being progressive, or from 1 to 0 in case of it being regressive (see {@link Stride}).
     *  This method returns the number of times the animation has been repeated.
     *
     * @return The number of times the animation has been repeated.
     *         This number is guaranteed to be 0 at the beginning of the animation,
     *         and for most animations it will be 0 at the end of the animation as well.
     *         An animation may be repeated if it is explicitly scheduled to run for a longer time.
     */
    public long repeats() { return howManyLoops; }

    /**
     *  Exposes the {@link LifeSpan} of the animation, which defines
     *  when the animation starts, for how long it should run, how is should progress and
     *  the refresh rate of the animation.
     *
     * @return The {@link LifeSpan} of the animation, i.e. the time when the animation started and how long it should run.
     */
    public LifeSpan lifeSpan() { return lifeSpan; }

    /**
     *  Exposes the timer event that triggered the animation.
     *  Note that under the hood, all animations with the same refresh rate will be
     *  updated by the same timer and thus share the same event.
     *
     * @return The timer event that triggered the animation.
     */
    public ActionEvent event() { return event; }

    @Override
    public String toString() {
        return this.getClass().getSimpleName()+"[" +
                "progress="   + progress +
                ", repeats="  + howManyLoops +
                ", lifeSpan=" + lifeSpan +
                ", event="    + event +
                "]";
    }

    @Override
    public boolean equals( Object o ) {
        if ( this == o ) return true;
        if ( o == null || getClass() != o.getClass() ) return false;

        AnimationState that = (AnimationState) o;

        if ( Double.compare(that.progress, progress) != 0 ) return false;
        if ( howManyLoops != that.howManyLoops ) return false;
        if ( !lifeSpan.equals(that.lifeSpan) ) return false;
        return event.equals(that.event);
    }

    @Override
    public int hashCode() {
        int result;
        result = Double.hashCode(progress);
        result = 31 * result + Long.hashCode(howManyLoops);
        result = 31 * result + lifeSpan.hashCode();
        result = 31 * result + event.hashCode();
        return result;
    }
}