JavaFX 中的嵌套 PathTransitions

Nested PathTransitions in JavaFX

我试图让我的节点沿着一个圆的路径行进,同时让那个圆沿着一个矩形的路径行进。可能吗?

这是我目前拥有的:

void move(GamePane aThis)
{

    double speed = 10;
    Rectangle rectangle = new Rectangle(100, 200, 100, 500);

    Circle circle = new Circle(50);
    circle.setFill(Color.WHITE);
    circle.setStroke(Color.BLACK);
    circle.setStrokeWidth(3);


    PathTransition pt = new PathTransition();
    pt.setDuration(Duration.millis(1000));
    pt.setPath(circle);
    pt.setNode(this);
    pt.setOrientation(PathTransition.OrientationType.ORTHOGONAL_TO_TANGENT);
    pt.setCycleCount(Timeline.INDEFINITE);
    pt.setAutoReverse(false);
    pt.play();

    PathTransition pt2 = new PathTransition();
    pt2.setDuration(Duration.millis(1000));
    pt2.setPath(rectangle);
    pt2.setNode(circle);
         pt2.setOrientation
   (PathTransition.OrientationType.ORTHOGONAL_TO_TANGENT);
    pt2.setCycleCount(Timeline.INDEFINITE);
    pt2.setAutoReverse(false);
    pt2.play();

}

理论上应该可以将一个转换嵌套在另一个转换之上。

但是有一个问题:转换应用于翻译属性,而节点布局没有被修改。对于您的情况,这意味着圆将遵循矩形定义的路径,但您的节点将继续在圆的初始位置上旋转。

所以我们需要想办法在任何时刻更新圆的位置,这样节点就可以在那个位置旋转。

基于这个,一种可能的方法是使用两个AnimationTimer,以及一种在任何时刻插入路径并相应地更新位置的方法。

第一步是将原始路径转换为仅使用线性元素的路径:

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

现在,基于 javafx.animation.PathTransition.Segment 内部 class,并删除所有私有或弃用的 API,此 class 允许 public 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();
    }

    public PathInterpolator(Shape shape, Node node){
        this.originalPath=(Path)Shape.subtract(shape, new Rectangle(0,0));
        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) {
        interpolate(frac,0,0);
    }

    public void interpolate(double frac, double translateX, double translateY) {
        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 = interpolateAngle(
                    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 = interpolateAngle(
                            seg.rotateAngle, nextSeg.rotateAngle,
                            (z - dist) / z / 2);
                }
            }
        }
        node.setTranslateX(x - getPivotX() + translateX);
        node.setTranslateY(y - getPivotY() + translateY);
        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 interpolateAngle(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) {
    Pane root = new Pane();

    Polygon poly = new Polygon( 0, 0, 30, 15, 0, 30); 
    poly.setFill(Color.YELLOW);
    poly.setStroke(Color.RED);
    root.getChildren().add(poly);

    Rectangle rectangle = new Rectangle(200, 100, 100, 400);
    rectangle.setFill(Color.TRANSPARENT);
    rectangle.setStroke(Color.BLUE);

    Circle circle = new Circle(50);
    circle.setFill(Color.TRANSPARENT);
    circle.setStroke(Color.RED);
    circle.setStrokeWidth(3);

    root.getChildren().add(rectangle);
    root.getChildren().add(circle);

    PathInterpolator in1=new PathInterpolator(rectangle, circle);
    PathInterpolator in2=new PathInterpolator(circle, poly);

    AnimationTimer timer1 = new AnimationTimer() {

        @Override
        public void handle(long now) {
            double millis=(now/1_000_000)%10000;
            in1.interpolate(millis/10000);
        }
    };

    AnimationTimer timer2 = new AnimationTimer() {

        @Override
        public void handle(long now) {
            double millis=(now/1_000_000)%2000;
            // Interpolate over the translated circle
            in2.interpolate(millis/2000,
                            circle.getTranslateX(),
                            circle.getTranslateY());
        }
    };
    timer2.start();
    timer1.start();

    Scene scene = new Scene(root, 800, 600);
    primaryStage.setScene(scene);
    primaryStage.show();
}

请注意,您可以对动画应用不同的速度。

这张照片拍摄了这个动画的两个瞬间。