adding path transition to Group in JavaFX - javafx

I'm writing some javaFX code and I have added some Paths to a Group, this Group has been added to another Group which has been added to the root and is displaying on the screen. Now at a certain point I want to use a PathTransition to animate this lowest level group to a new location. I'm having trouble working out the correct coordinates for the transition. I have read that the PathTransition will animate the node from its center and not the upper left hand corner so I have tried adding
Group.getLayoutX()/2 to the starting x and Group.getLayoutY()/2 to the starting y coord but it still seems to make the group jump to a new starting location before the animation begins.
The final destination seems a bit off as well.
Is there a better way to animate a Group that contains several Paths?

Adjust the layoutX and layoutY of the nodes following paths based upon half the layout bounds of the nodes being animated.
rect.setLayoutX(rect.getLayoutX() + rect.getLayoutBounds().getWidth() / 2);
rect.setLayoutY(rect.getLayoutY() + rect.getLayoutBounds().getHeight() / 2);
Try the modification of Uluk's circle path transition code from JavaFX 2 circle path for animation.
The code will have the upper left corner of the nodes original layout follow the path.
import javafx.animation.PathTransition.OrientationType;
import javafx.animation.*;
import javafx.application.Application;
import javafx.scene.*;
import javafx.scene.paint.Color;
import javafx.scene.shape.*;
import javafx.stage.Stage;
import javafx.util.Duration;
public class ArcToDemo extends Application {
private PathTransition pathTransitionEllipse;
private PathTransition pathTransitionCircle;
private void init(Stage primaryStage) {
Group root = new Group();
primaryStage.setResizable(false);
primaryStage.setScene(new Scene(root, 600, 460));
// Ellipse path example
Rectangle rect = new Rectangle(0, 0, 40, 40);
rect.setArcHeight(10);
rect.setArcWidth(10);
rect.setFill(Color.ORANGE);
root.getChildren().add(rect);
Path path = createEllipsePath(200, 200, 50, 100, 45);
root.getChildren().add(path);
pathTransitionEllipse = PathTransitionBuilder.create()
.duration(Duration.seconds(4))
.path(path)
.node(rect)
.orientation(OrientationType.NONE)
.cycleCount(Timeline.INDEFINITE)
.autoReverse(false)
.build();
rect.setLayoutX(rect.getLayoutX() + rect.getLayoutBounds().getWidth() / 2);
rect.setLayoutY(rect.getLayoutY() + rect.getLayoutBounds().getHeight() / 2);
// Circle path example
Rectangle rect2 = new Rectangle(0, 0, 20, 20);
rect2.setArcHeight(10);
rect2.setArcWidth(10);
rect2.setFill(Color.GREEN);
root.getChildren().add(rect2);
Path path2 = createEllipsePath(400, 200, 150, 150, 0);
root.getChildren().add(path2);
pathTransitionCircle = PathTransitionBuilder.create()
.duration(Duration.seconds(2))
.path(path2)
.node(rect2)
.orientation(OrientationType.NONE)
.cycleCount(Timeline.INDEFINITE)
.autoReverse(false)
.build();
rect2.setLayoutX(rect2.getLayoutX() + rect2.getLayoutBounds().getWidth() / 2);
rect2.setLayoutY(rect2.getLayoutY() + rect2.getLayoutBounds().getHeight() / 2);
}
private Path createEllipsePath(double centerX, double centerY, double radiusX, double radiusY, double rotate) {
ArcTo arcTo = new ArcTo();
arcTo.setX(centerX - radiusX + 1); // to simulate a full 360 degree celcius circle.
arcTo.setY(centerY - radiusY);
arcTo.setSweepFlag(false);
arcTo.setLargeArcFlag(true);
arcTo.setRadiusX(radiusX);
arcTo.setRadiusY(radiusY);
arcTo.setXAxisRotation(rotate);
Path path = PathBuilder.create()
.elements(
new MoveTo(centerX - radiusX, centerY - radiusY),
arcTo,
new ClosePath()) // close 1 px gap.
.build();
path.setStroke(Color.DODGERBLUE);
path.getStrokeDashArray().setAll(5d, 5d);
return path;
}
#Override
public void start(Stage primaryStage) throws Exception {
init(primaryStage);
primaryStage.show();
pathTransitionEllipse.play();
pathTransitionCircle.play();
}
public static void main(String[] args) {
launch(args);
}
}
Implementation Notes
The code assumes that the original layout position of the nodes being animated is 0,0 (if it is not then you will need to adjust accordingly).
For non-square nodes, there is a slight gap between the node and the path (because the calculations are based on the layout bounds rectangle and not the visible shape of the node).
Also the path transition is using an orientation of NONE rather than ORTHOGONAL_TO_TANGENT (otherwise the layout calculations give a strange effect as the node starts to rotate and it seems as though the node is traveling divergent to the path).
Instead of the above approach, you could define a custom subclass of Transition which modifies the TranslateX and TranslateY properties of the node so that the corner of the node follows the path rather than the center and no modifications to the node's layout are required, however that would be significantly more work.

Related

JavaFX | changing Line anchor?

I tried drawing a line and changing its end coordinate when dragging.
The problem is, it changes both of the lines' ends, with respect to the middle, supposedly the anchor.
Is there a way to move the anchor to the start of the line?
My code is:
Line path = new Line(30,30, 70 , 75);
path.setStrokeWidth(5);
Circle point = new Circle(3);
point.setCenterX(path.getEndX());
point.setCenterY(path.getEndY());
point.setFill(Paint.valueOf("red"));
point.setOnMouseDragged(e ->{
point.setCenterX(e.getX());
point.setCenterY(e.getY());
path.setEndX(point.getCenterX());
path.setEndY(point.getCenterY());
});
Group shapes = new Group();
shapes.getChildren().addAll(path, point);
BorderPane root = new BorderPane(shapes);
Scene scene = new Scene(root,600,400);
scene.getStylesheets().add(getClass().getResource("application.css").toExternalForm());
primaryStage.setScene(scene);
primaryStage.show();
the result:
The ideal is that the pivot point will be at the start of the line, and not at the middle.
What you're seeing is not the line growing or shrinking in both directions; instead, what's happening is, as the line changes length, the BorderPane repositions it in order to keep it centered. The same thing is happening with the "rotation". When you move an end in such a way as to change the angle of the line, the bounding box changes in a way that, when the BorderPane repositions the line, causes the other end to move in the opposite direction.
More specifically, the BorderPane is repositioning the Group—effectively the same thing since Group doesn't position its children. One fix for this is to make the Group unmanaged. This will stop the BorderPane from repositioning the Group as its bounds change. Note, however, that this will also stop the Group from contributing to the size and layout calculations of the BorderPane.
Here's an example:
import javafx.application.Application;
import javafx.geometry.Point2D;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Line;
import javafx.stage.Stage;
public class Main extends Application {
private static void installDragHandlers(Circle circle) {
circle.setOnMousePressed(event -> {
Point2D offset = new Point2D(
event.getX() - circle.getCenterX(),
event.getY() - circle.getCenterY()
);
circle.setUserData(offset);
event.consume();
});
circle.setOnMouseDragged(event -> {
Point2D offset = (Point2D) circle.getUserData();
circle.setCenterX(event.getX() - offset.getX());
circle.setCenterY(event.getY() - offset.getY());
event.consume();
});
circle.setOnMouseReleased(event -> {
circle.setUserData(null);
event.consume();
});
}
#Override
public void start(Stage primaryStage) {
Line line = new Line(200, 200, 400, 200);
line.setStrokeWidth(2);
Circle start = new Circle(5, Color.GREEN);
start.centerXProperty().bindBidirectional(line.startXProperty());
start.centerYProperty().bindBidirectional(line.startYProperty());
installDragHandlers(start);
Circle end = new Circle(5, Color.RED);
end.centerXProperty().bindBidirectional(line.endXProperty());
end.centerYProperty().bindBidirectional(line.endYProperty());
installDragHandlers(end);
Group group = new Group(line, start, end);
group.setManaged(false);
primaryStage.setScene(new Scene(new BorderPane(group), 600, 400));
primaryStage.setTitle("SO-55196882");
primaryStage.show();
}
}
The Line is initialized with hard coded start and end points so that it is initially centered in the scene (whose initial dimensions are also hard coded).
Put shapes in group.
public void start(final Stage primaryStage) throws AWTException {
final Line path = new Line(30, 30, 70, 75);
path.setStrokeWidth(5);
final Circle point = new Circle(3);
point.setCenterX(path.getEndX());
point.setCenterY(path.getEndY());
point.setFill(Paint.valueOf("red"));
point.setOnMouseDragged(e -> {
point.setCenterX(e.getX());
point.setCenterY(e.getY());
path.setEndX(point.getCenterX());
path.setEndY(point.getCenterY());
});
final Group root = new Group(path, point);
primaryStage.setScene(new Scene(root));
primaryStage.show();
}

After translating node The layout x and y stays the same

I currently have some stack panes on a pane and when dragged with a mouse they move around the pane. I do this by getting the coordinate of the mouse
and the translate x and y of the stack pane when I press the the stack pane. Then when I start to drag the stack pane I set the the translation x and y of the stack pane to the mouse coordinates when I pressed the stack pane + the difference of the new mouse coordinates and the old mouse coordinates.
My problem is after dragging the StackPane the layout x and y stays the same I want to update this as I used this else where.
My event handler when you press the StackPane:
EventHandler<MouseEvent> circleOnMousePressedEventHandler =
new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent t) {
currentStackPane = ((StackPane)(t.getSource()));
orgSceneX = t.getSceneX();
orgSceneY = t.getSceneY();
layoutX = currentStackPane.getLayoutX();
layoutY = currentStackPane.getLayoutY();
}
};
My event handler when i drag the StackPane:
EventHandler<MouseEvent> circleOnMouseDraggedEventHandler =
new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent t) {
double offsetX = t.getSceneX() - orgSceneX;
double offsetY = t.getSceneY() - orgSceneY;
currentStackPane.setTranslateX(offsetX);
currentStackPane.setTranslateY(offsetY);
}
};
I tried make a event handler after the drag is finished:
EventHandler<MouseEvent> circleOnMouseReleasedEventHandler =
new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent t) {
currentStackPane.setLayoutX(layoutX + ((StackPane)(t.getSource())).getTranslateX());
currentStackPane.setLayoutY(layoutY + ((StackPane)(t.getSource())).getTranslateY());
currentStackPane.setTranslateX(0);
currentStackPane.setTranslateY(0);
}
};
But this doesn't seem to work. Any help would be appreciated thanks!
EDIT:
I have changed my event handlers. It seems to be updating the layout x and y correctly for the first time I drag the stack pane but when i first drag the stack pane and then release the mouse the stackpane moves to a different position then every time i drag after it messes up completely. Not sure why, any help appreciated!
EDIT2: I realised I set translate x to 0 but didnt set translate y to 0 in the mouse released event. It all works now!
To understand the problem, I would first recommend to have a look at the documentation of layoutX/layoutY properties of a Node.
public final DoubleProperty layoutXProperty
Defines the x coordinate of the translation that is added to this
Node's transform for the purpose of layout. The value should be
computed as the offset required to adjust the position of the node
from its current layoutBounds minX position (which might not be 0) to
the desired location.
For example, if textnode should be positioned at finalX
textnode.setLayoutX(finalX - textnode.getLayoutBounds().getMinX());
Failure to subtract layoutBounds minX may result in misplacement of
the node. The relocate(x, y) method will automatically do the correct
computation and should generally be used over setting layoutX
directly.
The node's final translation will be computed as layoutX + translateX,
where layoutX establishes the node's stable position and translateX
optionally makes dynamic adjustments to that position.
If the node is managed and has a Region as its parent, then the layout
region will set layoutX according to its own layout policy. If the
node is unmanaged or parented by a Group, then the application may set
layoutX directly to position it.
In short,for every node that is rendered in the scene, its position is actually a sum of its layoutX/Y and translateX/Y values in relative to its parent node. The layoutX/Y are initially updated as per its parents layout policy. For that reason, there is no point in updating/relying on layoutX/Y values of node, IF its parent (eg,.StackPane,HBox,VBox,..etc) manages it position.
Pane will not manage/decide its children layout. For that reason the default layoutX/Y values of its children is always 0.
From the above info,if we now look into your code, you are updating the translate values and setting the layout values wrongly. Instead what you have to actually do is:
Take intial values of layoutX/Y.
Update the translateX/Y while dragging.
And on mouse released recompute layoutX/Y values and reset
translateX/Y values.
Below is a quick demo of what I have described.
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class PaneLayoutDemo extends Application {
double sceneX, sceneY, layoutX, layoutY;
#Override
public void start(Stage stage) throws Exception {
Pane root = new Pane();
Scene sc = new Scene(root, 600, 600);
stage.setScene(sc);
stage.show();
root.getChildren().addAll(getBox("green"), getBox("red"), getBox("yellow"));
}
private StackPane getBox(String color) {
StackPane box = new StackPane();
box.getChildren().add(new Label("Drag me !!"));
box.setStyle("-fx-background-color:" + color + ";-fx-border-width:2px;-fx-border-color:black;");
box.setPrefSize(150, 150);
box.setMaxSize(150, 150);
box.setMinSize(150, 150);
box.setOnMousePressed(e -> {
sceneX = e.getSceneX();
sceneY = e.getSceneY();
layoutX = box.getLayoutX();
layoutY = box.getLayoutY();
System.out.println(color.toUpperCase() + " Box onStart :: layoutX ::" + layoutX + ", layoutY::" + layoutY);
});
box.setOnMouseDragged(e -> {
double offsetX = e.getSceneX() - sceneX;
double offsetY = e.getSceneY() - sceneY;
box.setTranslateX(offsetX);
box.setTranslateY(offsetY);
});
box.setOnMouseReleased(e -> {
// Updating the new layout positions
box.setLayoutX(layoutX + box.getTranslateX());
box.setLayoutY(layoutY + box.getTranslateY());
// Resetting the translate positions
box.setTranslateX(0);
box.setTranslateY(0);
});
return box;
}
public static void main(String[] args) {
Application.launch(args);
}
}
Once you are familiar with the demo, try changing the root from Pane to StackPane and see the behaviour difference.

Prevent dragged circle from overlapping

As mentioned in the title, i have two Circle 's the first is draggable and the second is fixed, I would rotate (with the drag) the first one around the second without overlapping them but my Circle reacts oddly, I'm sure the error comes from the drag condition but I don't know how to solve it, that's why I need your help, here is a minimal and testable code :
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;
public class Collision extends Application{
private Pane root = new Pane();
private Scene scene;
private Circle CA = new Circle(20);
private Circle CB = new Circle(20);
private double xOffset = 0;
private double yOffset = 0;
#Override
public void start(Stage stage) throws Exception{
initCircles();
scene = new Scene(root,500,500);
stage.setScene(scene);
stage.show();
}
private void initCircles(){
CA.setCenterX(100);
CA.setCenterY(100);
CA.setFill(Color.rgb(255, 0, 0,0.2));
CA.setStroke(Color.BLACK);
CB.setCenterX(250);
CB.setCenterY(200);
CB.setFill(Color.rgb(255, 0, 0,0.2));
CB.setStroke(Color.BLACK);
CA.setOnMousePressed(evt->{
xOffset = CA.getCenterX() - evt.getSceneX();
yOffset = CA.getCenterY() - evt.getSceneY();
});
CA.setOnMouseDragged(evt->{
//get Scene coordinate from MouseEvent
drag(evt.getSceneX(),evt.getSceneY());
});
root.getChildren().addAll(CA,CB);
}
private void drag(double x, double y){
/* calculate the distance between
* the center of the first and the second circle
*/
double distance = Math.sqrt (Math.pow(CA.getCenterX() - CB.getCenterX(),2) + Math.pow(CA.getCenterY() - CB.getCenterY(),2));
if (!(distance < (CA.getRadius() + CB.getRadius()))){
CA.setCenterX(x + xOffset);
CA.setCenterY(y + yOffset);
}else{
/**************THE PROBLEM :Condition to drag************/
CA.setCenterX(CA.getCenterX() - (CB.getCenterX()-CA.getCenterX()));
CA.setCenterY(CA.getCenterY() - (CB.getCenterY()-CA.getCenterY()));
/*What condition must be established for the
* circle to behave correctly
*/
/********************************************************/
}
}
public static void main(String[] args) {
launch(args);
}
}
Here is a brief overview :
Note:
for my defense, i searched and found several subject close to mine but which have no precise or exact solution, among which:
-The circle remains blocked at the time of the collision
-Two circle that push each other
-JavaScript, Difficult to understand and convert to java
Thank you for your help !
Point2D can be interpreted as a 2D vector, and has useful methods for creating new vectors from it, etc. You can do:
private void drag(double x, double y){
// place drag wants to move circle to:
Point2D newCenter = new Point2D(x + xOffset, y+yOffset);
// center of fixed circle:
Point2D fixedCenter = new Point2D(CB.getCenterX(), CB.getCenterY());
// minimum distance between circles:
double minDistance = CA.getRadius() + CB.getRadius() ;
// if they overlap, adjust newCenter:
if (newCenter.distance(fixedCenter) < minDistance) {
// vector between fixedCenter and newCenter:
Point2D newDelta = newCenter.subtract(fixedCenter);
// adjust so that length of delta is distance between two centers:
Point2D adjustedDelta = newDelta.normalize().multiply(minDistance);
// move newCenter to match adjusted delta:
newCenter = fixedCenter.add(adjustedDelta);
}
CA.setCenterX(newCenter.getX());
CA.setCenterY(newCenter.getY());
}
Obviously, you could do all this without using Point2D and just doing the computation, but I think the API calls make the code easier to understand.

What is the difference between a Pane and a Group?

In JavaFX, what is the difference between a Pane and a Group? I can't make out any difference.
A Group is not resizable (meaning that its size is not managed by its parent in the scene graph), and takes on the union of the bounds of its child nodes. (So, in other words, the local bounds of a Group will be the smallest rectangle containing the bounds of all the child nodes). If it is larger than the space it is allocated in its parent, it will be clipped.
By contrast, a Pane is resizable, so its size is set by its parent, which essentially determine its bounds.
Here is a quick demo. The Group is on top and the Pane below. Both contain a fixed blue square at (100,100) and a green square which is moved by pressing the left/right arrow keys. Note how at the beginning, the blue square appears in the top left corner of the group, because the local bounds of the group start at the top-leftmost point of all its child nodes (i.e. the local bounds of the group extend from (100, 100) right and down). As you move the green rectangles "off screen", the group adjusts its bounds to incorporate the changes, wherever possible, whereas the pane remains fixed.
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
public class GroupVsPaneDemo extends Application {
#Override
public void start(Stage primaryStage) {
Pane pane = new Pane();
Group group = new Group();
VBox.setVgrow(group, Priority.NEVER);
VBox.setVgrow(pane, Priority.NEVER);
VBox vbox = new VBox(group, pane);
Rectangle rect1 = new Rectangle(100, 100, 100, 100);
Rectangle rect2 = new Rectangle(100, 100, 100, 100);
Rectangle rect3 = new Rectangle(200, 200, 100, 100);
Rectangle rect4 = new Rectangle(200, 200, 100, 100);
rect1.setFill(Color.BLUE);
rect2.setFill(Color.BLUE);
rect3.setFill(Color.GREEN);
rect4.setFill(Color.GREEN);
group.getChildren().addAll(rect1, rect3);
pane.getChildren().addAll(rect2, rect4);
Scene scene = new Scene(vbox, 800, 800);
scene.addEventHandler(KeyEvent.KEY_PRESSED, e -> {
double deltaX ;
switch(e.getCode()) {
case LEFT:
deltaX = -10 ;
break ;
case RIGHT:
deltaX = 10 ;
break ;
default:
deltaX = 0 ;
}
rect3.setX(rect3.getX() + deltaX);
rect4.setX(rect4.getX() + deltaX);
});
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
The few important difference between Pane and Group is that :
Pane can have its own size, where as a Group will take on the collective bounds of its children and is not directly resizable.
Pane can be used when you want to position its nodes at absolute position.
Also, note that Group was designed to be very lightweight and doesn't support a lot of styles. For example, you can't set border or background color for the group.
See this answer for more details.

JavaFX - using FillTransition and TranslateTransition misplaces the object

So I have a couple of Rectangle objects of different sizes, named r1 and r2, that initially appear in the dead center of the screen. They have different colors (say one is red and the other blue) and the smaller one is placed on top of the larger one, so even when they are stacked in the center, the two are still distinguishable. I wanted them to move around and return to their original positions, and thus used TranslateTransition as follows:
tt1 = new TranslateTransition(Duration.millis(20000), button1);
tt1.setByX(100); // moves 100 pixels to the right
tt1.setByY(-100); // moves 100 pixels down
tt1.setCycleCount(20); // oscillates 20 times
tt1.setAutoReverse(true);
tt2 = new TranslateTransition(Duration.millis(20000), button2);
tt2.setByX(-100); // moves 100 pixels to the left
tt2.setByY(100); // moves 100 pixels up
tt2.setCycleCount(20); // oscillates 20 times
tt2.setAutoReverse(true);
During their movement, however, if any of r1 and r2 is pressed by a MouseEvent, I would like them to disappear for a few seconds (say 5 seconds) and come back alive again. Using the fact the background color is completely black, I used FillTransition to achieve that effect:
FillTransition ft1 = new FillTransition(Duration.millis(5000), r1, Color.BLACK, Color.BLACK);
FillTransition ft2 = new FillTransition(Duration.millis(5000), r2, Color.BLACK, Color.BLACK);
By converting the Rectangles from Color.BLACK to Color.BLACK for 5 seconds, it gives an effect that the buttons have disappeared for 5 seconds. Also, I have the following setOnMouseClicked on r1 and r2 so they can disappear when a user input is made:
r1.setOnMouseClicked((MouseEvent t) -> {
ft1.play();
});
r2.setOnMouseClicked((MouseEvent t) -> {
ft2.play();
});
After the two objects have disappeared for 5 seconds, they must reappear in the center, as they did in the beginning, and repeat the same oscillating motion using tt1 and tt2, which I achieved with setOnFinished on ft1 and ft2:
ft1.setOnFinished((ActionEvent event) -> {
r1.setFill(color1); // restore the original color
tt1.play();
});
ft2.setOnFinished((ActionEvent event) -> {
r2.setFill(color2); // restore the original color
tt2.play();
});
The problem is, however, when r1 and r2 reappear, they are positioned not at the center, but rather at the location from which they last disappeared - in other words, the location of their rebirth is where they were at during the last TranslateTransition when a user's MouseEvent is detected. I have tried to modify this by using r1.setX(centerX) and r1.setY(centerY), where centerX and centerY are the original center coordinates used in the beginning, but it could not fix the problem. In fact, when I used r1.getX(), the returned value equaled the original centerX value even when it was conspicuous that r1 was not placed in the center. This gave me a suspicion that TranslateTransition performs its duty without altering the actual getX() values. I have also thought of using ParallelTransition somehow on TranslateTransition and FillTransition, so tt1 could finish while ft1 takes effect, but since then ft1 would start running when tt1 has already been running for some time, it would not provide a feasible solution.
So my question is, if an object's TranslateTransition is interrupted in the middle, how do I restore the object's "original" coordinate, not where the object was last left off when TranslateTransition was interrupted?
p.s. I want to avoid creating new Rectangle objects every time a MouseEvent is detected, because that means all TranslateTransition and FillTransition linked to r1 and r2 must be recreated as well.
Sample Solution
Run the program, the rectangle will start moving. Click on the rectangle and it will disappear momentarily. Shortly after it has disappeared, the rectangle will re-appear at its original start location and start moving along its original trajectory.
import javafx.animation.*;
import javafx.application.Application;
import javafx.scene.*;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import javafx.util.Duration;
public class Pauser extends Application {
#Override
public void start(Stage stage) throws Exception {
final Rectangle r1 = new Rectangle(
50, 150, 30, 30
);
final TranslateTransition tt1 = new TranslateTransition(
Duration.seconds(5),
r1
);
tt1.setFromX(0); // start at the layout origin for the node
tt1.setFromY(0); //
tt1.setByX(100); // moves 100 pixels to the right
tt1.setByY(-100); // moves 100 pixels down
tt1.setCycleCount(TranslateTransition.INDEFINITE);
tt1.setAutoReverse(true);
tt1.play();
final PauseTransition pt1 = new PauseTransition(
Duration.seconds(1)
);
pt1.setOnFinished(event -> {
tt1.playFromStart();
r1.setVisible(true);
});
r1.setOnMouseClicked(event -> {
r1.setVisible(false);
tt1.stop();
r1.setTranslateX(0);
r1.setTranslateY(0);
pt1.play();
});
stage.setScene(
new Scene(
new Group(r1),
200, 200
)
);
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
This just solves your problem for one rectangle. You can stick the solution in a loop to handle multiple rectangles.
Observations
A PauseTransition is used rather than a FillTransition and the visibility of the node is set to false while the pause is running. With a FillTransition the user can still click on the node, even though a node filled with the background color cannot be visibly distinguished from the background. So a FillTransition is probably undesirable.
fromX/fromY properties are set for the translate transition otherwise if you stop and run it from the start it will just use the current translateX/translateY values of the node rather than the origin values of 0/0 which is what you want.
While the node is not visible, there is no need to keep running the TranslateTransition, so the transition is stopped for that duration.
When the translate transition is stopped, it leaves the translateX/translateY co-ordinates at wherever they are currently set for the last animation frame. So a manual call to set translateX/translateY to 0/0 is added.
After the pause transition is complete, a request is made to play the translate transition from the start rather than wherever it was previously paused or stopped.
Understanding Transformations
A node's position on the screen is based on transformations applied to its layout position. The layout position is maintained in the node's layoutX/layoutY properties. But if you apply a translate transformation to a node (as you are implicitly doing in a TranslateTransition), then the node's screen position will be layoutX+translateX / layoutY+translateY.
Background Study
Read the Node documentation, in particular the sections on transformations and bounding rectangles.
Try out this layout bounds demonstration to help understand the concepts.
So the key was using setFromX and setFromY to start from the original coordinate.
It was also key to set manually set the translateX/translateY values to 0/0. If this was not done, the node would flash at its last translated position before starting to move from the origin position. I think this is because there is a frame delay from when you request the animation to start and when it actually starts and uses the fromX/fromY co-ordinates of 0/0. Which is kind of a strange behavior.
This works fine for me. Hopefully it good approach. There could be new class to create extends rectangle with events and transitions in it. This is just sample for two rectangles.
import javafx.animation.FadeTransition;
import javafx.animation.TranslateTransition;
import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import javafx.util.Duration;
public class Main extends Application {
#Override
public void start(Stage primaryStage) throws Exception {
Rectangle rect1 = new Rectangle (100, 40, 120, 120);
rect1.setArcHeight(42);
rect1.setArcWidth(42);
rect1.setFill(Color.AQUA);
TranslateTransition tt1;
tt1 = new TranslateTransition(Duration.millis(3000), rect1);
tt1.setByX(200);
tt1.setByY(-200);
tt1.setCycleCount(2);
tt1.setAutoReverse(true);
tt1.play();
rect1.setOnMousePressed(new EventHandler<MouseEvent>() {
#Override public void handle(MouseEvent event) {
FadeTransition ft = new FadeTransition(Duration.millis(1000), rect1);
ft.setFromValue(1.0);
ft.setToValue(0.0);
ft.play();
event.consume();
}
});
tt1.setOnFinished(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
tt1.play();
FadeTransition ft = new FadeTransition(Duration.millis(1000), rect1);
ft.setToValue(1.0);
ft.play();
}
});
Rectangle rect2 = new Rectangle (100, 40, 80, 80);
rect2.setArcHeight(42);
rect2.setArcWidth(42);
rect2.setFill(Color.YELLOWGREEN);
TranslateTransition tt2;
tt2= new TranslateTransition(Duration.millis(6000), rect2);
tt2.setByX(-200);
tt2.setByY(200);
tt2.setCycleCount(2);
tt2.setAutoReverse(true);
tt2.play();
rect2.setOnMousePressed(new EventHandler<MouseEvent>() {
#Override public void handle(MouseEvent event) {
FadeTransition ft = new FadeTransition(Duration.millis(1000), rect2);
ft.setFromValue(1.0);
ft.setToValue(0.0);
ft.play();
event.consume();
}
});
tt2.setOnFinished(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
tt2.play();
FadeTransition ft = new FadeTransition(Duration.millis(1000), rect2);
ft.setToValue(1.0);
ft.play();
}
});
StackPane pane = new StackPane();
pane.setAlignment(Pos.CENTER);
pane.getChildren().addAll(rect1,rect2);
Scene scene = new Scene(pane, 600, 600);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args){
launch(args);
}
}

Resources