I am working on an app which needs to move nodes around on a pane.
I've just recently started learning javaFx 8 so I have bare understanding of the workflow, but while I managed to get a working draggable node code working, it uses node.setTranslateX() and .setTranslateY().
This is all fine, until I tried to make the node snap to a grid that divides the scene area in both 10 height and width subdivisions.
Whenever I try to use modulos to get some sort of snapping going on, I'm stuck with the fact that I'm calling Translation transformation. I'd like to directly set the coordinates of the node myself. I've tried node.relocate(x,y) to no avail. As for node.setX() or node.setY(). These do no seem to do much.
So basically, I have two questions:
Is there a way to directly set the coordinates of a node ? If yes, how ?
What is the correct way to snap a node to a grid while dragging it with the mouse ?
My current code uses these methods to drag the node around:
public class Boat extends ImageView {
private int size ;
private String name ;
private Double dragDeltaX, dragDeltaY;
//Constructor etc here//
this.setOnMousePressed(event -> {
this.setCursor(Cursor.CLOSED_HAND);
dragDeltaX = this.getLayoutX() + event.getX();
dragDeltaY = this.getLayoutY() + event.getY();
});
this.setOnMouseReleased(event -> {
this.setCursor(Cursor.OPEN_HAND);
this.relocate(event.getSceneX(), event.getSceneY());
});
this.setOnMouseDragged(event -> {
this.setTranslateX(event.getSceneX() - dragDeltaX);
this.setTranslateY(event.getSceneY() - dragDeltaY);
});
this.setOnMouseEntered(event -> {
this.setCursor(Cursor.OPEN_HAND);
});
}
Thanks in advance for the help,
In most Pane subclasses, if a Node is managed, then the parent pane of the node will manage its layoutX and layoutY properties. So setting layoutX and layoutY for these nodes will have no visible effect on the position of the node.
Transformations (including the translation defined by translateX and translateY) are applied to nodes after the layout calculations (whether the node is managed by its parent or not).
So one way to manage dragging is just to manipulate the translateX and translateY properties. Another way is to manipulate the layoutX and layoutY properties (by setting them directly or by calling relocate), and make sure the node is not managed by its parent. I would not recommend mixing the two techniques, as your code will be harder to follow.
You can make the node unmanaged setManaged(false) on the node, or by putting it in a Pane instead of one of the subclasses (Pane does not manage layout of its child nodes).
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.image.WritableImage;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
public class ImageViewDragExample extends Application {
#Override
public void start(Stage primaryStage) {
Image image = createImage();
DraggableImageView imageView = new DraggableImageView(image);
// if the node is placed in a parent that manages its child nodes,
// you must call setManaged(false);
// imageView.setManaged(false);
// StackPane root = new StackPane(imageView);
// or use a plain `Pane`, which does not manage its child nodes:
Pane root = new Pane(imageView);
Scene scene = new Scene(root, 400, 400);
primaryStage.setScene(scene);
primaryStage.show();
}
private Image createImage() {
WritableImage image = new WritableImage(50, 50);
for (int y = 0 ; y < 50; y++) {
for (int x = 0 ; x < 50 ; x++) {
Color c ;
if ((x > 20 && x < 30) || (y > 20 && y < 30)) {
c = Color.AZURE ;
} else {
c = Color.CORNFLOWERBLUE ;
}
image.getPixelWriter().setColor(x, y, c);
}
}
return image ;
}
public static class DraggableImageView extends ImageView {
private double mouseX ;
private double mouseY ;
public DraggableImageView(Image image) {
super(image);
setOnMousePressed(event -> {
mouseX = event.getSceneX() ;
mouseY = event.getSceneY() ;
});
setOnMouseDragged(event -> {
double deltaX = event.getSceneX() - mouseX ;
double deltaY = event.getSceneY() - mouseY ;
relocate(getLayoutX() + deltaX, getLayoutY() + deltaY);
mouseX = event.getSceneX() ;
mouseY = event.getSceneY() ;
});
}
}
public static void main(String[] args) {
launch(args);
}
}
Related
I have this JavaFX Circle, which moves according to keyboard's arrows.
All the AnimationTimer does is refreshing the circle position every frame.
I found a movement of 0.1 every time a KeyEvent is triggered to be smooth enough for the animation, however it moves really slow. On the other hand if I change the movement to let's say 1.0 or 10.0, it's undoubtedly faster, but also much more choppy (you can clearly see it starts moving by discrete values).
I want to be able to keep the smoothness of translating at most 0.1 per frame, but also be able to change how much space it should move every time a key is triggered.
Below is an mre describing the problem:
public class MainFX extends Application {
private double playerX;
private double playerY;
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) {
AnchorPane pane = new AnchorPane();
Scene scene = new Scene(pane, 800, 600);
primaryStage.setScene(scene);
primaryStage.show();
playerX = pane.getWidth()/2;
playerY = pane.getHeight()/2;
Circle player = new Circle(playerX,playerY,10);
pane.getChildren().add(player);
scene.addEventHandler(KeyEvent.KEY_PRESSED, this::animate);
AnimationTimer timer = new AnimationTimer() {
#Override
public void handle(long now) {
player.setCenterX(playerX);
player.setCenterY(playerY);
}
};
timer.start();
}
private void animate(KeyEvent key){
if (key.getCode() == KeyCode.UP) {
playerY-=0.1;
}
if (key.getCode() == KeyCode.DOWN) {
playerY+=0.1;
}
if (key.getCode() == KeyCode.RIGHT) {
playerX+=0.1;
}
if (key.getCode() == KeyCode.LEFT) {
playerX-=0.1;
}
}
}
The AnimationTimer's handle() method is invoked on every frame rendering. Assuming the FX Application thread is not overwhelmed with other work, this will occur at approximately 60 frames per second. Updating the view from this method will give a relatively smooth animation.
By contrast, the key event handlers are invoked on every key press (or release, etc.) event. Typically, when a key is held down, the native system will issue repeated key press events at some rate that is system dependent (and usually user-configurable), and typically is much slower that animation frames (usually every half second or so). Changing the position of UI elements from here will result in jerky motion.
Your current code updates the position of the UI element from the playerX and playerY variables in the AnimationTimer: however you only change those variable is the key event handlers. So if the AnimationTimer is running at 60fps, and the key events are occurring every 0.5s (for example), you will "update" the UI elements 30 times with each new value, changing the actual position only two times per second.
A better approach is to use key event handlers merely to maintain the state of variables indicating if each key is pressed or not. In the AnimationTimer, update the UI depending on the state of the keys, and the amount of time elapsed since the last update.
Here is a version of your code using this approach:
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.geometry.Point2D;
import javafx.scene.Scene;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.AnchorPane;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;
public class MainFX extends Application {
private boolean leftPressed ;
private boolean rightPressed ;
private boolean upPressed ;
private boolean downPressed ;
private static final double SPEED = 100 ; // pixels/second
private static final double PLAYER_RADIUS = 10 ;
private AnchorPane pane;
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) {
pane = new AnchorPane();
Scene scene = new Scene(pane, 800, 600);
primaryStage.setScene(scene);
primaryStage.show();
double playerX = pane.getWidth() / 2;
double playerY = pane.getHeight() / 2;
Circle player = new Circle(playerX, playerY, PLAYER_RADIUS);
pane.getChildren().add(player);
scene.addEventHandler(KeyEvent.KEY_PRESSED, this::press);
scene.addEventHandler(KeyEvent.KEY_RELEASED, this::release);
AnimationTimer timer = new AnimationTimer() {
private long lastUpdate = System.nanoTime() ;
#Override
public void handle(long now) {
double elapsedSeconds = (now - lastUpdate) / 1_000_000_000.0 ;
int deltaX = 0 ;
int deltaY = 0 ;
if (leftPressed) deltaX -= 1 ;
if (rightPressed) deltaX += 1 ;
if (upPressed) deltaY -= 1 ;
if (downPressed) deltaY += 1 ;
Point2D translationVector = new Point2D(deltaX, deltaY)
.normalize()
.multiply(SPEED * elapsedSeconds);
player.setCenterX(clampX(player.getCenterX() + translationVector.getX()));
player.setCenterY(clampY(player.getCenterY() + translationVector.getY()));
lastUpdate = now ;
}
};
timer.start();
}
private double clampX(double value) {
return clamp(value, PLAYER_RADIUS, pane.getWidth() - PLAYER_RADIUS);
}
private double clampY(double value) {
return clamp(value, PLAYER_RADIUS, pane.getHeight() - PLAYER_RADIUS);
}
private double clamp(double value, double min, double max) {
return Math.max(min, Math.min(max, value));
}
private void press(KeyEvent event) {
handle(event.getCode(), true);
}
private void release(KeyEvent event) {
handle(event.getCode(), false);
}
private void handle(KeyCode key, boolean press) {
switch(key) {
case UP: upPressed = press ; break ;
case DOWN: downPressed = press ; break ;
case LEFT: leftPressed = press ; break ;
case RIGHT: rightPressed = press ; break ;
default: ;
}
}
}
You aren't using the animation timer properly.
Your key code processing should be used to set a velocity for the object. Set the velocity to 0 when the key is released.
In the animation timer code, change the position based on the elapsed time and the velocity.
The timer will fire events at the frame rate - likely around 60fps. If you want smooth motion you want to adjust the position on every frame. Instead you are using it to set the position to a pre-computed value. It isn't doing anything useful that way. You could just as easily set the position in the key processing code and get the same effect you are getting now.
If you don't want to have the user hold down the keys to move. That is, you want to tap once and have the object move by 10.0. You can set the target position in the key processing code. Jumping the target position by 10 at a time. Then have the animation timer move the current position towards the target position at an appropriate velocity, stopping when the target position is reached.
Maybe something like this:
AnimationTimer timer = new AnimationTimer() {
#Override
public void handle(long now) {
double curX = player.getCenterX();
double curY = player.getCenterY();
double diffX = playerX-curX;
double diffY = playerY-curY;
if (diffX > 1.0) {
curX += 1.0;
else if (diffX < -1.0) {
curX -= 1.0;
} else {
curX = playerX;
}
if (diffY > 1.0) {
curY += 1.0;
else if (diffY < -1.0) {
curY -= 1.0;
} else {
curY = playerY;
}
player.setCenterX(curX);
player.setCenterY(curY);
}
};
That's a primitive example... and note that it will make diagonal movements that go faster than axis-aligned movements. (The velocity vector magnitude for diagonal movements is sqrt(2) in that example instead of 1.)
Basically you want to update the position based on a velocity vector adjusted for the interval between ticks of the animation timer.
I have to detect when two "balls" collide in a javaFX program. Each time a button is clicked, a new ball is added to the pane. I know that getChildren() returns an observable list that contains the node for each ball, and when I print the list with two circles it will print, for example,
Circle[centerX=30.0, centerY=30.0, radius=20.0, fill=0x9ac26780], Circle[centerX=224.0, centerY=92.0, radius=20.0, fill=0x9ac26780]
My idea was to use nested loops to compare the (x,y) coordinates of each ball to every other ball. How do I access centerX and centerY from each Circle in order to compare them?
I tried getChildren().sublist(0,0); thinking that I would get the centerX value for the first element, but that does not work. I also tried getCenterX, because Ball extends Circle, but that also failed. Thanks for your time.
public class Exercise20_05 extends Application {
#Override // Override the start method in the Application class
public void start(Stage primaryStage) {
MultipleBallPane ballPane = new MultipleBallPane();
ballPane.setStyle("-fx-border-color: yellow");
Button btAdd = new Button("+");
Button btSubtract = new Button("-");
HBox hBox = new HBox(10);
hBox.getChildren().addAll(btAdd, btSubtract);
hBox.setAlignment(Pos.CENTER);
// Add or remove a ball
btAdd.setOnAction(e -> ballPane.add());
btSubtract.setOnAction(e -> ballPane.subtract());
// Pause and resume animation
ballPane.setOnMousePressed(e -> ballPane.pause());
ballPane.setOnMouseReleased(e -> ballPane.play());
// Use a scroll bar to control animation speed
ScrollBar sbSpeed = new ScrollBar();
sbSpeed.setMax(20);
sbSpeed.setValue(10);
ballPane.rateProperty().bind(sbSpeed.valueProperty());
BorderPane pane = new BorderPane();
pane.setCenter(ballPane);
pane.setTop(sbSpeed);
pane.setBottom(hBox);
// Create a scene and place the pane in the stage
Scene scene = new Scene(pane, 250, 150);
primaryStage.setTitle("MultipleBounceBall"); // Set the stage title
primaryStage.setScene(scene); // Place the scene in the stage
primaryStage.show(); // Display the stage
}
private class MultipleBallPane extends Pane {
private Timeline animation;
public MultipleBallPane() {
// Create an animation for moving the ball
animation = new Timeline(
new KeyFrame(Duration.millis(50), e -> moveBall()));
animation.setCycleCount(Timeline.INDEFINITE); //animation will play indefinitely
animation.play(); // Start animation
}
public void add() {
Color color = new Color(Math.random(),
Math.random(), Math.random(), 0.5);
//creates a new Ball at (30, 30) with a radius of 20
getChildren().add(new Ball(30, 30, 20, color));
ballCollision();
}
public void subtract() {
if (getChildren().size() > 0) {
getChildren().remove(getChildren().size() - 1);
}
}
public void play() {
animation.play();
}
public void pause() {
animation.pause();
}
public void increaseSpeed() {
animation.setRate(animation.getRate() + 0.1);
}
public void decreaseSpeed() {
animation.setRate(
animation.getRate() > 0 ? animation.getRate() - 0.1 : 0);
}
public DoubleProperty rateProperty() {
return animation.rateProperty();
}
protected void moveBall() {
for (Node node: this.getChildren()) {
Ball ball = (Ball)node;
// Check boundaries
if (ball.getCenterX() < ball.getRadius() ||
ball.getCenterX() > getWidth() - ball.getRadius()) {
ball.dx *= -1; // Change ball move direction
}
if (ball.getCenterY() < ball.getRadius() ||
ball.getCenterY() > getHeight() - ball.getRadius()) {
ball.dy *= -1; // Change ball move direction
}
// Adjust ball position
ball.setCenterX(ball.dx + ball.getCenterX());
ball.setCenterY(ball.dy + ball.getCenterY());
ballCollision();
}
}
//check for ball collisions
protected void ballCollision() {
/*System.out.println(getChildren().size());
getChildren returns an observableList; this observableList is what
keeps track of the balls (specifically, the nodes added to ballPane)
added each time the + button is clicked
*/
ObservableList ballList = getChildren();
System.out.println(ballList.get(0));
//if there are 2 or more balls, check for collision
if (ballList.size() > 1) {
//compare each (x,y) coordinate value to every other (x,y) value
for (int i = 0; i < ballList.size(); i++) {
for (int k = 0; k < ballList.size(); k++) {
// if (ballList.sublist(i,i) < 1) {
//
// }
}
}
}
}
}
class Ball extends Circle {
private double dx = 1, dy = 1;
Ball(double x, double y, double radius, Color color) {
super(x, y, radius);
setFill(color); // Set ball color
}
}
/**
* The main method is only needed for the IDE with limited
* JavaFX support. Not needed for running from the command line.
*/
public static void main(String[] args) {
launch(args);
}
}
Edit: Thanks to a couple of people, I was able to get the collision check to work. One ball will get removed, but I get the ConcurrentModificationException. Here is the updated method:
protected void ballCollision() {
ObservableList ballList = getChildren();
//if there are 2 or more balls, check for collision
if (ballList.size() > 1) {
//compare each (x,y) coordinate value to every other (x,y) value
for (int i = 0; i < ballList.size(); i++) {
for (int k = i + 1; k < ballList.size(); k++) {
Circle c1 = (Circle) ballList.get(i);
Circle c2 = (Circle) ballList.get(k);
if ((c1.getCenterX() <= c2.getCenterX() * 1.10 &&
(c1.getCenterX() >= c2.getCenterX()*.90)) &&
((c1.getCenterY() <= c2.getCenterY() * 1.10) &&
c1.getCenterY() >= c2.getCenterY() * .90)){
ballList.remove(c2);
}
}
}
}
}
Final Edit: Thanks to David Wallace for taking his time to help me. The issue was that I was calling ballCollision inside the for-each loop of the moveBall method. Once I moved it outside the loop, it worked perfectly.
You can treat the ObservableList just like any other List. You will probably want to cast the elements to the right class, as shown here. Use the hypot method of the Math class to calculate the distance between the centres.
for (int first = 0; first < ballList.size(); first++) {
Ball firstBall = (Ball) ballList.get(first);
for (int second = first + 1; second < ballList.size(); second++) {
Ball secondBall = (Ball) ballList.get(second);
double distanceBetweenCentres = Math.hypot(
firstBall.getCenterX() - secondBall.getCenterX(),
firstBall.getCenterY() - secondBall.getCenterY());
if (distanceBetweenCentres <= firstBall.getRadius() + secondBall.getRadius()) {
System.out.println("Collision between ball " + first + " and ball " + second);
}
}
}
I'm experimenting with JavaFX and animations, especially PathTransition. I'm creating a simple program that makes a ball "bounce," without using the QuadCurveTo class. Here is my code so far:
Ellipse ball = new Ellipse(375, 250, 10, 10);
root.getChildren().add(ball);
Path path = new Path();
path.getElements().add(new MoveTo(375, 500));
int posX = 375;
int posY = 500;
int changeX = 10;
int changeY = 50;
int gravity = 10; // approximate in m/s^2
int sec = 0;
for(; posY<=500; sec++, posX-=changeX, posY-=changeY, changeY-=gravity)
path.getElements().add(new LineTo(posX, posY));
// How do I equally space these elements?
PathTransition pathTransition = new PathTransition();
pathTransition.setDuration(Duration.millis(sec*1000));
pathTransition.setNode(ball);
pathTransition.setAutoReverse(true);
pathTransition.setCycleCount(Timeline.INDEFINITE);
pathTransition.setInterpolator(Interpolator.LINEAR);
pathTransition.setPath(path);
pathTransition.play();
I have the for loop running through a quadratic sequence, and the ball moves in the correct motion (a curved path).
However, I want it to move slower at the top of the curve (vertex) because it is moving less distance (as changeY, the vertical increment variable, is decreasing as the loop goes on) to simulate a more realistic curve. However, it is traveling in a linear speed throughout the full time.
Is there any way to make each of the elements equally spaced (throughout) time, so that this "bounce" would show correctly? Thanks.
I wouldn't use a timeline or transition at all for this. Use an AnimationTimer and compute the new coordinates based on the elapsed time since the last frame. The AnimationTimer has a handle method which is invoked once per rendering frame, taking a value that represents a timestamp in nanoseconds.
SSCCE (with elasticity added to the physics):
import javafx.animation.AnimationTimer;
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 BouncingBall extends Application {
#Override
public void start(Stage primaryStage) {
Circle ball = new Circle(20, 80, 10);
ball.setFill(Color.DARKBLUE);
Pane pane = new Pane(ball);
AnimationTimer timer = new AnimationTimer() {
long lastUpdate = 0 ;
double changeX = 0.1 ;
double changeY = 0 ;
double gravity = 10 ;
double elasticity = 0.95 ;
#Override
public void handle(long now) {
if (lastUpdate == 0) {
lastUpdate = now ;
return ;
}
long elapsedNanos = now - lastUpdate;
double elapsedSeconds = elapsedNanos / 1_000_000_000.0 ;
lastUpdate = now ;
ball.setCenterX(ball.getCenterX() + changeX);
if (ball.getCenterY() + changeY + ball.getRadius() >= pane.getHeight()) {
changeY = - changeY * elasticity;
} else {
changeY = changeY + gravity * elapsedSeconds ;
}
ball.setCenterY(Math.min(ball.getCenterY() + changeY, pane.getHeight() - ball.getRadius()));
}
};
primaryStage.setScene(new Scene(pane, 400, 400));
primaryStage.show();
timer.start();
}
public static void main(String[] args) {
launch(args);
}
}
I am trying to layout my nodes like this:
Here is my current layout, called CircularPane:
import javafx.geometry.HPos;
import javafx.geometry.VPos;
import javafx.scene.Node;
import javafx.scene.layout.Pane;
public class CircularPane extends Pane {
#Override
protected void layoutChildren() {
final int radius = 50;
final double increment = 360 / getChildren().size();
double degreese = 0;
for (Node node : getChildren()) {
double x = radius * Math.cos(Math.toRadians(degreese)) + getWidth() / 2;
double y = radius * Math.sin(Math.toRadians(degreese)) + getHeight() / 2;
layoutInArea(node, x - node.getBoundsInLocal().getWidth() / 2, y - node.getBoundsInLocal().getHeight() / 2, getWidth(), getHeight(), 0.0, HPos.LEFT, VPos.TOP);
degreese += increment;
}
}
}
Here is my main class:
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.stage.Stage;
public class Main extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage stage) {
CircularPane pane = new CircularPane();
for(int i = 0; i < 6; i++) {
Button button = new Button("" + i);
pane.getChildren().add(button);
}
stage.setScene(new Scene(pane));
stage.show();
}
}
And here is my current display:
The nodes are not at the bottom touching, they are equally spread out around the circle. I want to make it so they go to the bottom, but can't figure out how to.
Your approach to layout the buttons over a circle is correct, but in this line you are defining how they will be layouted:
final double increment = 360 / getChildren().size();
This gives the same angle between any two buttons refered from the center of the circle! And that's why you get your current display.
If you want to layout the nodes like in your figure, if I get it right, these are the conditions:
Every node has its center over the circle
The nodes are equally separated in horizontal: the horizontal gap goes from 0 to some value.
The initial gap from the circle to the first node goes from 0 to some value.
The size of each node may be adjusted to fulfill the previous conditions
So let's define some fields for those values, and adjust the size of the pane:
class CircularPane extends Pane {
private final double radius;
private final double ext_gap;
private final double int_gap;
public CircularPane(double radius, double ext_gap, double int_gap){
this.radius=radius;
this.ext_gap=ext_gap;
this.int_gap=int_gap;
setMinSize(2*radius, 2d*radius);
setPrefSize(2*radius, 2d*radius);
setMaxSize(2*radius, 2d*radius);
}
}
And now, given any n buttons, the above conditions can be turned into one single equation that solves the size of the node. If the total available length (2*radius) minus two exterior gaps (2*ext_gap) is the same as n buttons of size buttonSize and n-1 interior gaps (int_size), then, the size of every button has to be:
#Override
protected void layoutChildren() {
int n=getChildren().size();
double buttonSize = (2*radius-2*ext_gap-(n-1)*int_gap)/n;
}
Finally, now you can set the size of the button and layout every node, just by increasing the x coordinate (by the size of the button plus an inner gap), and then getting the y coordinate from the circle equation:
#Override
protected void layoutChildren() {
int n=getChildren().size();
double buttonSize = (2*radius-2*ext_gap-(n-1)*int_gap)/n;
double x=ext_gap+buttonSize/2d, y;
for (Node node : getChildren()) {
((Button)node).setMinSize(buttonSize, buttonSize);
((Button)node).setPrefSize(buttonSize, buttonSize);
((Button)node).setMaxSize(buttonSize, buttonSize);
node.setStyle("-fx-font-size: "+Math.round(buttonSize/3));
node.setManaged(false);
y=getHeight()/2d+Math.sqrt(radius*radius-Math.pow(x-radius,2d));
layoutInArea(node, x-buttonSize/2d, y-buttonSize/2d, getWidth(), getHeight(), 0.0, HPos.LEFT, VPos.TOP);
x+=buttonSize+int_gap;
}
}
Note that you can also change the size of the font, to get a visible number for any size of the button.
Note also that node.setManaged(false); avoids the calls to layoutChildren() when you click the buttons (due to changes in the size of the clicked button when being focused or clicked).
Finally this will create the circular pane and draw a circle:
#Override
public void start(Stage primaryStage) {
CircularPane pane = new CircularPane(200,20,10);
for(int i = 0; i < 6; i++) {
Button button = new Button("" + (i+1));
pane.getChildren().add(button);
}
Circle circle = new Circle(200);
circle.setFill(null);
circle.setStroke(Color.BLACK);
StackPane stack=new StackPane(circle,pane);
Scene scene = new Scene(stack, 500, 500);
primaryStage.setScene(scene);
primaryStage.show();
}
With this result:
So ive got this code where im trying to get the moving circles to bounce on the walls so they dont go outside the stage. Ive tried to do it with the moveCircle method but i feel really out of my comfort zone.
import javafx.animation.Animation.Status;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.geometry.Orientation;
import javafx.geometry.Rectangle2D;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollBar;
import javafx.scene.effect.Light;
import javafx.scene.effect.Lighting;
import javafx.scene.input.KeyCode;
import javafx.scene.layout.BorderPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.stage.Screen;
import javafx.stage.Stage;
import javafx.util.Duration;
public class TimelineSample extends Application {
Timeline timeline;
private void init(Stage primaryStage) {
double height = primaryStage.getHeight();
double width = primaryStage.getWidth();
BorderPane root = new BorderPane();
primaryStage.setScene(new Scene(root, width, height));
double radius = 30;
Circle circle = new Circle(radius, radius, radius, Color.BLUE);
Circle circle2 = new Circle(radius, radius, radius, Color.RED);
Light.Distant light = new Light.Distant();
light.setAzimuth(-135.0);
Label label = new Label(
"Space för starta spelet\nSpace för att pausa spelet\nTryck på cirklarna för att byta färg på dem");
Label label2 = new Label("44");
root.setStyle("-fx-background-color: green;");
label2.setStyle(("-fx-padding : 100;"));
root.setBottom(label2);
root.setCenter(label);
Screen screen = Screen.getPrimary();
Rectangle2D bounds = screen.getVisualBounds();
ScrollBar sbSpeed = new ScrollBar();
sbSpeed.setMax(50);
sbSpeed.setValue(25);
sbSpeed.setOrientation(Orientation.VERTICAL);
circle.opacityProperty().bind(sbSpeed.valueProperty().divide(30));
circle2.opacityProperty().bind(sbSpeed.valueProperty().divide(30));
sbSpeed.setOnScroll(e -> {
circle.setTranslateX(+50);
});
circle.centerXProperty().bind(root.widthProperty().divide(2));
circle.centerYProperty().bind(root.heightProperty().divide(2));
circle.radiusProperty().bind(Bindings.min(root.widthProperty().divide(10),
root.heightProperty().divide(10)));
circle2.centerXProperty().bind(root.widthProperty().divide(2));
circle2.centerYProperty().bind(root.heightProperty().divide(2));
circle2.radiusProperty().bind(Bindings.min(root.widthProperty().divide(10),
root.heightProperty().divide(10)));
root.setTop(sbSpeed);
primaryStage.setWidth(bounds.getWidth() * 0.40);
primaryStage.setHeight(bounds.getHeight() * 0.40);
Lighting lighting = new Lighting();
lighting.setLight(light);
lighting.setSurfaceScale(5.0);
circle.setEffect(lighting);
circle2.setEffect(lighting);
timeline = new Timeline();
timeline.setCycleCount(Timeline.INDEFINITE);
timeline.setAutoReverse(true);
timeline.getKeyFrames().addAll
(new KeyFrame(Duration.ZERO, new KeyValue(circle.translateXProperty(),
0)),
new KeyFrame(new Duration(5000), new KeyValue(circle
.translateXProperty(), width - (radius * 2))));
timeline.getKeyFrames().addAll(
new KeyFrame(Duration.ZERO, new KeyValue(
circle2.translateYProperty(), 0)),
new KeyFrame(new Duration(5000), new KeyValue(circle2
.translateYProperty(), height - (radius * 2))));
timeline.play();
root.getChildren().addAll(circle, circle2);
boolean a = true;
root.requestFocus();
root.setOnKeyPressed(e -> {
if (e.getCode().equals(KeyCode.SPACE)) {
if (timeline.statusProperty().getValue().equals(Status.RUNNING)) {
timeline.pause();
} else
timeline.play();
}
});
circle.setOnMousePressed(event -> {
if (circle.getFill().equals(Color.BLACK))
circle.setFill(Color.YELLOW);
else if (circle.getFill().equals(Color.BLUE))
circle.setFill(Color.BROWN);
else if (circle.getFill().equals(Color.YELLOW))
circle.setFill(Color.BROWN);
else if (circle.getFill().equals(Color.BROWN))
circle.setFill(Color.BLACK);
else
circle.setFill(Color.BLUE);
});
circle2.setOnMousePressed(event -> {
if (circle2.getFill().equals(Color.BLACK))
circle2.setFill(Color.YELLOW);
else if (circle2.getFill().equals(Color.BLUE))
circle2.setFill(Color.BROWN);
else if (circle2.getFill().equals(Color.YELLOW))
circle2.setFill(Color.BROWN);
else if (circle2.getFill().equals(Color.BROWN))
circle2.setFill(Color.BLACK);
else
circle2.setFill(Color.BLUE);
// }
});
}
protected void moveCircle(Circle circle) {
if (circle.getCenterX() < circle.getRadius() ||
circle.getCenterX() > circle.getCenterY() - circle.getRadius()) {
circle.translateYProperty();
}
if (circle.getCenterY() < circle.getRadius() ||
circle.getCenterY() > circle.getCenterX() - circle.getRadius()) {
circle.translateXProperty();}
}
public void pause() {
timeline.pause();
}
#Override
public void start(Stage primaryStage) throws Exception {
init(primaryStage);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
just need to start the balls and it will work.
Now this is general pseudo code for something like this, although not specific to your problem this is the general way of making this "bounce" off the edge of the screen.
First off you need 5 pieces of information, the circles velocity (on x and y, as variables or a vector2), the circles radius (or diameter), the circle position (on x and y, as variables or a vector2) and the screens width and height.
Also depending on if the origin point of the circle is the center (you will need radius), bottom left or top left (you will need diameter). In my example we assume the origin is bang on the middle of the circle.
The generally idea goes something like this:
int windowWidth = 800, windowHeight = 600;
Circle c;
// Check if the left or right side of the circle leaves the screen bounds
// if so, reverse the velocity (mirror it)
if(c.x - c.radius < 0 || c.x + c.radius > windowWidth)
c.velocityX = -x.velocityX;
// Check if the top or bottom side of the circle leaves the screen bounds
// if so, reverse the velocity (mirror it)
if(c.y - c.radius < 0 || c.y + c.radius > windowHeight)
c.velocityY = -c.velocityY;
Hope that makes sense, it is a case of checking if the circle is passed the screen boundaries and simply mirroring the velocity on that given axis. So if the ball is moving at a velocity of 5,0 (directly right) and then goes beyond the window width, we want to take the velocity on x and negate it, so now the velocity becomes -5, 0 (directly left).
The one issue with this is the linear look, you can easily add some other variables like acceleration, restitution, friction and drag to give it a more realistic feel.