1 /*
   2  * Copyright (c) 2008, 2014, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package com.sun.scenario.animation.shared;
  27 
  28 import java.util.Arrays;
  29 import java.util.Collection;
  30 import java.util.Comparator;
  31 import javafx.animation.Animation.Status;
  32 import javafx.animation.KeyFrame;
  33 import javafx.animation.Timeline;
  34 import javafx.event.ActionEvent;
  35 import javafx.event.EventHandler;
  36 import javafx.util.Duration;
  37 import com.sun.javafx.animation.TickCalculation;
  38 
  39 /**
  40  * An instance of ClipCore handles the core part of a clip.
  41  * 
  42  * The functionality to react on a pulse-signal from the timer is implemented in
  43  * two classes: ClipEnvelope and ClipCore. ClipEnvelope is responsible for the
  44  * "loop-part" (keeping track of the number of cycles, handling the direction of
  45  * the clip etc.). ClipCore takes care of the inner part (interpolating the
  46  * values, triggering the action-functions, ...)
  47  * 
  48  * Both classes have an abstract public definition and can only be created using
  49  * the factory method create(). The intend is to provide a general
  50  * implementation plus eventually some fast-track implementations for common use
  51  * cases.
  52  */
  53 
  54 // @@OPT
  55 // - Use known information (kf) in visitKeyFrame to set values?
  56 
  57 public class TimelineClipCore {
  58 
  59     private static final int UNDEFINED_KEYFRAME = -1;
  60 
  61     /**
  62      * Note: this comparator imposes orderings that are inconsistent with
  63      * equals.
  64      */
  65     private static final Comparator<KeyFrame> KEY_FRAME_COMPARATOR = (kf1, kf2) -> kf1.getTime().compareTo(kf2.getTime());
  66 
  67     // The owner of this ClipCore
  68     Timeline timeline;
  69 
  70     // The sorted list of keyframes
  71     private KeyFrame[] keyFrames = new KeyFrame[0];
  72     private long[] keyFrameTicks = new long[0];
  73 
  74     private ClipInterpolator clipInterpolator;
  75 
  76     public TimelineClipCore(Timeline timeline) {
  77         this.timeline = timeline;
  78         this.clipInterpolator = ClipInterpolator.create(keyFrames, keyFrameTicks);
  79     }
  80 
  81     /**
  82      * Changes the keyframes.
  83      */
  84     public Duration setKeyFrames(Collection<KeyFrame> keyFrames) {
  85         final int n = keyFrames.size();
  86         final KeyFrame[] sortedKeyFrames = new KeyFrame[n];
  87         keyFrames.toArray(sortedKeyFrames);
  88         Arrays.sort(sortedKeyFrames, KEY_FRAME_COMPARATOR);
  89         
  90         this.keyFrames = sortedKeyFrames;
  91         keyFrameTicks = new long[n];
  92         for (int i = 0; i < n; ++i) {
  93             keyFrameTicks[i] = TickCalculation.fromDuration(this.keyFrames[i].getTime());
  94         }
  95         clipInterpolator = clipInterpolator.setKeyFrames(sortedKeyFrames, keyFrameTicks);
  96         return (n == 0) ? Duration.ZERO
  97                 : sortedKeyFrames[n-1].getTime();
  98     }
  99 
 100     public void notifyCurrentRateChanged() {
 101         // special case: if clip is toggled while stopped, we want to revisit
 102         // all key frames
 103         if (timeline.getStatus() != Status.RUNNING) {
 104             clearLastKeyFrame();
 105         }
 106     }
 107 
 108     /**
 109      * This method is called if while visiting a keyframe of a timeline the time
 110      * or rate are changed, or if the timeline is stopped. In these cases
 111      * visiting the keyframes must be aborted.
 112      */
 113     public void abort() {
 114         aborted = true;
 115     }
 116 
 117     private boolean aborted = false;
 118 
 119     // The index of the keyframe that was visited last
 120     private int lastKF = UNDEFINED_KEYFRAME;
 121 
 122     // The position where clip currently stands
 123     private long curTicks = 0;
 124 
 125     private void clearLastKeyFrame() {
 126         lastKF = UNDEFINED_KEYFRAME;
 127     }
 128 
 129     public void jumpTo(long ticks, boolean forceJump) {
 130         lastKF = UNDEFINED_KEYFRAME;
 131         curTicks = ticks;
 132         if (timeline.getStatus() != Status.STOPPED || forceJump) {
 133             if (forceJump) {
 134                 clipInterpolator.validate(false);
 135             }
 136             clipInterpolator.interpolate(ticks);
 137         }
 138     }
 139 
 140     public void start(boolean forceSync) {
 141         clearLastKeyFrame();
 142         clipInterpolator.validate(forceSync);
 143         if (curTicks > 0) {
 144             clipInterpolator.interpolate(curTicks);
 145         }
 146     }
 147 
 148     /**
 149      * Called to visit all keyframes within a specified time-interval.
 150      */
 151     public void playTo(long ticks) {
 152         aborted = false;
 153         final boolean forward = curTicks <= ticks;
 154 
 155         if (forward) {
 156             final int fromKF = (lastKF == UNDEFINED_KEYFRAME) ? 0
 157                     : (keyFrameTicks[lastKF] <= curTicks) ? lastKF + 1
 158                             : lastKF;
 159             final int toKF = keyFrames.length;
 160             for (int fi = fromKF; fi < toKF; fi++) {
 161                 final long kfTicks = keyFrameTicks[fi];
 162                 if (kfTicks > ticks) {
 163                     lastKF = fi - 1;
 164                     break;
 165                 }
 166                 if (kfTicks >= curTicks) {
 167                     visitKeyFrame(fi, kfTicks);
 168                     if (aborted) {
 169                         break;
 170                     }
 171                 }
 172             }
 173         } else {
 174             final int fromKF = (lastKF == UNDEFINED_KEYFRAME) ? keyFrames.length - 1
 175                     : (keyFrameTicks[lastKF] >= curTicks) ? lastKF - 1
 176                             : lastKF;
 177             for (int fi = fromKF; fi >= 0; fi--) {
 178                 final long kfTicks = keyFrameTicks[fi];
 179                 if (kfTicks < ticks) {
 180                     lastKF = fi + 1;
 181                     break;
 182                 }
 183                 if (kfTicks <= curTicks) {
 184                     visitKeyFrame(fi, kfTicks);
 185                     if (aborted) {
 186                         break;
 187                     }
 188                 }
 189             }
 190         }
 191         if (!aborted
 192                 && ((lastKF == UNDEFINED_KEYFRAME)
 193                         || keyFrameTicks[lastKF] != ticks || (keyFrames[lastKF]
 194                         .getOnFinished() == null))) {
 195             setTime(ticks);
 196             clipInterpolator.interpolate(ticks);
 197         }
 198     }
 199 
 200     private void setTime(long ticks) {
 201         curTicks = ticks;
 202         AnimationAccessor.getDefault().setCurrentTicks(timeline, ticks);
 203     }
 204 
 205     /**
 206      * Visit a single keyframe.
 207      * 
 208      * @param kfIndex
 209      *            the index of the keyframe in the keyframe-array
 210      * @param kfTicks
 211      *            the time of that keyframe
 212      */
 213     private void visitKeyFrame(int kfIndex, long kfTicks) {
 214         if (kfIndex != lastKF) { // suppress double visiting on toggle for
 215                                  // autoReverse
 216             lastKF = kfIndex;
 217 
 218             final KeyFrame kf = keyFrames[kfIndex];
 219             final EventHandler<ActionEvent> onFinished = kf.getOnFinished();
 220 
 221             if (onFinished != null) {
 222                 // visit the action of this keyframe
 223                 setTime(kfTicks);
 224                 clipInterpolator.interpolate(kfTicks);
 225                 try {
 226                     onFinished.handle(new ActionEvent(kf, null));
 227                 } catch (Throwable ex) {
 228                     ex.printStackTrace();
 229                 }
 230             }
 231         }
 232     }
 233 }