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