【问题标题】:Move node along path without PathTransition在没有 PathTransition 的情况下沿路径移动节点
【发布时间】:2015-08-13 20:59:19
【问题描述】:

问题

我想沿着路径移动一个对象。 PathTransition 在 Duration 方面起作用,但我需要在 AnimationTimer 中使用沿路径的移动。

问题

有人知道通过 AnimationTimer 沿给定路径移动节点的方法吗?

或者,如果有人对沿硬路点在锐边处平滑节点的旋转有更好的想法,那也足够了。

代码

我需要它来沿着陡峭的路径移动物体,但旋转应该有平滑的转弯。下面的代码沿着航路点绘制路径(黑色)。

我认为这样做的一种方法是缩短路径段(红色),而不是硬 LineTo 制作 CubicCurveTo(黄色)。

PathTransition 可以方便地沿路径移动节点,并在边缘正确旋转,但不幸的是它仅在 Duration 基础上起作用。

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);
    }
}

非常感谢您的帮助!

【问题讨论】:

    标签: javafx path


    【解决方案1】:

    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();
    }
    

    【讨论】:

    • 太棒了!非常感谢您的快速帮助! :-)
    • 谢谢。请注意,现在您可以实现不同的插值函数,只要它们从 0 变为 1。就像这个:interpolator.interpolate(Math.abs(Math.sin(millis/10000*2d*Math.PI)));
    • 哦,没想到,谢谢提示。这是一个很好的插件,非常有用,我可以使用它。
    猜你喜欢
    • 2015-07-15
    • 1970-01-01
    • 2018-06-20
    • 2011-01-06
    • 1970-01-01
    • 2015-05-26
    • 1970-01-01
    • 1970-01-01
    • 2022-11-14
    相关资源
    最近更新 更多