/*
* Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved.
* ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*/
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 java.util.
ArrayList;
/**
* This {@code Transition} creates a path animation that spans its
* {@link #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.transition.*;
*
* ...
*
* 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 {@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 {@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.
* <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 {@code node} along the {@code path}.
* @since JavaFX 2.0
*/
public static enum
OrientationType {
/**
* The targeted {@code node}'s rotation matrix stays unchange along the
* geometric path.
*/
NONE,
/**
* 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 {@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 {@code PathTransition}.
*
* @param duration
* The {@link #duration} of this {@code PathTransition}
* @param path
* The {@link #path} of this {@code PathTransition}
* @param node
* The {@link #node} of this {@code PathTransition}
*/
public
PathTransition(
Duration duration,
Shape path,
Node node) {
setDuration(
duration);
setPath(
path);
setNode(
node);
setCycleDuration(
duration);
}
/**
* The constructor of {@code PathTransition}.
*
* @param duration
* The {@link #duration} of this {@code PathTransition}
* @param path
* The {@link #path} of this {@code PathTransition}
*/
public
PathTransition(
Duration duration,
Shape path) {
this(
duration,
path, null);
}
/**
* The constructor of {@code PathTransition}.
*/
public
PathTransition() {
this(
DEFAULT_DURATION, null, null);
}
/**
* {@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 -
cachedNode.
impl_getPivotX());
cachedNode.
setTranslateY(
y -
cachedNode.
impl_getPivotY());
// 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
impl_startable(boolean
forceSync) {
return super.impl_startable(
forceSync)
&& (((
getTargetNode() != null) && (
getPath() != null) && !
getPath().
getLayoutBounds().
isEmpty()) || (!
forceSync
&& (
cachedNode != null)));
}
@
Override
void
impl_sync(boolean
forceSync) {
super.impl_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 =
p.
impl_configShape().
getPathIterator(
p.
impl_getLeafTransform(), 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 {@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
*/
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
*/
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;
}
}
}