Issue
I don't have an MWE because I'm not sure how to start. I guess my question is mostly about which are the best tools for the job.
I have an object that amounts to a Function<Double, VectorXYZ>
, which outputs the position of an object given a time. It handles its own interpolation. I'm wondering if there's a way to handle the functionality of a Timeline
without having to use KeyFrame
s. I would like to be able to both play it forward, and to use a Slider
.
I thought of having a DoubleProperty
that is somehow linked to the Timeline
, associated with a Listener
that updates the translation property of the Group
containing the object. But I don't know how to go about doing that.
Thanks for your help!
Solution
So yeah, it was a very vague question, but as @James_D said, AnimationTimer
is the tool I was looking for. Basically I was looking for low-level access to the animation loop, and this seemed to be it. Here's an MWE of an object following some path across the Scene. It separates system time from scene time (stored as a DoubleProperty) so that it can be paused and restarted, and the time can be set via a Slider as well.
import com.google.common.base.Function;
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.geometry.Point2D;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.SubScene;
import javafx.scene.control.Slider;
import javafx.scene.input.KeyCode;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
public class AnimationTestApp extends Application {
private static final double durationSeconds = 5;
private static final double screenWidthMeters = 10;
private static final double screenHeightMeters = 10;
private static final double pixelsPerMeter = 50;
private static final double squareSizeMeters = 0.5;
private static final double screenWidthPixels = pixelsPerMeter * screenWidthMeters;
private static final double screenHeightPixels = pixelsPerMeter * screenHeightMeters;
private static final double squareSizePixels = pixelsPerMeter * squareSizeMeters;
private static final double originXPixels = screenWidthPixels/2;
private static final double originYPixels = screenHeightPixels/2;
private final Rectangle square = new Rectangle(squareSizePixels, squareSizePixels, Color.RED);
private long lastTime = -1;
private boolean isStopped = true;
private double t = 0;
private DoubleProperty timeProperty;
private DoubleProperty timeProperty() {
if (timeProperty == null) {
timeProperty = new SimpleDoubleProperty();
timeProperty.addListener((obs, ov, nv) -> {updateScene();});
}
return timeProperty;
}
@Override
public void start(Stage primaryStage) throws Exception {
final SubScene subscene = new SubScene(new Group(square), screenWidthPixels, screenHeightPixels);
Slider timeSlider = new Slider(0, 5, 1);
timeSlider.valueProperty().bindBidirectional(timeProperty());
VBox root = new VBox(timeSlider, subscene);
Scene scene = new Scene(root);
primaryStage.setScene(scene);
primaryStage.show();
AnimationTimer animationTimer = buildTimer();
handleKeyboard(scene, animationTimer);
}
private AnimationTimer buildTimer() {
AnimationTimer animationTimer = new AnimationTimer() {
@Override
public void handle(long now) {
double elapsedNS = now - lastTime;
double dt = elapsedNS * 1E-9;
if (timeProperty().get() + dt > durationSeconds) {
stop();
}
timeProperty().set(timeProperty().get() + dt);
updateScene();//timeProperty.get());
lastTime = now;
}
@Override
public void start() {
lastTime = System.nanoTime();
isStopped = false;
if (timeProperty().get() > durationSeconds) {
timeProperty().set(0);
}
super.start();
}
@Override
public void stop() {
isStopped = true;
super.stop();
}
};
return animationTimer;
}
private void updateScene() {
double t = timeProperty().get();
Point2D point = positionFunction().apply(t);
double xPixels = originXPixels + point.getX() * pixelsPerMeter;
double yPixels = originYPixels + point.getY() * pixelsPerMeter;
square.setTranslateX(xPixels);
square.setTranslateY(yPixels);
}
private Function<Double, Point2D> positionFunction() {
double radius = 3;
double period = 2;
return (t) -> new Point2D(radius * Math.sin(2*Math.PI*t/period), radius * Math.cos(2*Math.PI*t/period));
}
private void handleKeyboard(Scene scene, AnimationTimer timer) {
scene.setOnKeyPressed((ke) -> {
if (ke.getCode().equals(KeyCode.SPACE)) {
if (isStopped) {timer.start();}
else {timer.stop();}
}
});
}
}
Answered By - Frank Harris