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 }