Spec-Zone .ru
спецификации, руководства, описания, API
001/*
002 * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved.
003 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
004 *
005 * This code is free software; you can redistribute it and/or modify it
006 * under the terms of the GNU General Public License version 2 only, as
007 * published by the Free Software Foundation.  Oracle designates this
008 * particular file as subject to the "Classpath" exception as provided
009 * by Oracle in the LICENSE file that accompanied this code.
010 *
011 * This code is distributed in the hope that it will be useful, but WITHOUT
012 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
013 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
014 * version 2 for more details (a copy is included in the LICENSE file that
015 * accompanied this code).
016 *
017 * You should have received a copy of the GNU General Public License version
018 * 2 along with this work; if not, write to the Free Software Foundation,
019 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
020 *
021 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
022 * or visit www.oracle.com if you need additional information or have any
023 * questions.
024 */
025
026package javafx.animation;
027
028import java.util.HashMap;
029import javafx.beans.property.BooleanProperty;
030import javafx.beans.property.DoubleProperty;
031import javafx.beans.property.DoublePropertyBase;
032import javafx.beans.property.IntegerProperty;
033import javafx.beans.property.IntegerPropertyBase;
034import javafx.beans.property.ObjectProperty;
035import javafx.beans.property.ObjectPropertyBase;
036import javafx.beans.property.ReadOnlyDoubleProperty;
037import javafx.beans.property.ReadOnlyDoublePropertyBase;
038import javafx.beans.property.ReadOnlyObjectProperty;
039import javafx.beans.property.ReadOnlyObjectPropertyBase;
040import javafx.beans.property.SimpleBooleanProperty;
041import javafx.beans.property.SimpleObjectProperty;
042import javafx.collections.FXCollections;
043import javafx.collections.ObservableMap;
044import javafx.event.ActionEvent;
045import javafx.event.EventHandler;
046import javafx.util.Duration;
047import com.sun.javafx.animation.TickCalculation;
048import com.sun.scenario.ToolkitAccessor;
049import com.sun.scenario.animation.AbstractMasterTimer;
050import com.sun.scenario.animation.shared.ClipEnvelope;
051import com.sun.scenario.animation.shared.PulseReceiver;
052
053import static com.sun.javafx.animation.TickCalculation.*;
054
055/**
056 * The class {@code Animation} provides the core functionality of all animations
057 * used in the JavaFX runtime.
058 * <p>
059 * An animation can run in a loop by setting {@link #cycleCount}. To make an
060 * animation run back and forth while looping, set the {@link #autoReverse}
061 * -flag.
062 * <p>
063 * Call {@link #play()} or {@link #playFromStart()} to play an {@code Animation}
064 * . The {@code Animation} progresses in the direction and speed specified by
065 * {@link #rate}, and stops when its duration is elapsed. An {@code Animation}
066 * with indefinite duration (a {@link #cycleCount} of {@link #INDEFINITE}) runs
067 * repeatedly until the {@link #stop()} method is explicitly called, which will
068 * stop the running {@code Animation} and reset its play head to the initial
069 * position.
070 * <p>
071 * An {@code Animation} can be paused by calling {@link #pause()}, and the next
072 * {@link #play()} call will resume the {@code Animation} from where it was
073 * paused.
074 * <p>
075 * An {@code Animation}'s play head can be randomly positioned, whether it is
076 * running or not. If the {@code Animation} is running, the play head jumps to
077 * the specified position immediately and continues playing from new position.
078 * If the {@code Animation} is not running, the next {@link #play()} will start
079 * the {@code Animation} from the specified position.
080 * <p>
081 * Inverting the value of {@link #rate} toggles the play direction.
082 * 
083 * @see Timeline
084 * @see Transition
085 * 
086 */
087public abstract class Animation {
088
089    static {
090        AnimationAccessorImpl.DEFAULT = new AnimationAccessorImpl();
091    }
092
093    /**
094     * Used to specify an animation that repeats indefinitely, until the
095     * {@code stop()} method is called.
096     */
097    public static final int INDEFINITE = -1;
098
099    /**
100     * The possible states for {@link Animation#statusProperty status}.
101     */
102    public static enum Status {
103        /**
104         * The paused state.
105         */
106        PAUSED,
107        /**
108         * The running state.
109         */
110        RUNNING,
111        /**
112         * The stopped state.
113         */
114        STOPPED
115    }
116
117    private static final double EPSILON = 1e-12;
118
119    /*
120        These four fields and associated methods were moved here from AnimationPulseReceiver
121        when that class was removed. They could probably be integrated much cleaner into Animation,
122        but to make sure the change was made without introducing regressions, this code was
123        moved pretty much verbatim.
124     */
125    private long startTime;
126    private long pauseTime;
127    private boolean paused = false;
128    private final AbstractMasterTimer timer;
129
130    private long now() {
131        return TickCalculation.fromNano(timer.nanos());
132    }
133
134    void startReceiver(long delay) {
135        paused = false;
136        startTime = now() + delay;
137        timer.addPulseReceiver(pulseReceiver);
138    }
139
140    void pauseReceiver() {
141        if (!paused) {
142            pauseTime = now();
143            paused = true;
144            timer.removePulseReceiver(pulseReceiver);
145        }
146    }
147
148    void resumeReceiver() {
149        if (paused) {
150            final long deltaTime = now() - pauseTime;
151            startTime += deltaTime;
152            paused = false;
153            timer.addPulseReceiver(pulseReceiver);
154        }
155    }
156
157    // package private only for the sake of testing
158    final PulseReceiver pulseReceiver = new PulseReceiver() {
159        @Override public void timePulse(long now) {
160            final long elapsedTime = now - startTime;
161            if (elapsedTime < 0) {
162                return;
163            }
164
165            impl_timePulse(elapsedTime);
166        }
167    };
168
169    private class CurrentRateProperty extends ReadOnlyDoublePropertyBase {
170        private double value;
171        
172        @Override
173        public Object getBean() {
174            return Animation.this;
175        }
176
177        @Override
178        public String getName() {
179            return "currentRate";
180        }
181
182        @Override
183        public double get() {
184            return value;
185        }
186        
187        private void set(double value) {
188            this.value = value;
189            fireValueChangedEvent();
190        }
191    }
192    
193    private class AnimationReadOnlyProperty<T> extends ReadOnlyObjectPropertyBase<T> {
194        
195        private final String name;
196        private T value;
197        
198        private AnimationReadOnlyProperty(String name, T value) {
199            this.name = name;
200            this.value = value;
201        }
202
203        @Override
204        public Object getBean() {
205            return Animation.this;
206        }
207
208        @Override
209        public String getName() {
210            return name;
211        }
212
213        @Override
214        public T get() {
215            return value;
216        }
217        
218        private void set(T value) {
219            this.value = value;
220            fireValueChangedEvent();
221        }
222    }
223    
224    /**
225     * The parent of this {@code Animation}. If this animation has not been
226     * added to another animation, such as {@link ParallelTransition} and
227     * {@link SequentialTransition}, then parent will be null.
228     *
229     * @defaultValue null
230     */
231    Animation parent = null;
232
233    /* Package-private for testing purposes */
234    ClipEnvelope clipEnvelope;
235
236    private boolean lastPlayedFinished = false;
237    
238    private boolean lastPlayedForward = true;
239    /**
240     * Defines the direction/speed at which the {@code Animation} is expected to
241     * be played.
242     * <p>
243     * The absolute value of {@code rate} indicates the speed which the
244     * {@code Animation} is to be played, while the sign of {@code rate}
245     * indicates the direction. A positive value of {@code rate} indicates
246     * forward play, a negative value indicates backward play and {@code 0.0} to
247     * stop a running {@code Animation}.
248     * <p>
249     * Rate {@code 1.0} is normal play, {@code 2.0} is 2 time normal,
250     * {@code -1.0} is backwards, etc...
251     * 
252     * <p>
253     * Inverting the rate of a running {@code Animation} will cause the
254     * {@code Animation} to reverse direction in place and play back over the
255     * portion of the {@code Animation} that has already elapsed.
256     * 
257     * @defaultValue 1.0
258     */
259    private DoubleProperty rate;
260    private static final double DEFAULT_RATE = 1.0;
261
262    public final void setRate(double value) {
263        if ((rate != null) || (Math.abs(value - DEFAULT_RATE) > EPSILON)) {
264            rateProperty().set(value);
265        }
266    }
267
268    public final double getRate() {
269        return (rate == null)? DEFAULT_RATE : rate.get();
270    }
271
272    public final DoubleProperty rateProperty() {
273        if (rate == null) {
274            rate = new DoublePropertyBase(DEFAULT_RATE) {
275
276                @Override
277                public void invalidated() {
278                    final double newRate = getRate();
279                    if (isRunningEmbedded()) {
280                        if (isBound()) {
281                            unbind();
282                        }
283                        set(oldRate);
284                        throw new IllegalArgumentException("Cannot set rate of embedded animation while running.");
285                    } else {
286                        if (Math.abs(newRate) < EPSILON) {
287                            if (getStatus() == Status.RUNNING) {
288                                lastPlayedForward = (Math.abs(getCurrentRate()
289                                        - oldRate) < EPSILON);
290                            }
291                            setCurrentRate(0.0);
292                            pauseReceiver();
293                        } else {
294                            if (getStatus() == Status.RUNNING) {
295                                final double currentRate = getCurrentRate();
296                                if (Math.abs(currentRate) < EPSILON) {
297                                    setCurrentRate(lastPlayedForward ? newRate : -newRate);
298                                    resumeReceiver();
299                                } else {
300                                    final boolean playingForward = Math.abs(currentRate - oldRate) < EPSILON;
301                                    setCurrentRate(playingForward ? newRate : -newRate);
302                                }
303                            }
304                            oldRate = newRate;
305                        }
306                        clipEnvelope.setRate(newRate);
307                    }
308                }
309
310                @Override
311                public Object getBean() {
312                    return Animation.this;
313                }
314
315                @Override
316                public String getName() { 
317                    return "rate";
318                }
319            };
320        }
321        return rate;
322    }
323
324    private boolean isRunningEmbedded() {
325        if (parent == null) {
326            return false;
327        }
328        return parent.getStatus() != Status.STOPPED || parent.isRunningEmbedded();
329    }
330
331    private double oldRate = 1.0;
332    /**
333     * Read-only variable to indicate current direction/speed at which the
334     * {@code Animation} is being played.
335     * <p>
336     * {@code currentRate} is not necessary equal to {@code rate}.
337     * {@code currentRate} is set to {@code 0.0} when animation is paused or
338     * stopped. {@code currentRate} may also point to different direction during
339     * reverse cycles when {@code autoReverse} is {@code true}
340     * 
341     * @defaultValue 0.0
342     */
343    private ReadOnlyDoubleProperty currentRate;
344    private static final double DEFAULT_CURRENT_RATE = 0.0;
345    
346    private void setCurrentRate(double value) {
347        if ((currentRate != null) || (Math.abs(value - DEFAULT_CURRENT_RATE) > EPSILON)) {
348            ((CurrentRateProperty)currentRateProperty()).set(value);
349        }
350    }
351
352    public final double getCurrentRate() {
353        return (currentRate == null)? DEFAULT_CURRENT_RATE : currentRate.get();
354    }
355
356    public final ReadOnlyDoubleProperty currentRateProperty() {
357        if (currentRate == null) {
358            currentRate = new CurrentRateProperty();
359        }
360        return currentRate;
361    }
362
363    /**
364     * Read-only variable to indicate the duration of one cycle of this
365     * {@code Animation}: the time it takes to play from time 0 to the
366     * end of the Animation (at the default {@code rate} of
367     * 1.0).
368     * 
369     * @defaultValue 0ms
370     */
371    private ReadOnlyObjectProperty<Duration> cycleDuration;
372    private static final Duration DEFAULT_CYCLE_DURATION = Duration.ZERO;
373
374    protected final void setCycleDuration(Duration value) {
375        if ((cycleDuration != null) || (!DEFAULT_CYCLE_DURATION.equals(value))) {
376            if (value.lessThan(Duration.ZERO)) {
377                throw new IllegalArgumentException("Cycle duration cannot be negative");
378            }
379            ((AnimationReadOnlyProperty<Duration>)cycleDurationProperty()).set(value);
380            updateTotalDuration();
381        }
382    }
383
384    public final Duration getCycleDuration() {
385        return (cycleDuration == null)? DEFAULT_CYCLE_DURATION : cycleDuration.get();
386    }
387
388    public final ReadOnlyObjectProperty<Duration> cycleDurationProperty() {
389        if (cycleDuration == null) {
390            cycleDuration = new AnimationReadOnlyProperty<Duration>("cycleDuration", DEFAULT_CYCLE_DURATION);
391        }
392        return cycleDuration;
393    }
394
395    /**
396     * Read-only variable to indicate the total duration of this
397     * {@code Animation}, including repeats. A {@code Animation} with a {@code cycleCount}
398     * of {@code Animation.INDEFINITE} will have a {@code totalDuration} of
399     * {@code Duration.INDEFINITE}.
400     * 
401     * <p>
402     * This is set to cycleDuration * cycleCount.
403     * 
404     * @defaultValue 0ms
405     */
406    private ReadOnlyObjectProperty<Duration> totalDuration;
407    private static final Duration DEFAULT_TOTAL_DURATION = Duration.ZERO;
408
409    public final Duration getTotalDuration() {
410        return (totalDuration == null)? DEFAULT_TOTAL_DURATION : totalDuration.get();
411    }
412
413    public final ReadOnlyObjectProperty<Duration> totalDurationProperty() {
414        if (totalDuration == null) {
415            totalDuration = new AnimationReadOnlyProperty<Duration>("totalDuration", DEFAULT_TOTAL_DURATION);
416        }
417        return totalDuration;
418    }
419
420    private void updateTotalDuration() {
421        // Implementing the bind eagerly, because cycleCount and
422        // cycleDuration should not change that often
423        final int cycleCount = getCycleCount();
424        final Duration cycleDuration = getCycleDuration();
425        final Duration newTotalDuration = Duration.ZERO.equals(cycleDuration) ? Duration.ZERO
426                : (cycleCount == Animation.INDEFINITE) ? Duration.INDEFINITE
427                        : (cycleCount <= 1) ? cycleDuration : cycleDuration
428                                .multiply(cycleCount);
429        if ((totalDuration != null) || (!DEFAULT_TOTAL_DURATION.equals(newTotalDuration))) {
430            ((AnimationReadOnlyProperty<Duration>)totalDurationProperty()).set(newTotalDuration);
431        }
432        if (newTotalDuration.lessThan(getCurrentTime())) {
433            jumpTo(newTotalDuration);
434        }
435    }
436
437    /**
438     * Defines the {@code Animation}'s play head position.
439     * 
440     * @defaultValue 0ms
441     */
442    private CurrentTimeProperty currentTime;
443    private long currentTicks;
444    private class CurrentTimeProperty extends ReadOnlyObjectPropertyBase<Duration> {
445        
446        @Override
447        public Object getBean() {
448            return Animation.this;
449        }
450
451        @Override
452        public String getName() {
453            return "currentTime";
454        }
455
456        @Override
457        public Duration get() {
458            return getCurrentTime();
459        }
460        
461        @Override
462        public void fireValueChangedEvent() {
463            super.fireValueChangedEvent();
464        }
465        
466    }
467    
468    public final Duration getCurrentTime() {
469        return TickCalculation.toDuration(currentTicks);
470    }
471
472    public final ReadOnlyObjectProperty<Duration> currentTimeProperty() {
473        if (currentTime == null) {
474            currentTime = new CurrentTimeProperty();
475        }
476        return currentTime;
477    }
478
479    /**
480     * Delays the start of an animation.
481     *
482     * Cannot be negative. Setting to a negative number will result in {@link IllegalArgumentException}.
483     * 
484     * @defaultValue 0ms
485     */
486    private ObjectProperty<Duration> delay;
487    private static final Duration DEFAULT_DELAY = Duration.ZERO;
488
489    public final void setDelay(Duration value) {
490        if ((delay != null) || (!DEFAULT_DELAY.equals(value))) {
491            delayProperty().set(value);
492        }
493    }
494
495    public final Duration getDelay() {
496        return (delay == null)? DEFAULT_DELAY : delay.get();
497    }
498
499    public final ObjectProperty<Duration> delayProperty() {
500        if (delay == null) {
501            delay = new ObjectPropertyBase<Duration>(DEFAULT_DELAY) {
502
503                @Override
504                public Object getBean() {
505                    return Animation.this;
506                }
507
508                @Override
509                public String getName() {
510                    return "delay";
511                }
512
513                @Override
514                protected void invalidated() {
515                        final Duration newDuration = get();
516                        if (newDuration.lessThan(Duration.ZERO)) {
517                            if (isBound()) {
518                                unbind();
519                            }
520                            set(Duration.ZERO);
521                            throw new IllegalArgumentException("Cannot set delay to negative value. Setting to Duration.ZERO");
522                        }
523                }
524                
525            };
526        }
527        return delay;
528    }
529
530    /**
531     * Defines the number of cycles in this animation. The {@code cycleCount}
532     * may be {@code INDEFINITE} for animations that repeat indefinitely, but
533     * must otherwise be > 0.
534     * <p>
535     * It is not possible to change the {@code cycleCount} of a running
536     * {@code Animation}. If the value of {@code cycleCount} is changed for a
537     * running {@code Animation}, the animation has to be stopped and started again to pick
538     * up the new value.
539     * 
540     * @defaultValue 1.0
541     * 
542     */
543    private IntegerProperty cycleCount;
544    private static final int DEFAULT_CYCLE_COUNT = 1;
545
546    public final void setCycleCount(int value) {
547        if ((cycleCount != null) || (value != DEFAULT_CYCLE_COUNT)) {
548            cycleCountProperty().set(value);
549        }
550    }
551
552    public final int getCycleCount() {
553        return (cycleCount == null)? DEFAULT_CYCLE_COUNT : cycleCount.get();
554    }
555
556    public final IntegerProperty cycleCountProperty() {
557        if (cycleCount == null) {
558            cycleCount = new IntegerPropertyBase(DEFAULT_CYCLE_COUNT) {
559
560                @Override
561                public void invalidated() {
562                    updateTotalDuration();
563                }
564
565                @Override
566                public Object getBean() {
567                    return Animation.this;
568                }
569
570                @Override
571                public String getName() {
572                    return "cycleCount";
573                }
574            };
575        }
576        return cycleCount;
577    }
578
579    /**
580     * Defines whether this
581     * {@code Animation} reverses direction on alternating cycles. If
582     * {@code true}, the
583     * {@code Animation} will proceed forward on the first cycle,
584     * then reverses on the second cycle, and so on. Otherwise, animation will
585     * loop such that each cycle proceeds forward from the start.
586     * 
587     * It is not possible to change the {@code autoReverse} flag of a running
588     * {@code Animation}. If the value of {@code autoReverse} is changed for a
589     * running {@code Animation}, the animation has to be stopped and started again to pick
590     * up the new value.
591     * 
592     * @defaultValue false
593     */
594    private BooleanProperty autoReverse;
595    private static final boolean DEFAULT_AUTO_REVERSE = false;
596
597    public final void setAutoReverse(boolean value) {
598        if ((autoReverse != null) || (value != DEFAULT_AUTO_REVERSE)) {
599            autoReverseProperty().set(value);
600        }
601    }
602
603    public final boolean isAutoReverse() {
604        return (autoReverse == null)? DEFAULT_AUTO_REVERSE : autoReverse.get();
605    }
606
607    public final BooleanProperty autoReverseProperty() {
608        if (autoReverse == null) {
609            autoReverse = new SimpleBooleanProperty(this, "autoReverse", DEFAULT_AUTO_REVERSE);
610        }
611        return autoReverse;
612    }
613
614    /**
615     * The status of the {@code Animation}.
616     * 
617     * In {@code Animation} can be in one of three states:
618     * {@link Status#STOPPED}, {@link Status#PAUSED} or {@link Status#RUNNING}.
619     */
620    private ReadOnlyObjectProperty<Status> status;
621    private static final Status DEFAULT_STATUS = Status.STOPPED;
622
623    protected final void setStatus(Status value) {
624        if ((status != null) || (!DEFAULT_STATUS.equals(value))) {
625            ((AnimationReadOnlyProperty<Status>)statusProperty()).set(value);
626        }
627    }
628
629    public final Status getStatus() {
630        return (status == null)? DEFAULT_STATUS : status.get();
631    }
632
633    public final ReadOnlyObjectProperty<Status> statusProperty() {
634        if (status == null) {
635            status = new AnimationReadOnlyProperty<Status>("status", Status.STOPPED);
636        }
637        return status;
638    }
639    
640    private final double targetFramerate;
641    private final int resolution;
642    private long lastPulse;
643
644    /**
645     * The target framerate is the maximum framerate at which this {@code Animation}
646     * will run, in frames per second. This can be used, for example, to keep
647     * particularly complex {@code Animations} from over-consuming system resources.
648     * By default, an {@code Animation}'s framerate is not explicitly limited, meaning
649     * the {@code Animation} will run at an optimal framerate for the underlying platform.
650     *
651     * @return the target framerate
652     */
653    public final double getTargetFramerate() {
654        return targetFramerate;
655    }
656
657    /**
658     * The action to be executed at the conclusion of this {@code Animation}.
659     *
660     * @defaultValue null
661     */
662    private ObjectProperty<EventHandler<ActionEvent>> onFinished;
663    private static final EventHandler<ActionEvent> DEFAULT_ON_FINISHED = null;
664
665    public final void setOnFinished(EventHandler<ActionEvent> value) {
666        if ((onFinished != null) || (value != null /* DEFAULT_ON_FINISHED */)) {
667            onFinishedProperty().set(value);
668        }
669    }
670
671    public final EventHandler<ActionEvent> getOnFinished() {
672        return (onFinished == null)? DEFAULT_ON_FINISHED : onFinished.get();
673    }
674
675    public final ObjectProperty<EventHandler<ActionEvent>> onFinishedProperty() {
676        if (onFinished == null) {
677            onFinished = new SimpleObjectProperty<EventHandler<ActionEvent>>(this, "onFinished", DEFAULT_ON_FINISHED);
678        }
679        return onFinished;
680    }
681
682    private final ObservableMap<String, Duration> cuePoints = FXCollections
683            .observableMap(new HashMap<String, Duration>(0));
684
685    /**
686     * The cue points can be
687     * used to mark important positions of the {@code Animation}. Once a cue
688     * point was defined, it can be used as an argument of
689     * {@link #jumpTo(String) jumpTo()} and {@link #playFrom(String) playFrom()}
690     * to move to the associated position quickly.
691     * <p>
692     * Every {@code Animation} has two predefined cue points {@code "start"} and
693     * {@code "end"}, which are set at the start respectively the end of the
694     * {@code Animation}. The predefined cuepoints do not appear in the map,
695     * attempts to override them have no effect.
696     * <p>
697     * Another option to define a cue point in a {@code Animation} is to set the
698     * {@link KeyFrame#name} property of a {@link KeyFrame}.
699     *
700     * @return {@link javafx.collections.ObservableMap} of cue points
701     */
702    public final ObservableMap<String, Duration> getCuePoints() {
703        return cuePoints;
704    }
705
706    /**
707     * Jumps to a given position in this {@code Animation}.
708     * 
709     * If the given time is less than {@link Duration#ZERO}, this method will
710     * jump to the start of the animation. If the given time is larger than the
711     * duration of this {@code Animation}, this method will jump to the end.
712     * 
713     * @param time
714     *            the new position
715     * @throws NullPointerException
716     *             if {@code time} is {@code null}
717     * @throws IllegalArgumentException
718     *             if {@code time} is {@link Duration#UNKNOWN}
719     * @throws IllegalStateException
720     *             if embedded in another animation,
721     *                such as {@link SequentialTransition} or {@link ParallelTransition}
722     */
723    public void jumpTo(Duration time) {
724        if (time == null) {
725            throw new NullPointerException("Time needs to be specified.");
726        }
727        if (time.isUnknown()) {
728            throw new IllegalArgumentException("The time is invalid");
729        }
730        if (parent != null) {
731            throw new IllegalStateException("Cannot jump when embedded in another animation");
732        }
733
734        lastPlayedFinished = false;
735        
736        final Duration totalDuration = getTotalDuration();
737        time = time.lessThan(Duration.ZERO) ? Duration.ZERO : time
738                .greaterThan(totalDuration) ? totalDuration : time;
739        final long ticks = fromDuration(time);
740
741        if (getStatus() == Status.STOPPED) {
742            syncClipEnvelope();
743        }
744        clipEnvelope.jumpTo(ticks);
745    }
746
747    /**
748     * Jumps to a predefined position in this {@code Animation}. This method
749     * looks for an entry in cue points and jumps to the associated
750     * position, if it finds one.
751     * <p>
752     * If the cue point is behind the end of this {@code Animation}, calling
753     * {@code jumpTo} will result in a jump to the end. If the cue point has a
754     * negative {@link javafx.util.Duration} it will result in a jump to the
755     * beginning. If the cue point has a value of
756     * {@link javafx.util.Duration#UNKNOWN} calling {@code jumpTo} will have no
757     * effect for this cue point.
758     * <p>
759     * There are two predefined cue points {@code "start"} and {@code "end"}
760     * which are defined to be at the start respectively the end of this
761     * {@code Animation}.
762     * 
763     * @param cuePoint
764     *            the name of the cue point
765     * @throws NullPointerException
766     *             if {@code cuePoint} is {@code null}
767     * @throws IllegalStateException
768     *             if embedded in another animation,
769     *                such as {@link SequentialTransition} or {@link ParallelTransition}
770     * @see #getCuePoints()
771     */
772    public void jumpTo(String cuePoint) {
773        if (cuePoint == null) {
774            throw new NullPointerException("CuePoint needs to be specified");
775        }
776        if ("start".equalsIgnoreCase(cuePoint)) {
777            jumpTo(Duration.ZERO);
778        } else if ("end".equalsIgnoreCase(cuePoint)) {
779            jumpTo(getTotalDuration());
780        } else {
781            final Duration target = getCuePoints().get(cuePoint);
782            if (target != null) {
783                jumpTo(target);
784            }
785        }
786    }
787
788    /**
789     * A convenience method to play this {@code Animation} from a predefined
790     * position. The position has to be predefined in cue points.
791     * Calling this method is equivalent to
792     * 
793     * <pre>
794     * <code>
795     * animation.jumpTo(cuePoint);
796     * animation.play();
797     * </code>
798     * </pre>
799     * 
800     * Note that unlike {@link #playFromStart()} calling this method will not
801     * change the playing direction of this {@code Animation}.
802     * 
803     * @param cuePoint
804     *            name of the cue point
805     * @throws NullPointerException
806     *             if {@code cuePoint} is {@code null}
807     * @throws IllegalStateException
808     *             if embedded in another animation,
809     *                such as {@link SequentialTransition} or {@link ParallelTransition}
810     * @see #getCuePoints()
811     */
812    public void playFrom(String cuePoint) {
813        jumpTo(cuePoint);
814        play();
815    }
816
817    /**
818     * A convenience method to play this {@code Animation} from a specific
819     * position. Calling this method is equivalent to
820     * 
821     * <pre>
822     * <code>
823     * animation.jumpTo(time);
824     * animation.play();
825     * </code>
826     * </pre>
827     * 
828     * Note that unlike {@link #playFromStart()} calling this method will not
829     * change the playing direction of this {@code Animation}.
830     * 
831     * @param time
832     *            position where to play from
833     * @throws NullPointerException
834     *             if {@code time} is {@code null}
835     * @throws IllegalArgumentException
836     *             if {@code time} is {@link Duration#UNKNOWN}
837     * @throws IllegalStateException
838     *             if embedded in another animation,
839     *                such as {@link SequentialTransition} or {@link ParallelTransition}
840     */
841    public void playFrom(Duration time) {
842        jumpTo(time);
843        play();
844    }
845
846    /**
847     * Plays {@code Animation} from current position in the direction indicated
848     * by {@code rate}. If the {@code Animation} is running, it has no effect.
849     * <p>
850     * When {@code rate} > 0 (forward play), if an {@code Animation} is already
851     * positioned at the end, the first cycle will not be played, it is
852     * considered to have already finished. This also applies to a backward (
853     * {@code rate} < 0) cycle if an {@code Animation} is positioned at the beginning.
854     * However, if the {@code Animation} has {@code cycleCount} > 1, following
855     * cycle(s) will be played as usual.
856     * <p>
857     * When the {@code Animation} reaches the end, the {@code Animation} is stopped and
858     * the play head remains at the end.
859     * <p>
860     * To play an {@code Animation} backwards from the end:<br>
861     * <code>
862     *  animation.setRate(negative rate);<br>
863     *  animation.jumpTo(overall duration of animation);<br>
864     *  animation.play();<br>
865     * </code>
866     * <p>
867     * Note: <ul>
868     * <li>{@code play()} is an asynchronous call, the {@code Animation} may not
869     * start immediately. </ul>
870     *
871     * @throws IllegalStateException
872     *             if embedded in another animation,
873     *                such as {@link SequentialTransition} or {@link ParallelTransition}
874     */
875    public void play() {
876        play(true);
877    }
878
879    private void play(boolean forceSync) {
880        if (parent != null) {
881            throw new IllegalStateException("Cannot start when embedded in another animation");
882        }
883        switch (getStatus()) {
884            case STOPPED:
885                if (impl_startable(forceSync)) {
886                    final double rate = getRate();
887                    if (lastPlayedFinished) {
888                        jumpTo((rate < 0)? getTotalDuration() : Duration.ZERO);
889                    }
890                    lastPlayedFinished = false;
891                    impl_start(forceSync);
892                    startReceiver(TickCalculation.fromDuration(getDelay()));
893                    if (Math.abs(rate) < EPSILON) {
894                        pauseReceiver();
895                    } else {
896                        
897                    }
898                } else {
899                    final EventHandler<ActionEvent> handler = getOnFinished();
900                    if (handler != null) {
901                        handler.handle(new ActionEvent(this, null));
902                    }
903                }
904                break;
905            case PAUSED:
906                impl_resume();
907                if (Math.abs(getRate()) >= EPSILON) {
908                    resumeReceiver();
909                }
910                break;
911        }
912    }
913
914    /**
915     * Plays an {@code Animation} from initial position in forward direction.
916     * <p>
917     * It is equivalent to
918     * <p>
919     * <code>
920     *      animation.stop();<br>
921     *      animation.setRate = setRate(Math.abs(animation.getRate())); </br>
922     *      animation.jumpTo(Duration.ZERO);<br>
923     *      animation.play();<br>
924     *  </code>
925     * 
926     * <p>
927     * Note: <ul>
928     * <li>{@code playFromStart()} is an asynchronous call, {@code Animation} may
929     * not start immediately. </ul>
930     * <p>
931     *
932     * @throws IllegalStateException
933     *             if embedded in another animation,
934     *                such as {@link SequentialTransition} or {@link ParallelTransition}
935     */
936    public void playFromStart() {
937        stop();
938        setRate(Math.abs(getRate()));
939        jumpTo(Duration.ZERO);
940        play(true);
941    }
942
943    /**
944     * Stops the animation and resets the play head to its initial position. If
945     * the animation is not currently running, this method has no effect.
946     * <p>
947     * Note: <ul>
948     * <li>{@code stop()} is an asynchronous call, the {@code Animation} may not stop
949     * immediately. </ul>
950     * @throws IllegalStateException
951     *             if embedded in another animation,
952     *                such as {@link SequentialTransition} or {@link ParallelTransition}
953     */
954    public void stop() {
955        if (parent != null) {
956            throw new IllegalStateException("Cannot stop when embedded in another animation");
957        }
958        if (getStatus() != Status.STOPPED) {
959            clipEnvelope.abortCurrentPulse();
960            impl_stop();
961            jumpTo(Duration.ZERO);
962        }
963    }
964
965    /**
966     * Pauses the animation. If the animation is not currently running, this
967     * method has no effect.
968     * <p>
969     * Note: <ul>
970     * <li>{@code pause()} is an asynchronous call, the {@code Animation} may not pause
971     * immediately. </ul>
972     * @throws IllegalStateException
973     *             if embedded in another animation,
974     *                such as {@link SequentialTransition} or {@link ParallelTransition}
975     */
976    public void pause() {
977        if (parent != null) {
978            throw new IllegalStateException("Cannot pause when embedded in another animation");
979        }
980        if (getStatus() == Status.RUNNING) {
981            clipEnvelope.abortCurrentPulse();
982            pauseReceiver();
983            impl_pause();
984        }
985    }
986
987    /**
988     * The constructor of {@code Animation}.
989     * 
990     * This constructor allows to define a target framerate.
991     * 
992     * @param targetFramerate
993     *            The custom target frame rate for this {@code Animation}
994     * @see #getTargetFramerate()
995     */
996    protected Animation(double targetFramerate) {
997        this.targetFramerate = targetFramerate;
998        this.resolution = (int) Math.max(1, Math.round(TickCalculation.TICKS_PER_SECOND / targetFramerate));
999        this.clipEnvelope = ClipEnvelope.create(this);
1000        this.timer = ToolkitAccessor.getMasterTimer();
1001    }
1002
1003    /**
1004     * The constructor of {@code Animation}.
1005     */
1006    protected Animation() {
1007        this.resolution = 1;
1008        this.targetFramerate = TickCalculation.TICKS_PER_SECOND / ToolkitAccessor.getMasterTimer().getDefaultResolution();
1009        this.clipEnvelope = ClipEnvelope.create(this);
1010        this.timer = ToolkitAccessor.getMasterTimer();
1011    }
1012
1013    // These constructors are only for testing purposes
1014    Animation(AbstractMasterTimer timer) {
1015        this.resolution = 1;
1016        this.targetFramerate = TickCalculation.TICKS_PER_SECOND / timer.getDefaultResolution();
1017        this.clipEnvelope = ClipEnvelope.create(this);
1018        this.timer = timer;
1019    }
1020
1021    // These constructors are only for testing purposes
1022    Animation(AbstractMasterTimer timer, ClipEnvelope clipEnvelope, int resolution) {
1023        this.resolution = resolution;
1024        this.targetFramerate = TickCalculation.TICKS_PER_SECOND / resolution;
1025        this.clipEnvelope = clipEnvelope;
1026        this.timer = timer;
1027    }
1028
1029    boolean impl_startable(boolean forceSync) {
1030        return (fromDuration(getCycleDuration()) > 0L)
1031                || (!forceSync && clipEnvelope.wasSynched());
1032    }
1033
1034    void impl_sync(boolean forceSync) {
1035        if (forceSync || !clipEnvelope.wasSynched()) {
1036            syncClipEnvelope();
1037        }
1038    }
1039    
1040    private void syncClipEnvelope() {
1041        final int publicCycleCount = getCycleCount();
1042        final int internalCycleCount = (publicCycleCount <= 0)
1043                && (publicCycleCount != INDEFINITE) ? 1 : publicCycleCount;
1044        clipEnvelope = clipEnvelope.setCycleCount(internalCycleCount);
1045        clipEnvelope.setCycleDuration(getCycleDuration());
1046        clipEnvelope.setAutoReverse(isAutoReverse());
1047    }
1048
1049    void impl_start(boolean forceSync) {
1050        impl_sync(forceSync);
1051        setStatus(Status.RUNNING);
1052        clipEnvelope.start();
1053        setCurrentRate(clipEnvelope.getCurrentRate());
1054        lastPulse = 0;
1055    }
1056
1057    void impl_pause() {
1058        final double currentRate = getCurrentRate();
1059        if (Math.abs(currentRate) >= EPSILON) {
1060            lastPlayedForward = Math.abs(getCurrentRate() - getRate()) < EPSILON;
1061        }
1062        setCurrentRate(0.0);
1063        setStatus(Status.PAUSED);
1064    }
1065
1066    void impl_resume() {
1067        setStatus(Status.RUNNING);
1068        setCurrentRate(lastPlayedForward ? getRate() : -getRate());
1069    }
1070
1071    void impl_stop() {
1072        if (!paused) {
1073            timer.removePulseReceiver(pulseReceiver);
1074        }
1075        setStatus(Status.STOPPED);
1076        setCurrentRate(0.0);
1077    }
1078
1079    void impl_timePulse(long elapsedTime) {
1080        if (resolution == 1) { // fullspeed
1081            clipEnvelope.timePulse(elapsedTime);
1082        } else if (elapsedTime - lastPulse >= resolution) {
1083            lastPulse = (elapsedTime / resolution) * resolution;
1084            clipEnvelope.timePulse(elapsedTime);
1085        }
1086    }
1087
1088    abstract void impl_playTo(long currentTicks, long cycleTicks);
1089
1090    abstract void impl_jumpTo(long currentTicks, long cycleTicks, boolean forceJump);
1091
1092    void impl_setCurrentTicks(long ticks) {
1093        currentTicks = ticks;
1094        if (currentTime != null) {
1095            currentTime.fireValueChangedEvent();
1096        }
1097    }
1098
1099    void impl_setCurrentRate(double currentRate) {
1100//        if (getStatus() == Status.RUNNING) {
1101            setCurrentRate(currentRate);
1102//        }
1103    }
1104
1105    final void impl_finished() {
1106        lastPlayedFinished = true;
1107        impl_stop();
1108        final EventHandler<ActionEvent> handler = getOnFinished();
1109        if (handler != null) {
1110            try {
1111                handler.handle(new ActionEvent(this, null));
1112            } catch (Exception ex) {
1113                ex.printStackTrace();
1114            }
1115        }
1116    }
1117}