将Path作为对象设置动画的最简单方法是什么?

时间:2016-02-23 18:09:04

标签: javafx javafx-8

考虑一个遍历长而线性路径的矩形。找出动画中早期形状的位置会很有用。在形状移动之前显示整个路径不是我想要的。通过添加窗格的路径可以轻松完成。

我希望在形状后面有一条尾随线,表示形状到目前为止所经过的路径。有谁知道如何在Javafx中这样做?我正在使用Path和PathTransition在路径上为我的对象设置动画。

2 个答案:

答案 0 :(得分:2)

有各种解决方案。取决于您选择哪一个决定您的结果。

当节点沿路径移动时,您可以在其上使用画布和绘画线。

import javafx.animation.Animation;
import javafx.animation.PathTransition;
import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.CubicCurveTo;
import javafx.scene.shape.LineTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.Path;
import javafx.stage.Stage;
import javafx.util.Duration;

public class PathVisualization extends Application {

    private static double SCENE_WIDTH = 400;
    private static double SCENE_HEIGHT = 260;

    Canvas canvas;

    @Override
    public void start(Stage primaryStage) throws Exception {

        Pane root = new Pane();
        Path path = createPath();
        canvas = new Canvas(SCENE_WIDTH,SCENE_HEIGHT);

        root.getChildren().addAll(path, canvas);

        primaryStage.setScene(new Scene(root, SCENE_WIDTH, SCENE_HEIGHT));
        primaryStage.show();

        Animation animation = createPathAnimation(path, Duration.seconds(5));
        animation.play();
    }

    private Path createPath() {

        Path path = new Path();

        path.setStroke(Color.RED);
        path.setStrokeWidth(10);

        path.getElements().addAll(new MoveTo(20, 20), new CubicCurveTo(380, 0, 380, 120, 200, 120), new CubicCurveTo(0, 120, 0, 240, 380, 240), new LineTo(20,20));

        return path;
    }

    private Animation createPathAnimation(Path path, Duration duration) {

        GraphicsContext gc = canvas.getGraphicsContext2D();

        // move a node along a path. we want its position
        Circle pen = new Circle(0, 0, 4);

        // create path transition
        PathTransition pathTransition = new PathTransition( duration, path, pen);
        pathTransition.currentTimeProperty().addListener( new ChangeListener<Duration>() {

            Location oldLocation = null;

            /**
             * Draw a line from the old location to the new location
             */
            @Override
            public void changed(ObservableValue<? extends Duration> observable, Duration oldValue, Duration newValue) {

                // skip starting at 0/0
                if( oldValue == Duration.ZERO)
                    return;

                // get current location
                double x = pen.getTranslateX();
                double y = pen.getTranslateY();

                // initialize the location
                if( oldLocation == null) {
                    oldLocation = new Location();
                    oldLocation.x = x;
                    oldLocation.y = y;
                    return;
                }

                // draw line
                gc.setStroke(Color.BLUE);
                gc.setFill(Color.YELLOW);
                gc.setLineWidth(4);
                gc.strokeLine(oldLocation.x, oldLocation.y, x, y);

                // update old location with current one
                oldLocation.x = x;
                oldLocation.y = y;
            }
        });

        return pathTransition;
    }

    public static class Location {
        double x;
        double y;
    }

    public static void main(String[] args) {
        launch(args);
    }
}

这是截图的截图。红色是实际路径,蓝色是在画布上绘制的路径:

file URI scheme

其他解决方案使用e。 G。 enter image description here。但是,如果您使用该技术选择与我上面相同的持续时间(即5秒),您将得到这样的差距:

a clip

带有线条图的解决方案也有其缺点。如果选择1秒,您将看到线段。绕开这个问题的可能性就是enter image description here你自己。但是,为此你必须将路径分成几段而且这有点数学。

略微偏离主题,但如何smooth the path也可以让你为你提供想法。

答案 1 :(得分:0)

Michael Bostock通过操纵笔触虚线阵列并插入笔划短划线偏移来执行路径动画。他提供了一个示例(当然),您可以查看here

在JavaFX中可以采用相同的方法。这是一个DrawPathTransition 我创建的(Kotlin)类使用了这种技术:

class DrawPathTransition(val path: Path) : Transition() {
    private val DEFAULT_DURATION = Duration.millis(400.0)

    private val length = path.totalLength

    var duration: Duration
        get() = durationProperty.get()
        set(value) {
            durationProperty.set(value)
        }
    val durationProperty = SimpleObjectProperty(DEFAULT_DURATION)

    init {
        durationProperty.addListener({ _ -> cycleDuration = duration })

        statusProperty().addListener({ _, _, status ->
            when(status) {
                Status.RUNNING -> path.strokeDashArray.addAll(length, length)
                Status.STOPPED -> path.strokeDashArray.clear()
            }
        })
    }

    override fun interpolate(frac: Double) {
        path.strokeDashOffset = length - length * frac
    }
}

这里棘手的部分是获取路径的总长度。有关如何完成此操作,请参阅answer至此question

然后,您可以将PathTransitionDrawPathTransition中相同持续时间的上述ParallelTransition合并,以获得您想要的内容。

由于这种方法修改了strokeDashArraystrokeDashOffset,它只适用于实线,但如果我们想要支持虚线呢? Nadieh Bremer有一篇很好的文章可以查看here

下面提供的DrawPathTransition(Kotlin)类实现了这种技术。请注意,这可以在转换过程中创建相当大的strokeDashArray

class DrawPathTransition(val path: Path) : Transition() {
    private val length = path.totalLength
    private val stroked = path.strokeDashArray.isNotEmpty()
    private val dashArray: List<Double> = if(stroked) ArrayList(path.strokeDashArray) else emptyList()
    private val dashLength = dashArray.sum()
    private val dashOffset = path.strokeDashOffset

    var duration: Duration
        get() = durationProperty.get()
        set(value) {
            durationProperty.set(value)
        }
    val durationProperty = SimpleObjectProperty(DEFAULT_DURATION)

    init {
        durationProperty.addListener({ _ -> cycleDuration = duration })

        if(stroked) {
            val n = (length / dashLength).toInt()
            path.strokeDashArray.clear()
            (1..n).forEach { path.strokeDashArray.addAll(dashArray) }
            path.strokeDashArray.addAll(0.0, length)

            statusProperty().addListener({ _, _, status ->
                if(status == Animation.Status.STOPPED) {
                    path.strokeDashOffset = dashOffset
                    path.strokeDashArray.setAll(dashArray)
                }
            })
        }
    }

    override fun interpolate(frac: Double) {
        path.strokeDashOffset = length - length * frac
    }
}

我对这种方法并不完全满意,因为当绘制路径时,笔划似乎沿着路径“行进”,这对于持续时间较短而言看起来并不是很好。相反,我希望它看起来好像中风随着时间的推移被“显露”(所以没有中风运动)。下面的DrawPathTransition(Kotlin)类实现了我的解决方案:

class DrawPathTransition(val path: Path) : Transition() {
    private val length = path.totalLength
    private val stroked = path.strokeDashArray.isNotEmpty()
    private val dashArray: List<Double> = if(stroked) ArrayList(path.strokeDashArray) else emptyList()
    private val dashSum = dashArray.sum()
    private val dashOffset = path.strokeDashOffset

    var duration: Duration
        get() = durationProperty.get()
        set(value) {
            durationProperty.set(value)
        }
    val durationProperty = SimpleObjectProperty(DEFAULT_DURATION)


    init {
        durationProperty.addListener({ _ -> cycleDuration = duration })

        if(stroked) {
            statusProperty().addListener({ _, _, status ->
                if(status == Animation.Status.STOPPED) {
                    path.strokeDashOffset = dashOffset
                    path.strokeDashArray.setAll(dashArray)
                }
            })
        }
    }

    override fun interpolate(frac: Double) {
        val l = length * frac
        if(stroked) {
            path.strokeDashOffset = l

            val n = ceil(l / dashSum).toInt()
            path.strokeDashArray.clear()
            path.strokeDashArray.addAll(0.0, l)
            (1..n).forEach { path.strokeDashArray.addAll(dashArray) }
            path.strokeDashArray.addAll(0.0, length - l)
        }
        else path.strokeDashOffset = length - l
    }
}