问题
我想沿路径移动一个对象。 PathTransition在持续时间方面有效,但我需要在AnimationTimer中使用Path中的移动。
问题
有没有人知道通过AnimationTimer在给定路径上移动节点的方法?
或者,如果有人能够更好地平滑沿着硬路点平滑边缘处节点的旋转,那么它就足够了。
代码
我需要它来沿着锐利的路径移动物体,但旋转应该有平滑的转弯。下面的代码沿着航路点绘制路径(黑色)。
我认为这样做的一种方法是缩短路径段(红色),而不是用硬线来制作CubicCurveTo(黄色)。
PathTransition可以方便地沿着路径移动节点,边缘处有正确的旋转,但不幸的是它只能在持续时间的基础上工作。
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);
}
}
非常感谢您的帮助!
答案 0 :(得分:4)
PathTransition
有一个公共interpolate
方法,可以在0(开始)和1(结束)之间的任何分数中调用,但遗憾的是它不是针对用户的,并且它只能在路径转换运行时调用。
如果您了解interpolate
的工作原理,它会根据路径中的线性段使用名为Segment
的内部类。
所以第一步是将原始路径转换为线性路径:
import java.util.ArrayList;
import java.util.List;
import java.util.stream.IntStream;
import javafx.geometry.Point2D;
import javafx.scene.shape.ClosePath;
import javafx.scene.shape.CubicCurveTo;
import javafx.scene.shape.LineTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.Path;
import javafx.scene.shape.PathElement;
import javafx.scene.shape.QuadCurveTo;
/**
*
* @author jpereda
*/
public class LinearPath {
private final Path originalPath;
public LinearPath(Path path){
this.originalPath=path;
}
public Path generateLinePath(){
/*
Generate a list of points interpolating the original path
*/
originalPath.getElements().forEach(this::getPoints);
/*
Create a path only with MoveTo,LineTo
*/
Path path = new Path(new MoveTo(list.get(0).getX(),list.get(0).getY()));
list.stream().skip(1).forEach(p->path.getElements().add(new LineTo(p.getX(),p.getY())));
path.getElements().add(new ClosePath());
return path;
}
private Point2D p0;
private List<Point2D> list;
private final int POINTS_CURVE=5;
private void getPoints(PathElement elem){
if(elem instanceof MoveTo){
list=new ArrayList<>();
p0=new Point2D(((MoveTo)elem).getX(),((MoveTo)elem).getY());
list.add(p0);
} else if(elem instanceof LineTo){
list.add(new Point2D(((LineTo)elem).getX(),((LineTo)elem).getY()));
} else if(elem instanceof CubicCurveTo){
Point2D ini = (list.size()>0?list.get(list.size()-1):p0);
IntStream.rangeClosed(1, POINTS_CURVE).forEach(i->list.add(evalCubicBezier((CubicCurveTo)elem, ini, ((double)i)/POINTS_CURVE)));
} else if(elem instanceof QuadCurveTo){
Point2D ini = (list.size()>0?list.get(list.size()-1):p0);
IntStream.rangeClosed(1, POINTS_CURVE).forEach(i->list.add(evalQuadBezier((QuadCurveTo)elem, ini, ((double)i)/POINTS_CURVE)));
} else if(elem instanceof ClosePath){
list.add(p0);
}
}
private Point2D evalCubicBezier(CubicCurveTo c, Point2D ini, double t){
Point2D p=new Point2D(Math.pow(1-t,3)*ini.getX()+
3*t*Math.pow(1-t,2)*c.getControlX1()+
3*(1-t)*t*t*c.getControlX2()+
Math.pow(t, 3)*c.getX(),
Math.pow(1-t,3)*ini.getY()+
3*t*Math.pow(1-t, 2)*c.getControlY1()+
3*(1-t)*t*t*c.getControlY2()+
Math.pow(t, 3)*c.getY());
return p;
}
private Point2D evalQuadBezier(QuadCurveTo c, Point2D ini, double t){
Point2D p=new Point2D(Math.pow(1-t,2)*ini.getX()+
2*(1-t)*t*c.getControlX()+
Math.pow(t, 2)*c.getX(),
Math.pow(1-t,2)*ini.getY()+
2*(1-t)*t*c.getControlY()+
Math.pow(t, 2)*c.getY());
return p;
}
}
现在,根据PathTransition.Segment
类,删除所有私有或弃用的API,我已经使用公共interpolator
方法提出了这个类:
import java.util.ArrayList;
import javafx.geometry.Bounds;
import javafx.scene.Node;
import javafx.scene.shape.ClosePath;
import javafx.scene.shape.LineTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.Path;
/**
* Based on javafx.animation.PathTransition
*
* @author jpereda
*/
public class PathInterpolator {
private final Path originalPath;
private final Node node;
private double totalLength = 0;
private static final int SMOOTH_ZONE = 10;
private final ArrayList<Segment> segments = new ArrayList<>();
private Segment moveToSeg = Segment.getZeroSegment();
private Segment lastSeg = Segment.getZeroSegment();
public PathInterpolator(Path path, Node node){
this.originalPath=path;
this.node=node;
calculateSegments();
}
private void calculateSegments() {
segments.clear();
Path linePath = new LinearPath(originalPath).generateLinePath();
linePath.getElements().forEach(elem->{
Segment newSeg = null;
if(elem instanceof MoveTo){
moveToSeg = Segment.newMoveTo(((MoveTo)elem).getX(),((MoveTo)elem).getY(), lastSeg.accumLength);
newSeg = moveToSeg;
} else if(elem instanceof LineTo){
newSeg = Segment.newLineTo(lastSeg, ((LineTo)elem).getX(),((LineTo)elem).getY());
} else if(elem instanceof ClosePath){
newSeg = Segment.newClosePath(lastSeg, moveToSeg);
if (newSeg == null) {
lastSeg.convertToClosePath(moveToSeg);
}
}
if (newSeg != null) {
segments.add(newSeg);
lastSeg = newSeg;
}
});
totalLength = lastSeg.accumLength;
}
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);
}
}
}
node.setTranslateX(x - getPivotX());
node.setTranslateY(y - getPivotY());
node.setRotate(rotateAngle);
}
private double getPivotX() {
final Bounds bounds = node.getLayoutBounds();
return bounds.getMinX() + bounds.getWidth()/2;
}
private double getPivotY() {
final Bounds bounds = node.getLayoutBounds();
return bounds.getMinY() + bounds.getHeight()/2;
}
/**
* 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;
}
}
}
基本上,一旦你有一个线性路径,每行产生一个Segment
。现在使用这些段的列表,您可以调用interpolate
方法来计算节点在0到1之间的任何分数的位置和旋转。
最后,您可以在应用中创建AnimationTimer
:
@Override
public void start(Stage primaryStage) {
...
// move arrow on path
// --------------------------------------------
ImageView arrow = createArrow(30,30);
root.getChildren().add( arrow);
PathInterpolator interpolator=new PathInterpolator(smoothPath, arrow);
AnimationTimer timer = new AnimationTimer() {
@Override
public void handle(long now) {
double millis=(now/1_000_000)%10000;
interpolator.interpolate(millis/10000);
}
};
timer.start();
}