I have an resizable ImageView and some parts of the image should be clickable.
My idea was to create a GridPane that would act as an overlay and position several transparent buttons inside it so that when a user clicks some part of the image, he would actually trigger a hidden button over the image:
<!-- parent that resizes from time to time -->
<StackPane>
<StackPane>
<!-- background image, preserves ration -->
<ImageView/>
<!-- overlay with transparent buttons -->
<!-- should be positionion exactly the same as Image inside ImageView -->
<GridPane>
<!- buttons here, columns == rows -->
</GridPane>
</StackPane>
</StackPane>
Code looks likes this:
StackPane stackPane_wrapper = new StackPane();
stackPane_wrapper.setAlignment(Pos.CENTER);
//menu wheel
WrappedImageView imageView_wheel = new WrappedImageView();
imageView_wheel.setImage(new Image("images/menu-wheel.png"));
imageView_wheel.setPreserveRatio(true);
stackPane_wrapper.getChildren().add(imageView_wheel);
GridPane gridPane_overlay = new GridPane();
stackPane_wrapper.getChildren().add(gridPane_overlay);
int size = 20;
for (int i = 0; i < size; ++i)
{
ColumnConstraints columnConstraints = new ColumnConstraints();
columnConstraints.setPercentWidth(100.0d / size);
gridPane_overlay.getColumnConstraints().add(columnConstraints);
RowConstraints rowConstraints = new RowConstraints();
rowConstraints.setPercentHeight(100.0d / size);
gridPane_overlay.getRowConstraints().add(rowConstraints);
}
Button button = new Button();
button.setText("test");
//... set style to make this button transparent
gridPane_overlay.add(button, 3, 5, 2, 4);
My problem is that I am unable to place gridPane_overlay on the same position as the Image inside ImageView. Image inside imageView_wheel keeps resizing and changing it's position, which is totally fine, but I do not know how assign it's size and position to gridPane_overlay.
I've tries adding listeners to various x/y and width/height properties and was able to achieve some results but it stopped working one the stage became maximized and kept setting completely invalid coordinates.
Update:
Seems like getParentInParent.getWidth() and getParentInParent.getHeight() return correct size of the Image inside WrappedImageView, now I need to get it's position and assign both size and position to the grid.
Update 2:
Based on the comments, I've made the following solution;
/**
*
* #param bounds bounds of image - result of {#link WrappedImageView#localToScene(Bounds)} from {#link WrappedImageView#getBoundsInLocal()}
* #param x click X - {#link MouseEvent#getSceneX()} minus {#link Bounds#getMinX()}
* #param y click Y - {#link MouseEvent#getSceneY()} minus {#link Bounds#getMinY()}
* #return
*/
private int determineClick(Bounds bounds, double x, double y)
{
double centerX = bounds.getWidth() / 2;
double centerY = bounds.getHeight() / 2;
double angle = Math.toDegrees(Math.atan2(x - centerX, y - centerY));
Point2D center = new Point2D(centerX, centerY);
Point2D click = new Point2D(x, y);
double distance = center.distance(click);
double diameter = centerX;
boolean isInner = distance < diameter * 0.6;
//-90 -> -135
if (angle <= -90 && angle > -135)
{
if (isInner)
{
return 10;
}
return 0;
}
//-135 -> -180/180
if (angle <= -135 && angle > -180)
{
if (isInner)
{
return 11;
}
return 1;
}
//-180/180 -> 135
if (angle <= 180 && angle > 135)
{
if (isInner)
{
return 12;
}
return 2;
}
//135 -> 90
if (angle <= 135 && angle > 90)
{
if (isInner)
{
return 13;
}
return 3;
}
//90 -> 45
if (angle <= 90 && angle > 45)
{
if (isInner)
{
return 14;
}
return 4;
}
//45 -> -0/0
if (angle <= 45 && angle > 0)
{
if (isInner)
{
return 15;
}
return 5;
}
//-0/0 -> -45
if (angle <= 0 && angle > -45)
{
if (isInner)
{
return 16;
}
return 6;
}
//-45 -> -90
if (angle <= -45 && angle > -90)
{
if (isInner)
{
return 17;
}
return 7;
}
throw new RuntimeException("Unable to determine point coordinates");
}
I am also thinking as exactly what #Jai mentioned in the comment. You can include a mouse clicked event on the imageview itself to determine at what point of its bounds is clicked. And then you can determine the range of click position and perform your logic.
Something like...
imageView_wheel.setOnMouseClicked(e -> {
Bounds bounds = imageView_wheel.localToScene(imageView_wheel.getBoundsInLocal());
double x = e.getSceneX()-bounds.getMinX();
double y = e.getSceneY()-bounds.getMinY();
System.out.println("Clicked at :: "+x+","+y);
// Now you know at what point in the image bounds is clicked, compute your logic as per your needs..
});
Related
Currently, I have a working implementation of an undirected graph where vertices are represented as StackPanes and edges are represented as Lines. The StackPanes are draggable and when dragged the lines move accordingly.
This is what it looks like so far.
However, I am really struggling with implementing directed graphs. Undirected graphs only really need 1 line, but with directed graphs, you will need 2 lines when there is an edge from A to B and an edge from B to A.
I want something like this when there are 2 edges between a vertex:
This is how I binded the undirected lines between 2 vertices (ie A Line between 2 StackPanes):
Line line = new Line();
line.setStroke(Color.BLACK);
line.setFill(null);
line.setStrokeWidth(2);
line.startXProperty().bind(vertexClickedOn.layoutXProperty().add(vertexClickedOn.translateXProperty()).add(vertexClickedOn.widthProperty().divide(2)));
line.startYProperty().bind(vertexClickedOn.layoutYProperty().add(vertexClickedOn.translateYProperty()).add(vertexClickedOn.heightProperty().divide(2)));
line.endXProperty().bind(vertexTo.layoutXProperty().add(vertexTo.translateXProperty()).add(vertexTo.widthProperty().divide(2)));
line.endYProperty().bind(vertexTo.layoutYProperty().add(vertexTo.translateYProperty()).add(vertexTo.heightProperty().divide(2)));
VertexClickedOn and VertexTo are both StackPanes, in graph terms, VertexClickedOn is where the edge starts and VertexTo is where the edge ends
I've been stuck on this for a while now any help will be greatly appreciated.
Thank you.
To start with, lets discuss your requirement in terms of vectors.
You have a line (joining the centers of two circles).
You want to place a node(arrow) at partical point on a line.
And this point is always located at a distance of (totalLineLength - circleRadius) for end arrow and a distance of circleRadius for start arrow.
Finally for directed lines, you want to translate this line up or down based on direction.
So once you have the line start and end points, using little Math you can get the point on a line at a certain distance. To keep the arrow direction correctly you can rotate the arrow based on the line slope.
As the code is a bit verbose because of calculations, please find below a working demo of what I mentioned above.
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.binding.DoubleBinding;
import javafx.beans.value.ChangeListener;
import javafx.event.EventHandler;
import javafx.geometry.Bounds;
import javafx.geometry.Insets;
import javafx.geometry.Point2D;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Line;
import javafx.scene.transform.Rotate;
import javafx.stage.Stage;
public class PaneLayoutDemo extends Application {
double sceneX, sceneY, layoutX, layoutY;
#Override
public void start(Stage stage) throws Exception {
StackPane root = new StackPane();
root.setPadding(new Insets(20));
Pane pane = new Pane();
root.getChildren().add(pane);
Scene sc = new Scene(root, 600, 600);
stage.setScene(sc);
stage.show();
StackPane dotA = getDot("green", "A");
StackPane dotB = getDot("red", "B");
StackPane dotC = getDot("yellow", "C");
StackPane dotD = getDot("pink", "D");
StackPane dotE = getDot("silver", "E");
buildSingleDirectionalLine(dotA, dotB, pane, true, true); // A <--> B
buildSingleDirectionalLine(dotB, dotC, pane, true, true); // B <--> C
buildSingleDirectionalLine(dotC, dotD, pane, true, false); // C --> D
// D <===> E
buildBiDirectionalLine(true, dotD, dotE, pane);
buildBiDirectionalLine(false, dotD, dotE, pane);
pane.getChildren().addAll(dotA, dotB, dotC, dotD, dotE);
}
/**
* Builds the single directional line with pointing arrows at each end.
* #param startDot Pane for considering start point
* #param endDot Pane for considering end point
* #param parent Parent container
* #param hasEndArrow Specifies whether to show arrow towards end
* #param hasStartArrow Specifies whether to show arrow towards start
*/
private void buildSingleDirectionalLine(StackPane startDot, StackPane endDot, Pane parent, boolean hasEndArrow, boolean hasStartArrow) {
Line line = getLine(startDot, endDot);
StackPane arrowAB = getArrow(true, line, startDot, endDot);
if (!hasEndArrow) {
arrowAB.setOpacity(0);
}
StackPane arrowBA = getArrow(false, line, startDot, endDot);
if (!hasStartArrow) {
arrowBA.setOpacity(0);
}
StackPane weightAB = getWeight(line);
parent.getChildren().addAll(line, weightAB, arrowBA, arrowAB);
}
/**
* Builds the bi directional line with pointing arrow at specified end.
* #param isEnd Specifies whether the line is towards end or not. If false then the line is towards start.
* #param startDot Pane for considering start point
* #param endDot Pane for considering end point
* #param parent Parent container
*/
private void buildBiDirectionalLine(boolean isEnd, StackPane startDot, StackPane endDot, Pane parent) {
Line virtualCenterLine = getLine(startDot, endDot);
virtualCenterLine.setOpacity(0);
StackPane centerLineArrowAB = getArrow(true, virtualCenterLine, startDot, endDot);
centerLineArrowAB.setOpacity(0);
StackPane centerLineArrowBA = getArrow(false, virtualCenterLine, startDot, endDot);
centerLineArrowBA.setOpacity(0);
Line directedLine = new Line();
directedLine.setStroke(Color.RED);
directedLine.setStrokeWidth(2);
double diff = isEnd ? -centerLineArrowAB.getPrefWidth() / 2 : centerLineArrowAB.getPrefWidth() / 2;
final ChangeListener<Number> listener = (obs, old, newVal) -> {
Rotate r = new Rotate();
r.setPivotX(virtualCenterLine.getStartX());
r.setPivotY(virtualCenterLine.getStartY());
r.setAngle(centerLineArrowAB.getRotate());
Point2D point = r.transform(new Point2D(virtualCenterLine.getStartX(), virtualCenterLine.getStartY() + diff));
directedLine.setStartX(point.getX());
directedLine.setStartY(point.getY());
Rotate r2 = new Rotate();
r2.setPivotX(virtualCenterLine.getEndX());
r2.setPivotY(virtualCenterLine.getEndY());
r2.setAngle(centerLineArrowBA.getRotate());
Point2D point2 = r2.transform(new Point2D(virtualCenterLine.getEndX(), virtualCenterLine.getEndY() - diff));
directedLine.setEndX(point2.getX());
directedLine.setEndY(point2.getY());
};
centerLineArrowAB.rotateProperty().addListener(listener);
centerLineArrowBA.rotateProperty().addListener(listener);
virtualCenterLine.startXProperty().addListener(listener);
virtualCenterLine.startYProperty().addListener(listener);
virtualCenterLine.endXProperty().addListener(listener);
virtualCenterLine.endYProperty().addListener(listener);
StackPane mainArrow = getArrow(isEnd, directedLine, startDot, endDot);
parent.getChildren().addAll(virtualCenterLine, centerLineArrowAB, centerLineArrowBA, directedLine, mainArrow);
}
/**
* Builds a line between the provided start and end panes center point.
*
* #param startDot Pane for considering start point
* #param endDot Pane for considering end point
* #return Line joining the layout center points of the provided panes.
*/
private Line getLine(StackPane startDot, StackPane endDot) {
Line line = new Line();
line.setStroke(Color.BLUE);
line.setStrokeWidth(2);
line.startXProperty().bind(startDot.layoutXProperty().add(startDot.translateXProperty()).add(startDot.widthProperty().divide(2)));
line.startYProperty().bind(startDot.layoutYProperty().add(startDot.translateYProperty()).add(startDot.heightProperty().divide(2)));
line.endXProperty().bind(endDot.layoutXProperty().add(endDot.translateXProperty()).add(endDot.widthProperty().divide(2)));
line.endYProperty().bind(endDot.layoutYProperty().add(endDot.translateYProperty()).add(endDot.heightProperty().divide(2)));
return line;
}
/**
* Builds an arrow on the provided line pointing towards the specified pane.
*
* #param toLineEnd Specifies whether the arrow to point towards end pane or start pane.
* #param line Line joining the layout center points of the provided panes.
* #param startDot Pane which is considered as start point of line
* #param endDot Pane which is considered as end point of line
* #return Arrow towards the specified pane.
*/
private StackPane getArrow(boolean toLineEnd, Line line, StackPane startDot, StackPane endDot) {
double size = 12; // Arrow size
StackPane arrow = new StackPane();
arrow.setStyle("-fx-background-color:#333333;-fx-border-width:1px;-fx-border-color:black;-fx-shape: \"M0,-4L4,0L0,4Z\"");//
arrow.setPrefSize(size, size);
arrow.setMaxSize(size, size);
arrow.setMinSize(size, size);
// Determining the arrow visibility unless there is enough space between dots.
DoubleBinding xDiff = line.endXProperty().subtract(line.startXProperty());
DoubleBinding yDiff = line.endYProperty().subtract(line.startYProperty());
BooleanBinding visible = (xDiff.lessThanOrEqualTo(size).and(xDiff.greaterThanOrEqualTo(-size)).and(yDiff.greaterThanOrEqualTo(-size)).and(yDiff.lessThanOrEqualTo(size))).not();
arrow.visibleProperty().bind(visible);
// Determining the x point on the line which is at a certain distance.
DoubleBinding tX = Bindings.createDoubleBinding(() -> {
double xDiffSqu = (line.getEndX() - line.getStartX()) * (line.getEndX() - line.getStartX());
double yDiffSqu = (line.getEndY() - line.getStartY()) * (line.getEndY() - line.getStartY());
double lineLength = Math.sqrt(xDiffSqu + yDiffSqu);
double dt;
if (toLineEnd) {
// When determining the point towards end, the required distance is total length minus (radius + arrow half width)
dt = lineLength - (endDot.getWidth() / 2) - (arrow.getWidth() / 2);
} else {
// When determining the point towards start, the required distance is just (radius + arrow half width)
dt = (startDot.getWidth() / 2) + (arrow.getWidth() / 2);
}
double t = dt / lineLength;
double dx = ((1 - t) * line.getStartX()) + (t * line.getEndX());
return dx;
}, line.startXProperty(), line.endXProperty(), line.startYProperty(), line.endYProperty());
// Determining the y point on the line which is at a certain distance.
DoubleBinding tY = Bindings.createDoubleBinding(() -> {
double xDiffSqu = (line.getEndX() - line.getStartX()) * (line.getEndX() - line.getStartX());
double yDiffSqu = (line.getEndY() - line.getStartY()) * (line.getEndY() - line.getStartY());
double lineLength = Math.sqrt(xDiffSqu + yDiffSqu);
double dt;
if (toLineEnd) {
dt = lineLength - (endDot.getHeight() / 2) - (arrow.getHeight() / 2);
} else {
dt = (startDot.getHeight() / 2) + (arrow.getHeight() / 2);
}
double t = dt / lineLength;
double dy = ((1 - t) * line.getStartY()) + (t * line.getEndY());
return dy;
}, line.startXProperty(), line.endXProperty(), line.startYProperty(), line.endYProperty());
arrow.layoutXProperty().bind(tX.subtract(arrow.widthProperty().divide(2)));
arrow.layoutYProperty().bind(tY.subtract(arrow.heightProperty().divide(2)));
DoubleBinding endArrowAngle = Bindings.createDoubleBinding(() -> {
double stX = toLineEnd ? line.getStartX() : line.getEndX();
double stY = toLineEnd ? line.getStartY() : line.getEndY();
double enX = toLineEnd ? line.getEndX() : line.getStartX();
double enY = toLineEnd ? line.getEndY() : line.getStartY();
double angle = Math.toDegrees(Math.atan2(enY - stY, enX - stX));
if (angle < 0) {
angle += 360;
}
return angle;
}, line.startXProperty(), line.endXProperty(), line.startYProperty(), line.endYProperty());
arrow.rotateProperty().bind(endArrowAngle);
return arrow;
}
/**
* Builds a pane at the center of the provided line.
*
* #param line Line on which the pane need to be set.
* #return Pane located at the center of the provided line.
*/
private StackPane getWeight(Line line) {
double size = 20;
StackPane weight = new StackPane();
weight.setStyle("-fx-background-color:grey;-fx-border-width:1px;-fx-border-color:black;");
weight.setPrefSize(size, size);
weight.setMaxSize(size, size);
weight.setMinSize(size, size);
DoubleBinding wgtSqrHalfWidth = weight.widthProperty().divide(2);
DoubleBinding wgtSqrHalfHeight = weight.heightProperty().divide(2);
DoubleBinding lineXHalfLength = line.endXProperty().subtract(line.startXProperty()).divide(2);
DoubleBinding lineYHalfLength = line.endYProperty().subtract(line.startYProperty()).divide(2);
weight.layoutXProperty().bind(line.startXProperty().add(lineXHalfLength.subtract(wgtSqrHalfWidth)));
weight.layoutYProperty().bind(line.startYProperty().add(lineYHalfLength.subtract(wgtSqrHalfHeight)));
return weight;
}
/**
* Builds a pane consisting of circle with the provided specifications.
*
* #param color Color of the circle
* #param text Text inside the circle
* #return Draggable pane consisting a circle.
*/
private StackPane getDot(String color, String text) {
double radius = 50;
double paneSize = 2 * radius;
StackPane dotPane = new StackPane();
Circle dot = new Circle();
dot.setRadius(radius);
dot.setStyle("-fx-fill:" + color + ";-fx-stroke-width:2px;-fx-stroke:black;");
Label txt = new Label(text);
txt.setStyle("-fx-font-size:18px;-fx-font-weight:bold;");
dotPane.getChildren().addAll(dot, txt);
dotPane.setPrefSize(paneSize, paneSize);
dotPane.setMaxSize(paneSize, paneSize);
dotPane.setMinSize(paneSize, paneSize);
dotPane.setOnMousePressed(e -> {
sceneX = e.getSceneX();
sceneY = e.getSceneY();
layoutX = dotPane.getLayoutX();
layoutY = dotPane.getLayoutY();
});
EventHandler<MouseEvent> dotOnMouseDraggedEventHandler = e -> {
// Offset of drag
double offsetX = e.getSceneX() - sceneX;
double offsetY = e.getSceneY() - sceneY;
// Taking parent bounds
Bounds parentBounds = dotPane.getParent().getLayoutBounds();
// Drag node bounds
double currPaneLayoutX = dotPane.getLayoutX();
double currPaneWidth = dotPane.getWidth();
double currPaneLayoutY = dotPane.getLayoutY();
double currPaneHeight = dotPane.getHeight();
if ((currPaneLayoutX + offsetX < parentBounds.getWidth() - currPaneWidth) && (currPaneLayoutX + offsetX > -1)) {
// If the dragNode bounds is within the parent bounds, then you can set the offset value.
dotPane.setTranslateX(offsetX);
} else if (currPaneLayoutX + offsetX < 0) {
// If the sum of your offset and current layout position is negative, then you ALWAYS update your translate to negative layout value
// which makes the final layout position to 0 in mouse released event.
dotPane.setTranslateX(-currPaneLayoutX);
} else {
// If your dragNode bounds are outside parent bounds,ALWAYS setting the translate value that fits your node at end.
dotPane.setTranslateX(parentBounds.getWidth() - currPaneLayoutX - currPaneWidth);
}
if ((currPaneLayoutY + offsetY < parentBounds.getHeight() - currPaneHeight) && (currPaneLayoutY + offsetY > -1)) {
dotPane.setTranslateY(offsetY);
} else if (currPaneLayoutY + offsetY < 0) {
dotPane.setTranslateY(-currPaneLayoutY);
} else {
dotPane.setTranslateY(parentBounds.getHeight() - currPaneLayoutY - currPaneHeight);
}
};
dotPane.setOnMouseDragged(dotOnMouseDraggedEventHandler);
dotPane.setOnMouseReleased(e -> {
// Updating the new layout positions
dotPane.setLayoutX(layoutX + dotPane.getTranslateX());
dotPane.setLayoutY(layoutY + dotPane.getTranslateY());
// Resetting the translate positions
dotPane.setTranslateX(0);
dotPane.setTranslateY(0);
});
return dotPane;
}
public static void main(String[] args) {
Application.launch(args);
}
}
I am making the game Breakout and want to move the paddle by it following my mouse-movements. When the mouse leaves my AnchorPane named windowPane, the paddle is halfway gone, too. I would like to ask for help: how do I make the paddle not follow the mouse any longer when I reach the bounds of my pane?
windowPane.setOnMouseMoved(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
/**move Paddle with mouse *#author Can #author SziSzi */
paddle.setLayoutX(event.getY());
paddle.setLayoutX(event.getX()- paddle.getWidth()/2);
}
});
Check, if the position would make the paddle violate the bounds of the pane:
double h = paddle.getHeight();
double pW = paddle.getWidth() / 2;
double x = event.getX();
double y = event.getY();
if (x >= pW && x + pW <= windowPane.getWidth() && y + h <= windowPane.getHeight()) {
paddle.setLayoutY(y);
paddle.setLayoutX(x - pW);
} // otherwise ignore the event
I am coding a slider puzzle, using a 2-D array of buttons where 1 button is blank at all times, and the remaining buttons can only move if they are next to the blank button. Would anyone be able to offer a way to check the surrounding buttons and determine if it borders on the blank one? (keeping in mind boundaries)
Create fields blankX and blankY that contain the current position of the blank.
For each button save the current position in the properties. This allows you to retrieve the coordinates of the button, check, if they are adjacent to the blank and swap the positions, if that's the case.
Example:
private int blankX = 0;
private int blankY = 0;
private static final int SIZE = 50;
private static final String X_KEY = "TileLocationX";
private static final String Y_KEY = "TileLocationY";
private void move(Node n, int x, int y) {
// adjust position
n.setLayoutX(blankX * SIZE);
n.setLayoutY(blankY * SIZE);
// save coordinates to property
n.getProperties().put(X_KEY, blankX);
n.getProperties().put(Y_KEY, blankY);
// save node pos as blank pos
blankX = x;
blankY = y;
}
#Override
public void start(Stage primaryStage) {
Pane pane = new Pane();
EventHandler<ActionEvent> handler = evt -> {
Node n = (Node) evt.getSource();
int x = (Integer) n.getProperties().get(X_KEY);
int y = (Integer) n.getProperties().get(Y_KEY);
if (Math.abs(x - blankX) + Math.abs(y - blankY) == 1) {
// move button, if it's adjacent to the blank
move(n, x, y);
}
};
int count = 1;
// create grid
for (int i = 0; i < 5; i++) {
for (int j = 0; j < 5; j++) {
if (i != blankX || j != blankY) {
Button button = new Button(Integer.toString(count++));
button.setPrefSize(SIZE, SIZE);
button.setLayoutX(SIZE * i);
button.setLayoutY(SIZE * j);
button.getProperties().put(X_KEY, i);
button.getProperties().put(Y_KEY, j);
button.setOnAction(handler);
pane.getChildren().add(button);
}
}
}
Scene scene = new Scene(pane);
primaryStage.setScene(scene);
primaryStage.show();
}
2D arrays make it easy. Check for null above, below, right, and left, making sure that you aren't going out of bounds (aka checking boxes off the actual game grid).
I'm not sure if JavaFX is different from Java, so I can only give pseudo code:
//initialize int[5][5] (position) with tile classes (Tile), setting one to null
//checking tile at position[a][b]
//check position[a-1][b], [a+1][b], [a][b-1], [a][b+1] for null...
//...also making sure the value is between 0 and 5
//if any of those is true, square is moveable!
There might be an even more efficient way to do this, however. If you add another variable (boolean isMoveable) to each tile, then you can start each turn by finding the empty square and simply checking off every tile it borders as moveable. This would even allow you to highlight them somehow at the beginning of the turn, so the player knows what he can move.
And if you don't want to search through the entire board for the empty square, you can simply do it once at the beginning and save it's position to an array. Next turn, look to each square bordering that position to find the empty one.
If you have a 2D array, then you have the x and y location of each button. Keep the location of the blank button in variables, say blankX and blankY.
Then the condition for a button to be adjacent to the blank button is
Either x==blankX and y and blankY differ by 1, or
y==blankY and x and blankX differ by 1
So if you do
int deltaX = Math.abs(x - blankX) ;
int deltaY = Math.abs(y - blankY) ;
the condition is
deltaX==0 && deltaY==1, or
deltaX==1 && deltaY==0
but since deltaX >= 0 and deltaY >= 0, this is just equivalent to
deltaX + deltaY == 1
If you make blankX and blankY JavaFX properties:
private final IntegerProperty blankX = new SimpleIntegerProperty();
private final IntegerProperty blankY = new SimpleIntegerProperty();
then you can easily disable the button when it's not adjacent to the blank buttons:
// creates a button with coordinates x,y and initializes it
private Button createButton(int x, int y) {
Button button = new Button(Integer.toString(x + y * numColumns + 1));
// disable if not adjacent to blank button:
button.disableProperty().bind(Bindings.createBooleanBinding(
() -> Math.abs(x - blankX.get()) + Math.abs(y - blankY.get()) != 1,
blankX, blankY));
// when pressed, swap with blank button and update blankX, blankY:
button.setOnAction(e -> {
buttonArray[blankX.get()][blankY.get()].setText(button.getText());
button.setText("");
blankX.set(x);
blankY.set(y);
});
return button ;
}
I have a sample 3D application (built by taking reference from the Javafx sample 3DViewer) which has a table created by laying out Boxes and Panes:
The table is centered wrt (0,0,0) coordinates and camera is at -z position initially.
It has the zoom-in/out based on the camera z position from the object.
On zooming in/out the object's boundsInParent increases/decreases i.e. area of the face increases/decreases. So the idea is to put more text when we have more area (always confining within the face) and lesser text or no text when the face area is too less. I am able to to do that using this node hierarchy:
and resizing the Pane (and managing the vBox and number of texts in it) as per Box on each zoom-in/out.
Now the issue is that table boundsInParent is giving incorrect results (table image showing the boundingBox off at the top) whenever a text is added to the vBox for the first time only. On further zooming-in/out gives correct boundingBox and does not go off.
Below is the UIpane3D class:
public class UIPane3D extends Pane
{
VBox textPane;
ArrayList<String> infoTextKeys = new ArrayList<>();
ArrayList<Text> infoTextValues = new ArrayList<>();
Rectangle bgCanvasRect = null;
final double fontSize = 16.0;
public UIPane3D() {
setMouseTransparent(true);
textPane = new VBox(2.0)
}
public void updateContent() {
textPane.getChildren().clear();
getChildren().clear();
for (Text textNode : infoTextValues) {
textPane.getChildren().add(textNode);
textPane.autosize();
if (textPane.getHeight() > getHeight()) {
textPane.getChildren().remove(textNode);
textPane.autosize();
break;
}
}
textPane.setTranslateY(getHeight() / 2 - textPane.getHeight() / 2.0);
bgCanvasRect = new Rectangle(getWidth(), getHeight());
bgCanvasRect.setFill(Color.web(Color.BURLYWOOD.toString(), 0.10));
bgCanvasRect.setVisible(true);
getChildren().addAll(bgCanvasRect, textPane);
}
public void resetInfoTextMap()
{
if (infoTextKeys != null || infoTextValues != null)
{
try
{
infoTextKeys.clear();
infoTextValues.clear();
} catch (Exception e){e.printStackTrace();}
}
}
public void updateInfoTextMap(String pKey, String pValue)
{
int index = -1;
boolean objectFound = false;
for (String string : infoTextKeys)
{
index++;
if(string.equals(pKey))
{
objectFound = true;
break;
}
}
if(objectFound)
{
infoTextValues.get(index).setText(pValue.toUpperCase());
}
else
{
if (pValue != null)
{
Text textNode = new Text(pValue.toUpperCase());
textNode.setFont(Font.font("Consolas", FontWeight.BLACK, FontPosture.REGULAR, fontSize));
textNode.wrappingWidthProperty().bind(widthProperty());
textNode.setTextAlignment(TextAlignment.CENTER);
infoTextKeys.add(pKey);
infoTextValues.add(textNode);
}
}
}
}
The code which get called at the last after all the manipulations:
public void refreshBoundingBox()
{
if(boundingBox != null)
{
root3D.getChildren().remove(boundingBox);
}
PhongMaterial blueMaterial = new PhongMaterial();
blueMaterial.setDiffuseColor(Color.web(Color.CRIMSON.toString(), 0.25));
Bounds tableBounds = table.getBoundsInParent();
boundingBox = new Box(tableBounds.getWidth(), tableBounds.getHeight(), tableBounds.getDepth());
boundingBox.setMaterial(blueMaterial);
boundingBox.setTranslateX(tableBounds.getMinX() + tableBounds.getWidth()/2.0);
boundingBox.setTranslateY(tableBounds.getMinY() + tableBounds.getHeight()/2.0);
boundingBox.setTranslateZ(tableBounds.getMinZ() + tableBounds.getDepth()/2.0);
boundingBox.setMouseTransparent(true);
root3D.getChildren().add(boundingBox);
}
Two things:
The table3D's boundsInParent is not updated properly when texts are added for the first time.
What would be the right way of putting texts on 3D nodes? I am having to manipulate a whole lot to bring the texts as required.
Sharing code here.
For the first question, about the "jump" that can be noticed just when after scrolling a new text item is laid out:
After digging into the code, I noticed that the UIPane3D has a VBox textPane that contains the different Text nodes. Every time updateContent is called, it tries to add a text node, but it checks that the vbox's height is always lower than the pane's height, or else the node will be removed:
for (Text textNode : infoTextValues) {
textPane.getChildren().add(textNode);
textPane.autosize();
if (textPane.getHeight() > getHeight()) {
textPane.getChildren().remove(textNode);
textPane.autosize();
break;
}
}
While this is basically correct, when you add a node to the scene, you can't get textPane.getHeight() immediately, as it hasn't been laid out yet, and you have to wait until the next pulse. This is why the next time you scroll, the height is correct and the bounding box is well placed.
One way to force the layout and get the correct height of the textNode is by forcing css and a layout pass:
for (Text textNode : infoTextValues) {
textPane.getChildren().add(textNode);
// force css and layout
textPane.applyCss();
textPane.layout();
textPane.autosize();
if (textPane.getHeight() > getHeight()) {
textPane.getChildren().remove(textNode);
textPane.autosize();
break;
}
}
Note that:
This method [applyCss] does not normally need to be invoked directly but may be used in conjunction with Parent.layout() to size a Node before the next pulse, or if the Scene is not in a Stage.
For the second question, about a different solution to add Text to 3D Shape.
Indeed, placing a (2D) text on top of a 3D shape is quite difficult, and requires complex maths (that are done quite nicely in the project, by the way).
There is an alternative avoiding the use of 2D nodes directly.
Precisely in a previous question, I "wrote" into an image, that later on I used as the material diffuse map of a 3D shape.
The built-in 3D Box places the same image into every face, so that wouldn't work. We can implement a 3D prism, or we can make use of the CuboidMesh node from the FXyz3D library.
Replacing the Box in UIPaneBoxGroup:
final CuboidMesh contentShape;
UIPane3D displaypane = null;
PhongMaterial shader = new PhongMaterial();
final Color pColor;
public UIPaneBoxGroup(final double pWidth, final double pHeight, final double pDepth, final Color pColor) {
contentShape = new CuboidMesh(pWidth, pHeight, pDepth);
this.pColor = pColor;
contentShape.setMaterial(shader);
getChildren().add(contentShape);
addInfoUIPane();
}
and adding the generateNet method:
private Image generateNet(String string) {
GridPane grid = new GridPane();
grid.setAlignment(Pos.CENTER);
Label label5 = new Label(string);
label5.setFont(Font.font("Consolas", FontWeight.BLACK, FontPosture.REGULAR, 40));
GridPane.setHalignment(label5, HPos.CENTER);
grid.add(label5, 3, 1);
double w = contentShape.getWidth() * 10; // more resolution
double h = contentShape.getHeight() * 10;
double d = contentShape.getDepth() * 10;
final double W = 2 * d + 2 * w;
final double H = 2 * d + h;
ColumnConstraints col1 = new ColumnConstraints();
col1.setPercentWidth(d * 100 / W);
ColumnConstraints col2 = new ColumnConstraints();
col2.setPercentWidth(w * 100 / W);
ColumnConstraints col3 = new ColumnConstraints();
col3.setPercentWidth(d * 100 / W);
ColumnConstraints col4 = new ColumnConstraints();
col4.setPercentWidth(w * 100 / W);
grid.getColumnConstraints().addAll(col1, col2, col3, col4);
RowConstraints row1 = new RowConstraints();
row1.setPercentHeight(d * 100 / H);
RowConstraints row2 = new RowConstraints();
row2.setPercentHeight(h * 100 / H);
RowConstraints row3 = new RowConstraints();
row3.setPercentHeight(d * 100 / H);
grid.getRowConstraints().addAll(row1, row2, row3);
grid.setPrefSize(W, H);
grid.setBackground(new Background(new BackgroundFill(pColor, CornerRadii.EMPTY, Insets.EMPTY)));
new Scene(grid);
return grid.snapshot(null, null);
}
Now all the 2D related code can be removed (including displaypane), and after a scrolling event get the image:
public void refreshBomUIPane() {
Image net = generateNet(displaypane.getText());
shader.setDiffuseMap(net);
}
where in UIPane3D:
public String getText() {
return infoTextKeys.stream().collect(Collectors.joining("\n"));
}
I've also removed the bounding box to get this picture:
I haven't played around with the number of text nodes that can be added to the VBox, the font size nor with an strategy to avoid generating images on every scroll: only when the text changes this should be done. So with the current approach is quite slow, but it can be improved notably as there are only three possible images for each box.
I am creating a top-down game in JavaFX, but I'm having trouble with the implementation of a Camera that moves with the player.
My attempt at creating something like this was to instead of moving the player, move the scene in the opposite direction the player wanted to go. This created the illusion that the player was moving, but it required constant movement of all the objects in the scene which obviously created a ton of performance issues. So after this I made a clip, and put all the terrain nodes inside a clipped rectangle.
Below is my TerrainRenderer class that creates the clipped rectangle and the contents inside of it. What it does is take an image and then generate a bunch of rectangle nodes in order to make a map that looks like the image.
private static final Pane tileContainer = new Pane();
private static final Rectangle rectClip = new Rectangle();
private static void clipChildren(Region region) {
region.setClip(rectClip);
region.layoutBoundsProperty().addListener((ov, oldValue, newValue) -> {
rectClip.setWidth(newValue.getWidth());
rectClip.setHeight(newValue.getHeight());
});
}
private static void drawTile(int x, int y, Color color) {
final int TILE_SIZE = 15;
Rectangle tile = new Rectangle(x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE);
tile.setFill(color);
tileContainer.getChildren().add(tile);
}
public static Region generate() {
final Image map = new Image("main/Images/IcJR6.png");
for (int x = 0; x < (int) map.getWidth(); x++) {
for (int y = 0; y < (int) map.getHeight(); y++) {
drawTile(x, y, map.getPixelReader().getColor(x, y));
}
}
tileContainer.setPrefSize(Main.getAppWidth(), Main.getAppHeight());
clipChildren(tileContainer);
return tileContainer;
}
public static Rectangle getRectClip() {
return rectClip;
}
What you see below is my update method for the player that uses a sprite sheet. As of now this code only translates the clip node, but not the contents inside.
void update() {
int speed;
if (Main.isPressed(KeyCode.SHIFT)) speed = 6;
else speed = 3;
if (Main.isPressed(KeyCode.W)) {
getAnimation().play();
getAnimation().setOffsetY(96);
moveY(speed);
} else if (Main.isPressed(KeyCode.S)) {
getAnimation().play();
getAnimation().setOffsetY(0);
moveY(-speed);
} else if (Main.isPressed(KeyCode.D)) {
getAnimation().play();
getAnimation().setOffsetY(64);
moveX(-speed);
} else if (Main.isPressed(KeyCode.A)) {
getAnimation().play();
getAnimation().setOffsetY(32);
moveX(speed);
} else getAnimation().stop();
}
#Override
protected void moveX(int x) {
boolean right = x > 0;
for(int i = 0; i < Math.abs(x); i++) {
if (right) TerrainRenderer.getRectClip().setTranslateX(TerrainRenderer.getRectClip().getTranslateX() + 1);
else TerrainRenderer.getRectClip().setTranslateX(TerrainRenderer.getRectClip().getTranslateX() - 1);
}
}
#Override
protected void moveY(int y) {
boolean down = y > 0;
for (int i = 0; i < Math.abs(y); i++) {
if (down) TerrainRenderer.getRectClip().setTranslateY(TerrainRenderer.getRectClip().getTranslateY() + 1);
else TerrainRenderer.getRectClip().setTranslateY(TerrainRenderer.getRectClip().getTranslateY() - 1);
}
}
The result I want would look something like this (skip to 6:10), but how would I make something like this in JavaFX instead? Any suggestions?
You haven't posted a minimal, complete, and verifiable example showing what the actual problem is, so your question is difficult to answer completely.
I would approach something like this by drawing the background (e.g. on a canvas), and putting it in a pane with the moving parts (player, by the sounds of your description). Then show just a portion of the background by clipping and translating the pane.
Here's a very quick example; it just puts some random small rectangles on a large canvas and then moves a blue rectangle (player) around the scene on pressing the cursor (arrow) keys. The clip and translation of the main pane are bound to the player's position so the player always appears in the center, except when you get close to the edges of the pane.
This takes a little time to start up, and for some reason I sometimes see a blank screen until I have moved the player a couple of places; I didn't spend too much time on niceties so there may be some little bugs in there.
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.input.KeyCode;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
public class ScrollAndClipBackground extends Application {
private final int tileSize = 10 ;
private final int numTilesHoriz = 500 ;
private final int numTilesVert = 500 ;
private final int speed = 400 ; // pixels / second
private boolean up ;
private boolean down ;
private boolean left ;
private boolean right ;
private final int numFilledTiles = numTilesHoriz * numTilesVert / 8 ;
#Override
public void start(Stage primaryStage) {
Pane pane = createBackground();
Rectangle player = new Rectangle(numTilesHoriz*tileSize/2, numTilesVert*tileSize/2, 10, 10);
player.setFill(Color.BLUE);
pane.getChildren().add(player);
Scene scene = new Scene(new BorderPane(pane), 800, 800);
Rectangle clip = new Rectangle();
clip.widthProperty().bind(scene.widthProperty());
clip.heightProperty().bind(scene.heightProperty());
clip.xProperty().bind(Bindings.createDoubleBinding(
() -> clampRange(player.getX() - scene.getWidth() / 2, 0, pane.getWidth() - scene.getWidth()),
player.xProperty(), scene.widthProperty()));
clip.yProperty().bind(Bindings.createDoubleBinding(
() -> clampRange(player.getY() - scene.getHeight() / 2, 0, pane.getHeight() - scene.getHeight()),
player.yProperty(), scene.heightProperty()));
pane.setClip(clip);
pane.translateXProperty().bind(clip.xProperty().multiply(-1));
pane.translateYProperty().bind(clip.yProperty().multiply(-1));
scene.setOnKeyPressed(e -> processKey(e.getCode(), true));
scene.setOnKeyReleased(e -> processKey(e.getCode(), false));
AnimationTimer timer = new AnimationTimer() {
private long lastUpdate = -1 ;
#Override
public void handle(long now) {
long elapsedNanos = now - lastUpdate ;
if (lastUpdate < 0) {
lastUpdate = now ;
return ;
}
double elapsedSeconds = elapsedNanos / 1_000_000_000.0 ;
double deltaX = 0 ;
double deltaY = 0 ;
if (right) deltaX += speed ;
if (left) deltaX -= speed ;
if (down) deltaY += speed ;
if (up) deltaY -= speed ;
player.setX(clampRange(player.getX() + deltaX * elapsedSeconds, 0, pane.getWidth() - player.getWidth()));
player.setY(clampRange(player.getY() + deltaY * elapsedSeconds, 0, pane.getHeight() - player.getHeight()));
lastUpdate = now ;
}
};
primaryStage.setScene(scene);
primaryStage.show();
timer.start();
}
private double clampRange(double value, double min, double max) {
if (value < min) return min ;
if (value > max) return max ;
return value ;
}
private void processKey(KeyCode code, boolean on) {
switch (code) {
case LEFT:
left = on ;
break ;
case RIGHT:
right = on ;
break ;
case UP:
up = on ;
break ;
case DOWN:
down = on ;
break ;
default:
break ;
}
}
private Pane createBackground() {
List<Integer> filledTiles = sampleWithoutReplacement(numFilledTiles, numTilesHoriz * numTilesVert);
Canvas canvas = new Canvas(numTilesHoriz * tileSize, numTilesVert * tileSize);
GraphicsContext gc = canvas.getGraphicsContext2D();
gc.setFill(Color.GREEN);
Pane pane = new Pane(canvas);
pane.setMinSize(numTilesHoriz * tileSize, numTilesVert * tileSize);
pane.setPrefSize(numTilesHoriz * tileSize, numTilesVert * tileSize);
pane.setMaxSize(numTilesHoriz * tileSize, numTilesVert * tileSize);
for (Integer tile : filledTiles) {
int x = (tile % numTilesHoriz) * tileSize ;
int y = (tile / numTilesHoriz) * tileSize ;
gc.fillRect(x, y, tileSize, tileSize);
}
return pane ;
}
private List<Integer> sampleWithoutReplacement(int sampleSize, int populationSize) {
Random rng = new Random();
List<Integer> population = new ArrayList<>();
for (int i = 0 ; i < populationSize; i++)
population.add(i);
List<Integer> sample = new ArrayList<>();
for (int i = 0 ; i < sampleSize ; i++)
sample.add(population.remove(rng.nextInt(population.size())));
return sample;
}
public static void main(String[] args) {
launch(args);
}
}
A more complex approach, which would be less memory intensive, would be a "tiling" mechanism where the main view consists of a number of tiles which are moved, and created as needed on demand. This is more complex but allows for essentially arbitrary-sized scenes.