【问题标题】:What the easiest way to animate a Path as an object traverses it?当对象遍历路径时,为路径设置动画的最简单方法是什么?
【发布时间】:2015-05-31 20:47:00
【问题描述】:

假设一个矩形穿过一条长而线性的路径。找出形状在动画早期的位置会很有用。在形状移动之前显示整个路径不是我想要的。这很容易通过将路径添加到窗格来完成。

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

【问题讨论】:

  • 这是JavaFX path tracing animation 的副本。我无法关闭它是重复的,因为该问题没有投票或接受的答案(即使答案确实在问题中)。
  • @jewelsea:这种技术可能看起来有效,但它很丑。当速度太快时,就会出现间隙。一旦动画完成,绘图线就会被可能包含峰值和不包含峰值的路径覆盖。更好的方法是使用画布 imo。

标签: javafx javafx-8


【解决方案1】:

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,了解如何实现。

然后,您可以将PathTransition 与上述DrawPathTransition 组合到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
    }
}

【讨论】:

    【解决方案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);
        }
    }
    

    这是一个屏幕截图。红色是实际路径,蓝色是在 Canvas 上绘制的路径:

    其他解决方案使用 e。 G。 a clip。但是,如果您使用该技术选择与我在上面所做的相同的持续时间(即 5 秒),您将得到如下所示的间隙:

    使用线条绘制的解决方案也有其缺点。如果您选择 1 秒,您将看到线段。规避这种情况的一种可能是smooth the path你自己。但为此,您必须将路径分成几段,这有点数学问题。

    有点题外话,不过如何paint along the mouse coordinates 也可能对你来说很有趣,可以给你一些想法。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2020-02-03
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-05-17
      • 2011-09-28
      相关资源
      最近更新 更多