1 /*
   2  * Copyright (c) 2010, 2016, 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 javafx.animation;
  27 
  28 import javafx.beans.property.ObjectProperty;
  29 import javafx.beans.property.ObjectPropertyBase;
  30 import javafx.beans.property.SimpleObjectProperty;
  31 import javafx.scene.Node;
  32 import javafx.scene.shape.Shape;
  33 import javafx.util.Duration;
  34 
  35 import com.sun.javafx.geom.Path2D;
  36 import com.sun.javafx.geom.PathIterator;
  37 import com.sun.javafx.geom.transform.BaseTransform;
  38 import java.util.ArrayList;
  39 
  40 /**
  41  * This {@code Transition} creates a path animation that spans its
  42  * {@link #duration}. The translation along the path is done by updating the
  43  * {@code translateX} and {@code translateY} variables of the {@code node}, and
  44  * the {@code rotate} variable will get updated if {@code orientation} is set to
  45  * {@code OrientationType.ORTHOGONAL_TO_TANGENT}, at regular interval.
  46  * <p>
  47  * The animated path is defined by the outline of a shape.
  48  *
  49  * <p>
  50  * Code Segment Example:
  51  * </p>
  52  *
  53  * <pre>
  54  * <code>
  55  * import javafx.scene.shape.*;
  56  * import javafx.animation.*;
  57  *
  58  * ...
  59  *
  60  *     Rectangle rect = new Rectangle (100, 40, 100, 100);
  61  *     rect.setArcHeight(50);
  62  *     rect.setArcWidth(50);
  63  *     rect.setFill(Color.VIOLET);
  64  *
  65  *
  66  *     Path path = new Path();
  67  *     path.getElements().add (new MoveTo (0f, 50f));
  68  *     path.getElements().add (new CubicCurveTo (40f, 10f, 390f, 240f, 1904, 50f));
  69  *
  70  *     pathTransition.setDuration(Duration.millis(10000));
  71  *     pathTransition.setNode(rect);
  72  *     pathTransition.setPath(path);
  73  *     pathTransition.setOrientation(OrientationType.ORTHOGONAL_TO_TANGENT);
  74  *     pathTransition.setCycleCount(4f);
  75  *     pathTransition.setAutoReverse(true);
  76  *
  77  *     pathTransition.play();
  78  *
  79  * ...
  80  *
  81  * </code>
  82  * </pre>
  83  *
  84  * @see Transition
  85  * @see Animation
  86  *
  87  * @since JavaFX 2.0
  88  */
  89 public final class PathTransition extends Transition {
  90 
  91     /**
  92      * The target node of this {@code PathTransition}.
  93      * <p>
  94      * It is not possible to change the target {@code node} of a running
  95      * {@code PathTransition}. If the value of {@code node} is changed for a
  96      * running {@code PathTransition}, the animation has to be stopped and
  97      * started again to pick up the new value.
  98      */
  99     private ObjectProperty<Node> node;
 100     private double totalLength = 0;
 101     private final ArrayList<Segment> segments = new ArrayList<>();
 102 
 103     private static final Node DEFAULT_NODE = null;
 104     private static final int SMOOTH_ZONE = 10;
 105 
 106     public final void setNode(Node value) {
 107         if ((node != null) || (value != null /* DEFAULT_NODE */)) {
 108             nodeProperty().set(value);
 109         }
 110     }
 111 
 112     public final Node getNode() {
 113         return (node == null)? DEFAULT_NODE : node.get();
 114     }
 115 
 116     public final ObjectProperty<Node> nodeProperty() {
 117         if (node == null) {
 118             node = new SimpleObjectProperty<Node>(this, "node", DEFAULT_NODE);
 119         }
 120         return node;
 121     }
 122 
 123     private Node cachedNode;
 124 
 125     /**
 126      * The duration of this {@code Transition}.
 127      * <p>
 128      * It is not possible to change the {@code duration} of a running
 129      * {@code PathTransition}. If the value of {@code duration} is changed for a
 130      * running {@code PathTransition}, the animation has to be stopped and
 131      * started again to pick up the new value.
 132      * <p>
 133      * Note: While the unit of {@code duration} is a millisecond, the
 134      * granularity depends on the underlying operating system and will in
 135      * general be larger. For example animations on desktop systems usually run
 136      * with a maximum of 60fps which gives a granularity of ~17 ms.
 137      *
 138      * Setting duration to value lower than {@link Duration#ZERO} will result
 139      * in {@link IllegalArgumentException}.
 140      *
 141      * @defaultValue 400ms
 142      */
 143     private ObjectProperty<Duration> duration;
 144     private static final Duration DEFAULT_DURATION = Duration.millis(400);
 145 
 146     public final void setDuration(Duration value) {
 147         if ((duration != null) || (!DEFAULT_DURATION.equals(value))) {
 148             durationProperty().set(value);
 149         }
 150     }
 151 
 152     public final Duration getDuration() {
 153         return (duration == null)? DEFAULT_DURATION : duration.get();
 154     }
 155 
 156     public final ObjectProperty<Duration> durationProperty() {
 157         if (duration == null) {
 158             duration = new ObjectPropertyBase<Duration>(DEFAULT_DURATION) {
 159 
 160                 @Override
 161                 public void invalidated() {
 162                     try {
 163                         setCycleDuration(getDuration());
 164                     } catch (IllegalArgumentException e) {
 165                         if (isBound()) {
 166                             unbind();
 167                         }
 168                         set(getCycleDuration());
 169                         throw e;
 170                     }
 171                 }
 172 
 173                 @Override
 174                 public Object getBean() {
 175                     return PathTransition.this;
 176                 }
 177 
 178                 @Override
 179                 public String getName() {
 180                     return "duration";
 181                 }
 182             };
 183         }
 184         return duration;
 185     }
 186 
 187     /**
 188      * The shape on which outline the node should be animated.
 189      * <p>
 190      * It is not possible to change the {@code path} of a running
 191      * {@code PathTransition}. If the value of {@code path} is changed for a
 192      * running {@code PathTransition}, the animation has to be stopped and
 193      * started again to pick up the new value.
 194      *
 195      * @defaultValue null
 196      */
 197     private ObjectProperty<Shape> path;
 198     private static final Shape DEFAULT_PATH = null;
 199 
 200     public final void setPath(Shape value) {
 201         if ((path != null) || (value != null /* DEFAULT_PATH */)) {
 202             pathProperty().set(value);
 203         }
 204     }
 205 
 206     public final Shape getPath() {
 207         return (path == null)? DEFAULT_PATH : path.get();
 208     }
 209 
 210     public final ObjectProperty<Shape> pathProperty() {
 211         if (path == null) {
 212             path = new SimpleObjectProperty<Shape>(this, "path", DEFAULT_PATH);
 213         }
 214         return path;
 215     }
 216 
 217     /**
 218      * Specifies the upright orientation of {@code node} along the {@code path}.
 219      * @since JavaFX 2.0
 220      */
 221     public static enum OrientationType {
 222 
 223         /**
 224          * The targeted {@code node}'s rotation matrix stays unchange along the
 225          * geometric path.
 226          */
 227         NONE,
 228 
 229         /**
 230          * The targeted node's rotation matrix is set to keep {@code node}
 231          * perpendicular to the path's tangent along the geometric path.
 232          */
 233         ORTHOGONAL_TO_TANGENT
 234     }
 235 
 236     /**
 237      * Specifies the upright orientation of {@code node} along the {@code path}.
 238      * The default orientation is set to {@link OrientationType#NONE}.
 239      * <p>
 240      * It is not possible to change the {@code orientation} of a running
 241      * {@code PathTransition}. If the value of {@code orientation} is changed
 242      * for a running {@code PathTransition}, the animation has to be stopped and
 243      * started again to pick up the new value.
 244      *
 245      * @defaultValue NONE
 246      */
 247     private ObjectProperty<OrientationType> orientation;
 248     private static final OrientationType DEFAULT_ORIENTATION = OrientationType.NONE;
 249 
 250     public final void setOrientation(OrientationType value) {
 251         if ((orientation != null) || (!DEFAULT_ORIENTATION.equals(value))) {
 252             orientationProperty().set(value);
 253         }
 254     }
 255 
 256     public final OrientationType getOrientation() {
 257         return (orientation == null)? OrientationType.NONE : orientation.get();
 258     }
 259 
 260     public final ObjectProperty<OrientationType> orientationProperty() {
 261         if (orientation == null) {
 262             orientation = new SimpleObjectProperty<OrientationType>(this, "orientation", DEFAULT_ORIENTATION);
 263         }
 264         return orientation;
 265     }
 266 
 267     private boolean cachedIsNormalRequired;
 268 
 269     /**
 270      * The constructor of {@code PathTransition}.
 271      *
 272      * @param duration
 273      *            The {@link #duration} of this {@code PathTransition}
 274      * @param path
 275      *            The {@link #path} of this {@code PathTransition}
 276      * @param node
 277      *            The {@link #node} of this {@code PathTransition}
 278      */
 279     public PathTransition(Duration duration, Shape path, Node node) {
 280         setDuration(duration);
 281         setPath(path);
 282         setNode(node);
 283         setCycleDuration(duration);
 284     }
 285 
 286     /**
 287      * The constructor of {@code PathTransition}.
 288      *
 289      * @param duration
 290      *            The {@link #duration} of this {@code PathTransition}
 291      * @param path
 292      *            The {@link #path} of this {@code PathTransition}
 293      */
 294     public PathTransition(Duration duration, Shape path) {
 295         this(duration, path, null);
 296     }
 297 
 298     /**
 299      * The constructor of {@code PathTransition}.
 300      */
 301     public PathTransition() {
 302         this(DEFAULT_DURATION, null, null);
 303     }
 304 
 305     /**
 306      * {@inheritDoc}
 307      */
 308     @Override
 309     public void interpolate(double frac) {
 310         double part = totalLength * Math.min(1, Math.max(0, frac));
 311         int segIdx = findSegment(0, segments.size() - 1, part);
 312         Segment seg = segments.get(segIdx);
 313 
 314         double lengthBefore = seg.accumLength - seg.length;
 315 
 316         double partLength = part - lengthBefore;
 317 
 318         double ratio = partLength / seg.length;
 319         Segment prevSeg = seg.prevSeg;
 320         double x = prevSeg.toX + (seg.toX - prevSeg.toX) * ratio;
 321         double y = prevSeg.toY + (seg.toY - prevSeg.toY) * ratio;
 322         double rotateAngle = seg.rotateAngle;
 323 
 324         // provide smooth rotation on segment bounds
 325         double z = Math.min(SMOOTH_ZONE, seg.length / 2);
 326         if (partLength < z && !prevSeg.isMoveTo) {
 327             //interpolate rotation to previous segment
 328             rotateAngle = interpolate(
 329                     prevSeg.rotateAngle, seg.rotateAngle,
 330                     partLength / z / 2 + 0.5F);
 331         } else {
 332             double dist = seg.length - partLength;
 333             Segment nextSeg = seg.nextSeg;
 334             if (dist < z && nextSeg != null) {
 335                 //interpolate rotation to next segment
 336                 if (!nextSeg.isMoveTo) {
 337                     rotateAngle = interpolate(
 338                             seg.rotateAngle, nextSeg.rotateAngle,
 339                             (z - dist) / z / 2);
 340                 }
 341             }
 342         }
 343         cachedNode.setTranslateX(x - cachedNode.impl_getPivotX());
 344         cachedNode.setTranslateY(y - cachedNode.impl_getPivotY());
 345         // Need to handle orientation if it is requested
 346         if (cachedIsNormalRequired) {
 347             cachedNode.setRotate(rotateAngle);
 348         }
 349     }
 350 
 351     private Node getTargetNode() {
 352         final Node node = getNode();
 353         return (node != null) ? node : getParentTargetNode();
 354     }
 355 
 356     @Override
 357     boolean startable(boolean forceSync) {
 358         return super.startable(forceSync)
 359                 && (((getTargetNode() != null) && (getPath() != null) && !getPath().getLayoutBounds().isEmpty()) || (!forceSync
 360                         && (cachedNode != null)));
 361     }
 362 
 363     @Override
 364     void sync(boolean forceSync) {
 365         super.sync(forceSync);
 366         if (forceSync || (cachedNode == null)) {
 367             cachedNode = getTargetNode();
 368             recomputeSegments();
 369             cachedIsNormalRequired = getOrientation() == OrientationType.ORTHOGONAL_TO_TANGENT;
 370         }
 371     }
 372 
 373     private void recomputeSegments() {
 374         segments.clear();
 375         final Shape p = getPath();
 376         Segment moveToSeg = Segment.getZeroSegment();
 377         Segment lastSeg = Segment.getZeroSegment();
 378 
 379         float[] coords = new float[6];
 380         for (PathIterator i = p.impl_configShape().getPathIterator(p.impl_getLeafTransform(), 1.0f); !i.isDone(); i.next()) {
 381             Segment newSeg = null;
 382             int segType = i.currentSegment(coords);
 383             double x = coords[0];
 384             double y = coords[1];
 385 
 386             switch (segType) {
 387                 case PathIterator.SEG_MOVETO:
 388                     moveToSeg = Segment.newMoveTo(x, y, lastSeg.accumLength);
 389                     newSeg = moveToSeg;
 390                     break;
 391                 case PathIterator.SEG_CLOSE:
 392                     newSeg = Segment.newClosePath(lastSeg, moveToSeg);
 393                     if (newSeg == null) {
 394                         // make the last segment to close the path
 395                         lastSeg.convertToClosePath(moveToSeg);
 396                     }
 397                     break;
 398                 case PathIterator.SEG_LINETO:
 399                     newSeg = Segment.newLineTo(lastSeg, x, y);
 400                     break;
 401             }
 402 
 403             if (newSeg != null) {
 404                 segments.add(newSeg);
 405                 lastSeg = newSeg;
 406             }
 407         }
 408         totalLength = lastSeg.accumLength;
 409     }
 410 
 411     /**
 412      * Returns the index of the first segment having accumulated length
 413      * from the path beginning, greater than {@code length}
 414      */
 415     private int findSegment(int begin, int end, double length) {
 416         // check for search termination
 417         if (begin == end) {
 418             // find last non-moveTo segment for given length
 419             return segments.get(begin).isMoveTo && begin > 0
 420                     ? findSegment(begin - 1, begin - 1, length)
 421                     : begin;
 422         }
 423         // otherwise continue binary search
 424         int middle = begin + (end - begin) / 2;
 425         return segments.get(middle).accumLength > length
 426                 ? findSegment(begin, middle, length)
 427                 : findSegment(middle + 1, end, length);
 428     }
 429 
 430 
 431     /** Interpolates angle according to rate,
 432      *  with correct 0->360 and 360->0 transitions
 433      */
 434     private static double interpolate(double fromAngle, double toAngle, double ratio) {
 435         double delta = toAngle - fromAngle;
 436         if (Math.abs(delta) > 180) {
 437             toAngle += delta > 0 ? -360 : 360;
 438         }
 439         return normalize(fromAngle + ratio * (toAngle - fromAngle));
 440     }
 441 
 442     /** Converts angle to range 0-360
 443      */
 444     private static double normalize(double angle) {
 445         while (angle > 360) {
 446             angle -= 360;
 447         }
 448         while (angle < 0) {
 449             angle += 360;
 450         }
 451         return angle;
 452     }
 453 
 454     private static class Segment {
 455 
 456         private static final Segment zeroSegment = new Segment(true, 0, 0, 0, 0, 0);
 457         boolean isMoveTo;
 458         double length;
 459         // total length from the path's beginning to the end of this segment
 460         double accumLength;
 461         // end point of this segment
 462         double toX;
 463         double toY;
 464         // segment's rotation angle in degrees
 465         double rotateAngle;
 466         Segment prevSeg;
 467         Segment nextSeg;
 468 
 469         private Segment(boolean isMoveTo, double toX, double toY,
 470                 double length, double lengthBefore, double rotateAngle) {
 471             this.isMoveTo = isMoveTo;
 472             this.toX = toX;
 473             this.toY = toY;
 474             this.length = length;
 475             this.accumLength = lengthBefore + length;
 476             this.rotateAngle = rotateAngle;
 477         }
 478 
 479         public static Segment getZeroSegment() {
 480             return zeroSegment;
 481         }
 482 
 483         public static Segment newMoveTo(double toX, double toY,
 484                 double accumLength) {
 485             return new Segment(true, toX, toY, 0, accumLength, 0);
 486         }
 487 
 488         public static Segment newLineTo(Segment fromSeg, double toX, double toY) {
 489             double deltaX = toX - fromSeg.toX;
 490             double deltaY = toY - fromSeg.toY;
 491             double length = Math.sqrt((deltaX * deltaX) + (deltaY * deltaY));
 492             if ((length >= 1) || fromSeg.isMoveTo) { // filtering out flattening noise
 493                 double sign = Math.signum(deltaY == 0 ? deltaX : deltaY);
 494                 double angle = (sign * Math.acos(deltaX / length));
 495                 angle = normalize(angle / Math.PI * 180);
 496                 Segment newSeg = new Segment(false, toX, toY,
 497                         length, fromSeg.accumLength, angle);
 498                 fromSeg.nextSeg = newSeg;
 499                 newSeg.prevSeg = fromSeg;
 500                 return newSeg;
 501             }
 502             return null;
 503         }
 504 
 505         public static Segment newClosePath(Segment fromSeg, Segment moveToSeg) {
 506             Segment newSeg = newLineTo(fromSeg, moveToSeg.toX, moveToSeg.toY);
 507             if (newSeg != null) {
 508                 newSeg.convertToClosePath(moveToSeg);
 509             }
 510             return newSeg;
 511         }
 512 
 513         public void convertToClosePath(Segment moveToSeg) {
 514             Segment firstLineToSeg = moveToSeg.nextSeg;
 515             nextSeg = firstLineToSeg;
 516             firstLineToSeg.prevSeg = this;
 517         }
 518 
 519     }
 520 
 521 }