/*
 * Copyright (c) 2010, 2017, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package javafx.animation;

import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ObjectPropertyBase;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.Node;
import javafx.scene.shape.Shape;
import javafx.util.Duration;

import com.sun.javafx.geom.Path2D;
import com.sun.javafx.geom.PathIterator;
import com.sun.javafx.geom.transform.BaseTransform;
import com.sun.javafx.scene.NodeHelper;
import com.sun.javafx.scene.shape.ShapeHelper;
import java.util.ArrayList;

This Transition creates a path animation that spans its duration. The translation along the path is done by updating the translateX and translateY variables of the node, and the rotate variable will get updated if orientation is set to OrientationType.ORTHOGONAL_TO_TANGENT, at regular interval.

The animated path is defined by the outline of a shape.

Code Segment Example:


import javafx.scene.shape.*;
import javafx.animation.*;
...
    Rectangle rect = new Rectangle (100, 40, 100, 100);
    rect.setArcHeight(50);
    rect.setArcWidth(50);
    rect.setFill(Color.VIOLET);
    Path path = new Path();
    path.getElements().add (new MoveTo (0f, 50f));
    path.getElements().add (new CubicCurveTo (40f, 10f, 390f, 240f, 1904, 50f));
    pathTransition.setDuration(Duration.millis(10000));
    pathTransition.setNode(rect);
    pathTransition.setPath(path);
    pathTransition.setOrientation(OrientationType.ORTHOGONAL_TO_TANGENT);
    pathTransition.setCycleCount(4f);
    pathTransition.setAutoReverse(true);
    pathTransition.play();
...

See Also:
Since:JavaFX 2.0
/** * This {@code Transition} creates a path animation that spans its * {@link #durationProperty() duration}. The translation along the path is done by updating the * {@code translateX} and {@code translateY} variables of the {@code node}, and * the {@code rotate} variable will get updated if {@code orientation} is set to * {@code OrientationType.ORTHOGONAL_TO_TANGENT}, at regular interval. * <p> * The animated path is defined by the outline of a shape. * * <p> * Code Segment Example: * </p> * * <pre> * <code> * import javafx.scene.shape.*; * import javafx.animation.*; * * ... * * Rectangle rect = new Rectangle (100, 40, 100, 100); * rect.setArcHeight(50); * rect.setArcWidth(50); * rect.setFill(Color.VIOLET); * * * Path path = new Path(); * path.getElements().add (new MoveTo (0f, 50f)); * path.getElements().add (new CubicCurveTo (40f, 10f, 390f, 240f, 1904, 50f)); * * pathTransition.setDuration(Duration.millis(10000)); * pathTransition.setNode(rect); * pathTransition.setPath(path); * pathTransition.setOrientation(OrientationType.ORTHOGONAL_TO_TANGENT); * pathTransition.setCycleCount(4f); * pathTransition.setAutoReverse(true); * * pathTransition.play(); * * ... * * </code> * </pre> * * @see Transition * @see Animation * * @since JavaFX 2.0 */
public final class PathTransition extends Transition {
The target node of this PathTransition.

It is not possible to change the target node of a running PathTransition. If the value of node is changed for a running PathTransition, the animation has to be stopped and started again to pick up the new value.

/** * The target node of this {@code PathTransition}. * <p> * It is not possible to change the target {@code node} of a running * {@code PathTransition}. If the value of {@code node} is changed for a * running {@code PathTransition}, the animation has to be stopped and * started again to pick up the new value. */
private ObjectProperty<Node> node; private double totalLength = 0; private final ArrayList<Segment> segments = new ArrayList<>(); private static final Node DEFAULT_NODE = null; private static final int SMOOTH_ZONE = 10; public final void setNode(Node value) { if ((node != null) || (value != null /* DEFAULT_NODE */)) { nodeProperty().set(value); } } public final Node getNode() { return (node == null)? DEFAULT_NODE : node.get(); } public final ObjectProperty<Node> nodeProperty() { if (node == null) { node = new SimpleObjectProperty<Node>(this, "node", DEFAULT_NODE); } return node; } private Node cachedNode;
The duration of this Transition.

It is not possible to change the duration of a running PathTransition. If the value of duration is changed for a running PathTransition, the animation has to be stopped and started again to pick up the new value.

Note: While the unit of duration is a millisecond, the granularity depends on the underlying operating system and will in general be larger. For example animations on desktop systems usually run with a maximum of 60fps which gives a granularity of ~17 ms. Setting duration to value lower than Duration.ZERO will result in IllegalArgumentException.

@defaultValue400ms
/** * The duration of this {@code Transition}. * <p> * It is not possible to change the {@code duration} of a running * {@code PathTransition}. If the value of {@code duration} is changed for a * running {@code PathTransition}, the animation has to be stopped and * started again to pick up the new value. * <p> * Note: While the unit of {@code duration} is a millisecond, the * granularity depends on the underlying operating system and will in * general be larger. For example animations on desktop systems usually run * with a maximum of 60fps which gives a granularity of ~17 ms. * * Setting duration to value lower than {@link Duration#ZERO} will result * in {@link IllegalArgumentException}. * * @defaultValue 400ms */
private ObjectProperty<Duration> duration; private static final Duration DEFAULT_DURATION = Duration.millis(400); public final void setDuration(Duration value) { if ((duration != null) || (!DEFAULT_DURATION.equals(value))) { durationProperty().set(value); } } public final Duration getDuration() { return (duration == null)? DEFAULT_DURATION : duration.get(); } public final ObjectProperty<Duration> durationProperty() { if (duration == null) { duration = new ObjectPropertyBase<Duration>(DEFAULT_DURATION) { @Override public void invalidated() { try { setCycleDuration(getDuration()); } catch (IllegalArgumentException e) { if (isBound()) { unbind(); } set(getCycleDuration()); throw e; } } @Override public Object getBean() { return PathTransition.this; } @Override public String getName() { return "duration"; } }; } return duration; }
The shape on which outline the node should be animated.

It is not possible to change the path of a running PathTransition. If the value of path is changed for a running PathTransition, the animation has to be stopped and started again to pick up the new value.

@defaultValuenull
/** * The shape on which outline the node should be animated. * <p> * It is not possible to change the {@code path} of a running * {@code PathTransition}. If the value of {@code path} is changed for a * running {@code PathTransition}, the animation has to be stopped and * started again to pick up the new value. * * @defaultValue null */
private ObjectProperty<Shape> path; private static final Shape DEFAULT_PATH = null; public final void setPath(Shape value) { if ((path != null) || (value != null /* DEFAULT_PATH */)) { pathProperty().set(value); } } public final Shape getPath() { return (path == null)? DEFAULT_PATH : path.get(); } public final ObjectProperty<Shape> pathProperty() { if (path == null) { path = new SimpleObjectProperty<Shape>(this, "path", DEFAULT_PATH); } return path; }
Specifies the upright orientation of node along the path.
Since:JavaFX 2.0
/** * Specifies the upright orientation of {@code node} along the {@code path}. * @since JavaFX 2.0 */
public static enum OrientationType {
The targeted node's rotation matrix stays unchange along the geometric path.
/** * The targeted {@code node}'s rotation matrix stays unchange along the * geometric path. */
NONE,
The targeted node's rotation matrix is set to keep node perpendicular to the path's tangent along the geometric path.
/** * The targeted node's rotation matrix is set to keep {@code node} * perpendicular to the path's tangent along the geometric path. */
ORTHOGONAL_TO_TANGENT }
Specifies the upright orientation of node along the path. The default orientation is set to OrientationType.NONE.

It is not possible to change the orientation of a running PathTransition. If the value of orientation is changed for a running PathTransition, the animation has to be stopped and started again to pick up the new value.

@defaultValueNONE
/** * Specifies the upright orientation of {@code node} along the {@code path}. * The default orientation is set to {@link OrientationType#NONE}. * <p> * It is not possible to change the {@code orientation} of a running * {@code PathTransition}. If the value of {@code orientation} is changed * for a running {@code PathTransition}, the animation has to be stopped and * started again to pick up the new value. * * @defaultValue NONE */
private ObjectProperty<OrientationType> orientation; private static final OrientationType DEFAULT_ORIENTATION = OrientationType.NONE; public final void setOrientation(OrientationType value) { if ((orientation != null) || (!DEFAULT_ORIENTATION.equals(value))) { orientationProperty().set(value); } } public final OrientationType getOrientation() { return (orientation == null)? OrientationType.NONE : orientation.get(); } public final ObjectProperty<OrientationType> orientationProperty() { if (orientation == null) { orientation = new SimpleObjectProperty<OrientationType>(this, "orientation", DEFAULT_ORIENTATION); } return orientation; } private boolean cachedIsNormalRequired;
The constructor of PathTransition.
Params:
  • duration – The duration of this PathTransition
  • path – The path of this PathTransition
  • node – The node of this PathTransition
/** * The constructor of {@code PathTransition}. * * @param duration * The {@link #durationProperty() duration} of this {@code PathTransition} * @param path * The {@link #pathProperty() path} of this {@code PathTransition} * @param node * The {@link #nodeProperty() node} of this {@code PathTransition} */
public PathTransition(Duration duration, Shape path, Node node) { setDuration(duration); setPath(path); setNode(node); setCycleDuration(duration); }
The constructor of PathTransition.
Params:
  • duration – The duration of this PathTransition
  • path – The path of this PathTransition
/** * The constructor of {@code PathTransition}. * * @param duration * The {@link #durationProperty() duration} of this {@code PathTransition} * @param path * The {@link #pathProperty() path} of this {@code PathTransition} */
public PathTransition(Duration duration, Shape path) { this(duration, path, null); }
The constructor of PathTransition.
/** * The constructor of {@code PathTransition}. */
public PathTransition() { this(DEFAULT_DURATION, null, null); }
{@inheritDoc}
/** * {@inheritDoc} */
@Override public void interpolate(double frac) { double part = totalLength * Math.min(1, Math.max(0, frac)); int segIdx = findSegment(0, segments.size() - 1, part); Segment seg = segments.get(segIdx); double lengthBefore = seg.accumLength - seg.length; double partLength = part - lengthBefore; double ratio = partLength / seg.length; Segment prevSeg = seg.prevSeg; double x = prevSeg.toX + (seg.toX - prevSeg.toX) * ratio; double y = prevSeg.toY + (seg.toY - prevSeg.toY) * ratio; double rotateAngle = seg.rotateAngle; // provide smooth rotation on segment bounds double z = Math.min(SMOOTH_ZONE, seg.length / 2); if (partLength < z && !prevSeg.isMoveTo) { //interpolate rotation to previous segment rotateAngle = interpolate( prevSeg.rotateAngle, seg.rotateAngle, partLength / z / 2 + 0.5F); } else { double dist = seg.length - partLength; Segment nextSeg = seg.nextSeg; if (dist < z && nextSeg != null) { //interpolate rotation to next segment if (!nextSeg.isMoveTo) { rotateAngle = interpolate( seg.rotateAngle, nextSeg.rotateAngle, (z - dist) / z / 2); } } } cachedNode.setTranslateX(x - NodeHelper.getPivotX(cachedNode)); cachedNode.setTranslateY(y - NodeHelper.getPivotY(cachedNode)); // Need to handle orientation if it is requested if (cachedIsNormalRequired) { cachedNode.setRotate(rotateAngle); } } private Node getTargetNode() { final Node node = getNode(); return (node != null) ? node : getParentTargetNode(); } @Override boolean startable(boolean forceSync) { return super.startable(forceSync) && (((getTargetNode() != null) && (getPath() != null) && !getPath().getLayoutBounds().isEmpty()) || (!forceSync && (cachedNode != null))); } @Override void sync(boolean forceSync) { super.sync(forceSync); if (forceSync || (cachedNode == null)) { cachedNode = getTargetNode(); recomputeSegments(); cachedIsNormalRequired = getOrientation() == OrientationType.ORTHOGONAL_TO_TANGENT; } } private void recomputeSegments() { segments.clear(); final Shape p = getPath(); Segment moveToSeg = Segment.getZeroSegment(); Segment lastSeg = Segment.getZeroSegment(); float[] coords = new float[6]; for (PathIterator i = ShapeHelper.configShape(p).getPathIterator(NodeHelper.getLeafTransform(p), 1.0f); !i.isDone(); i.next()) { Segment newSeg = null; int segType = i.currentSegment(coords); double x = coords[0]; double y = coords[1]; switch (segType) { case PathIterator.SEG_MOVETO: moveToSeg = Segment.newMoveTo(x, y, lastSeg.accumLength); newSeg = moveToSeg; break; case PathIterator.SEG_CLOSE: newSeg = Segment.newClosePath(lastSeg, moveToSeg); if (newSeg == null) { // make the last segment to close the path lastSeg.convertToClosePath(moveToSeg); } break; case PathIterator.SEG_LINETO: newSeg = Segment.newLineTo(lastSeg, x, y); break; } if (newSeg != null) { segments.add(newSeg); lastSeg = newSeg; } } totalLength = lastSeg.accumLength; }
Returns the index of the first segment having accumulated length from the path beginning, greater than length
/** * Returns the index of the first segment having accumulated length * from the path beginning, greater than {@code length} */
private int findSegment(int begin, int end, double length) { // check for search termination if (begin == end) { // find last non-moveTo segment for given length return segments.get(begin).isMoveTo && begin > 0 ? findSegment(begin - 1, begin - 1, length) : begin; } // otherwise continue binary search int middle = begin + (end - begin) / 2; return segments.get(middle).accumLength > length ? findSegment(begin, middle, length) : findSegment(middle + 1, end, length); }
Interpolates angle according to rate, with correct 0->360 and 360->0 transitions
/** Interpolates angle according to rate, * with correct 0->360 and 360->0 transitions */
private static double interpolate(double fromAngle, double toAngle, double ratio) { double delta = toAngle - fromAngle; if (Math.abs(delta) > 180) { toAngle += delta > 0 ? -360 : 360; } return normalize(fromAngle + ratio * (toAngle - fromAngle)); }
Converts angle to range 0-360
/** Converts angle to range 0-360 */
private static double normalize(double angle) { while (angle > 360) { angle -= 360; } while (angle < 0) { angle += 360; } return angle; } private static class Segment { private static final Segment zeroSegment = new Segment(true, 0, 0, 0, 0, 0); boolean isMoveTo; double length; // total length from the path's beginning to the end of this segment double accumLength; // end point of this segment double toX; double toY; // segment's rotation angle in degrees double rotateAngle; Segment prevSeg; Segment nextSeg; private Segment(boolean isMoveTo, double toX, double toY, double length, double lengthBefore, double rotateAngle) { this.isMoveTo = isMoveTo; this.toX = toX; this.toY = toY; this.length = length; this.accumLength = lengthBefore + length; this.rotateAngle = rotateAngle; } public static Segment getZeroSegment() { return zeroSegment; } public static Segment newMoveTo(double toX, double toY, double accumLength) { return new Segment(true, toX, toY, 0, accumLength, 0); } public static Segment newLineTo(Segment fromSeg, double toX, double toY) { double deltaX = toX - fromSeg.toX; double deltaY = toY - fromSeg.toY; double length = Math.sqrt((deltaX * deltaX) + (deltaY * deltaY)); if ((length >= 1) || fromSeg.isMoveTo) { // filtering out flattening noise double sign = Math.signum(deltaY == 0 ? deltaX : deltaY); double angle = (sign * Math.acos(deltaX / length)); angle = normalize(angle / Math.PI * 180); Segment newSeg = new Segment(false, toX, toY, length, fromSeg.accumLength, angle); fromSeg.nextSeg = newSeg; newSeg.prevSeg = fromSeg; return newSeg; } return null; } public static Segment newClosePath(Segment fromSeg, Segment moveToSeg) { Segment newSeg = newLineTo(fromSeg, moveToSeg.toX, moveToSeg.toY); if (newSeg != null) { newSeg.convertToClosePath(moveToSeg); } return newSeg; } public void convertToClosePath(Segment moveToSeg) { Segment firstLineToSeg = moveToSeg.nextSeg; nextSeg = firstLineToSeg; firstLineToSeg.prevSeg = this; } } }