Problem
I want to move an object along a Path. The PathTransition works in terms of Duration, but I need to use the movement along the Path in an AnimationTimer.
Question
Does anyone know a way to move a Node along a given Path via AnimationTimer?
Or if someone has a better idea of smoothing the rotation of the nodes at the sharp edges along hard waypoints, it would suffice as well.
Code
I need it for moving an object along a sharp path, but the rotation should have smooth turns. The code below draws the path along waypoints (black color).
I thought a means of doing this would be to shorten the path segments (red color) and instead of a hard LineTo make a CubicCurveTo (yellow color).
The PathTransition conveniently would move the Node along the path with correct rotation at the edges, but unfortunately it works only on a Duration basis.
import java.util.ArrayList;
import java.util.List;
import javafx.animation.PathTransition;
import javafx.animation.PathTransition.OrientationType;
import javafx.animation.Transition;
import javafx.application.Application;
import javafx.geometry.Point2D;
import javafx.scene.Scene;
import javafx.scene.SnapshotParameters;
import javafx.scene.image.ImageView;
import javafx.scene.image.WritableImage;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.CubicCurveTo;
import javafx.scene.shape.LineTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.Path;
import javafx.scene.shape.Polygon;
import javafx.scene.shape.StrokeType;
import javafx.stage.Stage;
import javafx.util.Duration;
/**
* Cut a given path.
* Black = original
* Red = cut off
* Yellow = smoothed using bezier curve
*/
public class Main extends Application {
/**
* Pixels that are cut off from start and end of the paths in order to shorten them and make the path smoother.
*/
private double SMOOTHNESS = 30;
@Override
public void start(Stage primaryStage) {
Pane root = new Pane();
Scene scene = new Scene(root,1600,900);
primaryStage.setScene(scene);
primaryStage.show();
// get waypoints for path
List<Point2D> waypoints = getWayPoints();
// draw a path with sharp edges
// --------------------------------------------
Path sharpPath = createSharpPath( waypoints);
sharpPath.setStroke(Color.BLACK);
sharpPath.setStrokeWidth(8);
sharpPath.setStrokeType(StrokeType.CENTERED);
root.getChildren().add( sharpPath);
// draw a path with shortened edges
// --------------------------------------------
Path shortenedPath = createShortenedPath(waypoints, SMOOTHNESS);
shortenedPath.setStroke(Color.RED);
shortenedPath.setStrokeWidth(5);
shortenedPath.setStrokeType(StrokeType.CENTERED);
root.getChildren().add( shortenedPath);
// draw a path with smooth edges
// --------------------------------------------
Path smoothPath = createSmoothPath(waypoints, SMOOTHNESS);
smoothPath.setStroke(Color.YELLOW);
smoothPath.setStrokeWidth(2);
smoothPath.setStrokeType(StrokeType.CENTERED);
root.getChildren().add( smoothPath);
// move arrow on path
// --------------------------------------------
ImageView arrow = createArrow(30,30);
root.getChildren().add( arrow);
PathTransition pt = new PathTransition( Duration.millis(10000), smoothPath);
pt.setNode(arrow);
pt.setAutoReverse(true);
pt.setCycleCount( Transition.INDEFINITE);
pt.setOrientation(OrientationType.ORTHOGONAL_TO_TANGENT);
pt.play();
}
/**
* Create a path from the waypoints
* @param waypoints
* @return
*/
private Path createSharpPath( List<Point2D> waypoints) {
Path path = new Path();
for( Point2D point: waypoints) {
if( path.getElements().isEmpty()) {
path.getElements().add(new MoveTo( point.getX(), point.getY()));
}
else {
path.getElements().add(new LineTo( point.getX(), point.getY()));
}
}
return path;
}
/**
* Create a path from the waypoints, shorten the path and create a line segment between segments
* @param smoothness Pixels that are cut of from start and end.
* @return
*/
private Path createShortenedPath( List<Point2D> waypoints, double smoothness) {
Path path = new Path();
// waypoints to path
Point2D prev = null;
double x;
double y;
for( int i=0; i < waypoints.size(); i++) {
Point2D curr = waypoints.get( i);
if( i == 0) {
path.getElements().add(new MoveTo( curr.getX(), curr.getY()));
x = curr.getX();
y = curr.getY();
}
else {
// shorten previous path
double distanceX = curr.getX() - prev.getX();
double distanceY = curr.getY() - prev.getY();
double rad = Math.atan2(distanceY, distanceX);
double distance = Math.sqrt( distanceX * distanceX + distanceY * distanceY);
// cut off the paths except the last one
if( i != waypoints.size() - 1) {
distance -= smoothness;
}
x = prev.getX() + distance * Math.cos(rad);
y = prev.getY() + distance * Math.sin(rad);
path.getElements().add(new LineTo( x, y));
// shorten current path
if( i + 1 < waypoints.size()) {
Point2D next = waypoints.get( i+1);
distanceX = next.getX() - curr.getX();
distanceY = next.getY() - curr.getY();
distance = smoothness;
rad = Math.atan2(distanceY, distanceX);
x = curr.getX() + distance * Math.cos(rad);
y = curr.getY() + distance * Math.sin(rad);
path.getElements().add(new LineTo( x, y));
}
}
prev = curr;
}
return path;
}
/**
* Create a path from the waypoints, shorten the path and create a smoothing cubic curve segment between segments
* @param smoothness Pixels that are cut of from start and end.
* @return
*/
private Path createSmoothPath( List<Point2D> waypoints, double smoothness) {
Path smoothPath = new Path();
smoothPath.setStroke(Color.YELLOW);
smoothPath.setStrokeWidth(2);
smoothPath.setStrokeType(StrokeType.CENTERED);
// waypoints to path
Point2D ctrl1;
Point2D ctrl2;
Point2D prev = null;
double x;
double y;
for( int i=0; i < waypoints.size(); i++) {
Point2D curr = waypoints.get( i);
if( i == 0) {
smoothPath.getElements().add(new MoveTo( curr.getX(), curr.getY()));
x = curr.getX();
y = curr.getY();
}
else {
// shorten previous path
double distanceX = curr.getX() - prev.getX();
double distanceY = curr.getY() - prev.getY();
double rad = Math.atan2(distanceY, distanceX);
double distance = Math.sqrt( distanceX * distanceX + distanceY * distanceY);
// cut off the paths except the last one
if( i != waypoints.size() - 1) {
distance -= smoothness;
}
// System.out.println( "Segment " + i + ", angle: " + Math.toDegrees( rad) + ", distance: " + distance);
x = prev.getX() + distance * Math.cos(rad);
y = prev.getY() + distance * Math.sin(rad);
smoothPath.getElements().add(new LineTo( x, y));
// shorten current path and add a smoothing segment to it
if( i + 1 < waypoints.size()) {
Point2D next = waypoints.get( i+1);
distanceX = next.getX() - curr.getX();
distanceY = next.getY() - curr.getY();
distance = smoothness;
rad = Math.atan2(distanceY, distanceX);
x = curr.getX() + distance * Math.cos(rad);
y = curr.getY() + distance * Math.sin(rad);
ctrl1 = curr;
ctrl2 = curr;
smoothPath.getElements().add(new CubicCurveTo(ctrl1.getX(), ctrl1.getY(), ctrl2.getX(), ctrl2.getY(), x, y));
}
}
prev = curr;
}
return smoothPath;
}
/**
* Waypoints for the path
* @return
*/
public List<Point2D> getWayPoints() {
List<Point2D> path = new ArrayList<>();
// rectangle
// path.add(new Point2D( 100, 100));
// path.add(new Point2D( 400, 100));
// path.add(new Point2D( 400, 400));
// path.add(new Point2D( 100, 400));
// path.add(new Point2D( 100, 100));
// rectangle with peak on right
path.add(new Point2D( 100, 100));
path.add(new Point2D( 400, 100));
path.add(new Point2D( 450, 250));
path.add(new Point2D( 400, 400));
path.add(new Point2D( 100, 400));
path.add(new Point2D( 100, 100));
return path;
}
/**
* Create an arrow as ImageView
* @param width
* @param height
* @return
*/
private ImageView createArrow( double width, double height) {
WritableImage wi;
Polygon arrow = new Polygon( 0, 0, width, height / 2, 0, height); // left/right lines of the arrow
SnapshotParameters parameters = new SnapshotParameters();
parameters.setFill(Color.TRANSPARENT);
wi = new WritableImage( (int) width, (int) height);
arrow.snapshot(parameters, wi);
return new ImageView( wi);
}
public static void main(String[] args) {
launch(args);
}
}
Thanks a lot for the help!