LifeTime.java

package swingtree.animation;

import com.google.errorprone.annotations.Immutable;
import sprouts.Event;
import sprouts.Val;
import swingtree.SwingTree;
import swingtree.SwingTreeConfigurator;
import swingtree.api.AnimatedStyler;

import java.awt.Component;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

/**
 *  The lifetime is an immutable and thread safe value based object, which
 *  defines for how long an {@link Animation} should run.
 *  It consists of a delay, interval and duration as well as a unique id
 *  which ensures that two instances of this class are never equal.
 *  <br>
 *  You can create a new lifetime using the static factory methods {@link #of(long, TimeUnit)},
 *  {@link #of(double, TimeUnit)}, {@link #of(long, TimeUnit, long, TimeUnit)} and {@link #of(double, TimeUnit, double, TimeUnit)}.
 *  <br>
 *  Update an existing lifetime using the methods {@link #startingIn(long, TimeUnit)} and {@link #withInterval(long, TimeUnit)}.
 *  Note that the default interval of a newly created lifetime is always 16 ms which corresponds to 60 fps.
 *  <br><br>
 *  This class is typically used to schedule animations.
 *  The most straight forward way would be to call
 *  {@link swingtree.UI#animateFor(LifeTime)} or {@link swingtree.UI#animateFor(LifeTime, Component)}.
 *  But you may also schedule a style animation using {@link swingtree.UIForAnySwing#withTransitoryStyle(Event, LifeTime, AnimatedStyler)}
 *  or {@link swingtree.UIForAnySwing#withTransitionalStyle(Val, LifeTime, AnimatedStyler)}. <br>
 *  Another use case is to schedule an animation through the component event delegate
 *  as part of your event handling code using {@link swingtree.ComponentDelegate#animateFor(LifeTime)}. <br>
 *  This may look like this:
 *  <pre>{@code
 *  UI.button("I pop when you hover over me")
 *  .onMouseEnter( it -> it.animateFor(1, TimeUnit.SECONDS, state -> {
 *    it.style(state, conf -> conf
 *      .borderWidth( 10 * state.cycle() )
 *      .borderColor(UI.color(1,1,0,1-state.cycle()))
 *      .borderRadius( 100 * state.cycle() )
 *    );
 *  }))
 *  }</pre>
 */
@Immutable
public final class LifeTime
{
    private final long _delay; // in milliseconds
    private final long _duration;
    private final long _interval;

    /**
     *  Creates a new lifetime that will run for the given duration
     *  and without any start delay.
     * @param time The duration of the animation.
     * @param unit The time unit of the duration.
     * @return A new lifetime that will start immediately and run for the given duration.
     */
    public static LifeTime of( long time, TimeUnit unit ) {
        Objects.requireNonNull(unit);
        return new LifeTime(0, unit.toMillis(time), SwingTree.get().getDefaultAnimationInterval());
    }

    /**
     *  Creates a new lifetime that will run for the given duration
     *  in the given time unit and without any start delay. <br>
     *  Contrary to the {@link #of(long, TimeUnit)} method, this method
     *  uses a {@code double} type to allow for fractional time values.
     *
     * @param time The duration of the animation.
     * @param unit The time unit of the duration.
     * @return A new lifetime that will start immediately and run for the given duration.
     */
    public static LifeTime of( double time, TimeUnit unit ) {
        long millis = _convertTimeFromDoublePrecisely(time, unit, TimeUnit.MILLISECONDS);
        return new LifeTime(0, millis, SwingTree.get().getDefaultAnimationInterval());
    }

    /**
     *  Creates a new lifetime that will start after the given delay and run for the given duration.
     * @param startDelay The delay after which the animation should start.
     * @param startUnit The time unit of the delay.
     * @param duration The duration of the animation.
     * @param durationUnit The time unit of the duration.
     * @return A new lifetime that will start after the given delay and run for the given duration.
     */
    public static LifeTime of( long startDelay, TimeUnit startUnit, long duration, TimeUnit durationUnit ) {
        return new LifeTime(startUnit.toMillis(startDelay), durationUnit.toMillis(duration), SwingTree.get().getDefaultAnimationInterval());
    }

    /**
     *  Creates a new lifetime that will start after the given delay and run for the given duration.
     *  Contrary to the {@link #of(long, TimeUnit, long, TimeUnit)} method, this method
     *  uses a {@code double} type to allow for fractional time values.
     * @param startDelay The delay after which the animation should start.
     * @param startUnit The time unit of the delay.
     * @param duration The duration of the animation.
     * @param durationUnit The time unit of the duration.
     * @return A new lifetime that will start after the given delay and run for the given duration.
     */
    public static LifeTime of( double startDelay, TimeUnit startUnit, double duration, TimeUnit durationUnit ) {
        long startMillis    = _convertTimeFromDoublePrecisely(startDelay, startUnit, TimeUnit.MILLISECONDS);
        long durationMillis = _convertTimeFromDoublePrecisely(duration, durationUnit, TimeUnit.MILLISECONDS);
        return new LifeTime(startMillis, durationMillis, SwingTree.get().getDefaultAnimationInterval());
    }

    private static long _convertTimeFromDoublePrecisely( double time, TimeUnit from, TimeUnit to ) {
        long millis = (long) (time * from.toMillis(1));
        long remainderMillis = (long) (time * from.toMillis(1) - millis);
        return to.convert(millis + remainderMillis, TimeUnit.MILLISECONDS);
    }


    private LifeTime( long delay, long duration, long interval ) {
        _delay     = delay;
        _duration  = duration;
        _interval = interval;
    }


    /**
     *  Creates a new lifetime that will start after the given delay
     *  in the given time unit.
     * @param delay The delay after which the animation should start.
     * @param unit The time unit of the delay.
     * @return A new lifetime that will start after the given delay.
     */
    public LifeTime startingIn( long delay, TimeUnit unit ) {
        long offset = unit.toMillis( delay );
        return LifeTime.of(
                    offset,    TimeUnit.MILLISECONDS,
                    _duration, TimeUnit.MILLISECONDS
                );
    }

    /**
     *  Updates this lifetime with the given interval, which is a property that
     *  determines the delay between two consecutive animation steps.
     *  You can think of it as the time between the heartbeats of the animation.
     *  The smaller the interval, the higher the refresh rate and
     *  the smoother the animation will look.
     *  However, the smaller the interval, the more CPU time will be used.
     *  The default interval is 16 ms which corresponds to 60 fps.
     *  <br>
     *  If you want a custom interval default, you can configure it
     *  during library initialization through the {@link SwingTree#initialiseUsing(SwingTreeConfigurator)}
     *  method or change it at any other time using the
     *  {@link SwingTree#setDefaultAnimationInterval(long)} method.
     *  
     * @param interval The interval in the given time unit.
     * @param unit The time unit of the interval, typically {@link TimeUnit#MILLISECONDS}.
     * @return A new lifetime that will start after the given delay and run for the given duration.
     */
    public LifeTime withInterval( long interval, TimeUnit unit ) {
        return new LifeTime(_delay, _duration, unit.toMillis(interval));
    }

    /**
     *  Returns the duration of the animation in the given time unit.
     * @param unit The time unit in which the duration should be returned.
     * @return The duration of the animation.
     */
    public long getDurationIn( TimeUnit unit ) {
        Objects.requireNonNull(unit);
        return unit.convert(_duration, TimeUnit.MILLISECONDS);
    }

    /**
     *  Returns the delay after which the animation should start in the given time unit.
     * @param unit The time unit in which the delay should be returned.
     * @return The delay after which the animation should start.
     */
    public long getDelayIn( TimeUnit unit ) {
        Objects.requireNonNull(unit);
        return unit.convert(_delay, TimeUnit.MILLISECONDS);
    }

    /**
     *  Returns the interval in the given time unit,
     *  which is a number that determines the delay between two consecutive animation steps.
     *  You can think of it as the time between the heartbeats of an animation.
     *  The smaller the interval, the higher the refresh rate and
     *  the smoother the animation will look.
     *  However, the smaller the interval, the more CPU time will be used.
     *  The default interval is 16 ms which corresponds to 60 fps.
     *  <br>
     *  If you want a custom interval default, you can configure it
     *  during library initialization through the {@link SwingTree#initialiseUsing(SwingTreeConfigurator)}
     *  method or change it at any other time using the
     *  {@link SwingTree#setDefaultAnimationInterval(long)} method.
     *  
     * @param unit The time unit in which the interval should be returned.
     * @return The interval in the given time unit.
     */
    public long getIntervalIn( TimeUnit unit ) {
        Objects.requireNonNull(unit);
        return unit.convert(_interval, TimeUnit.MILLISECONDS);
    }

    @Override
    public boolean equals( Object o ) {
        if ( this == o ) return true;
        if ( !(o instanceof LifeTime) ) return false;
        LifeTime lifeTime = (LifeTime) o;
        return _delay     == lifeTime._delay    &&
               _duration  == lifeTime._duration &&
               _interval  == lifeTime._interval;
    }

    @Override
    public int hashCode() {
        return Objects.hash(_delay, _duration, _interval);
    }

    @Override
    public String toString() {
        return this.getClass().getSimpleName()+"[" +
                    "delay="    + _delay    + ", "+
                    "duration=" + _duration + ", "+
                    "interval=" + _interval +
                "]";
    }
}