问题
Consider a rectangle traversing a long, linear path. It would be useful to figure out where the shape had gone earlier in the animation. Displaying the entire path before the shape moves is not what I want. That is easily done by adding the path to the pane.
I want a trailing line behind the shape representing the path that the shape has traversed through so far. Does anyone know how to do this in Javafx? I am using Path and PathTransition to animate my object along a path.
回答1:
There are various solutions. Depending on which one you choose decides your outcome.
You could use a Canvas and paint lines on it while a Node moves along the Path.
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);
}
}
Here's a screenshot how it looks like. Red is the actual path, Blue is the path that is drawn on the Canvas:
Other solutions use e. g. a clip. However, if you choose the same Duration as I did above (i. e. 5 seconds) with that technique, you'll get gaps like this:
The solution with the line drawing has its drawbacks as well. If you choose 1 second, you'll see the line segments. A possibiliy to circumvent this would be to smooth the path yourself. But for that you'd have to get into splitting the path into segments and that's a bit math-y.
Slightly offtopic, but how to paint along the mouse coordinates may also be interesing for you to give you ideas.
回答2:
Michael Bostock does a path animation by manipulating the stroke dash array and interpolating the stroke dash offset. He provides an example (of course) which you can view here.
The same approach can be taken in JavaFX. Here is a DrawPathTransition
(Kotlin) class I created which uses this technique:
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
}
}
The tricky part here is getting the path's total length. See my answer to this question for how that can be accomplished.
You can then combine a PathTransition with the above DrawPathTransition of the same duration in a ParallelTransition to get what you desire.
Since this approach modifies strokeDashArray and strokeDashOffset it only works with solid lines, but what if we want to support dashed lines as well? Nadieh Bremer has an excellent article about this which can be reviewed here.
The DrawPathTransition (Kotlin) class provided below implements this technique. Note that this can create a rather large strokeDashArray during the transition.
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
}
}
I wasn't completely happy with this approach though, as the stroke appears to "march" along the path as the path is drawn, which doesn't look great particularly with short durations. Rather I wanted it to appear as if the stroke was being "revealed" over time (so no stroke movement). The DrawPathTransition (Kotlin) class below implements my solution:
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
}
}
来源:https://stackoverflow.com/questions/29302120/javafx-path-tracing-animation