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