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 }