I need to move Nodes (mosty clipped ImageViews) between containers. During the "parent change" I have to keep the components visually in place, so from the users' perspective this reorganization should be transparent.
The schema of my scene is like this:
The Nodes I have to move are clipped, translated some effects applied to them and originally reside under the Board pane. The contents group of the desk is scaled, clipped and translated.
I would like to move them to the MoverLayer. It works fine, due to the moverLayer is bound to the Board:
moverLayer.translateXProperty().bind(board.translateXProperty());
moverLayer.translateYProperty().bind(board.translateYProperty());
moverLayer.scaleXProperty().bind(board.scaleXProperty());
moverLayer.scaleYProperty().bind(board.scaleYProperty());
moverLayer.layoutXProperty().bind(board.layoutXProperty());
moverLayer.layoutYProperty().bind(board.layoutYProperty());
so I can simply move the nodes between them:
public void start(MouseEvent me) {
board.getContainer().getChildren().remove(node);
desk.getMoverLayer().getChildren().add(node);
}
public void finish(MouseEvent me) {
desk.getMoverLayer().getChildren().remove(node);
board.getContainer().getChildren().add(node);
}
However, when moving nodes between the contents of tray and the MoverLayer it starts to get complicated. I tried to play with different coordinates (local, parent, scene, screen), but somehow it is always misplaced. It seems, that when the scale is 1.0 for desk.contents, it works to map the coordinates of translateX and translateY to screen coordinates, switch the parent and then map back the screen coordinates to local and use as translation. But with non-identical scaling, the coordinates differs (and the node moves). I also tried to map the coordinates to the common parent (desk) recursively, but works neither.
My generalized question is, what is the best practice to calculate coordinates of the same point relative to different parents?
Here is the MCVE code. Sorry, I simply couldn't make it more simple.
package hu.vissy.puzzlefx.stackoverflow;
import javafx.application.Application;
import javafx.geometry.BoundingBox;
import javafx.geometry.Bounds;
import javafx.geometry.Insets;
import javafx.geometry.Point2D;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.CornerRadii;
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 Mcve extends Application {
// The piece to move
private Rectangle piece;
// The components representing the above structure
private Pane board;
private Group moverLayer;
private Pane trayContents;
private Pane desk;
private Group deskContents;
// The zoom scale
private double scale = 1;
// Drag info
private double startDragX;
private double startDragY;
private Point2D dragAnchor;
public Mcve() {
}
public void init(Stage primaryStage) {
// Added only for simulation
Button b = new Button("Zoom");
b.setOnAction((ah) -> setScale(0.8));
desk = new Pane();
desk.setPrefSize(800, 600);
desk.setBackground(new Background(new BackgroundFill(Color.LIGHTGREEN, CornerRadii.EMPTY, Insets.EMPTY)));
deskContents = new Group();
desk.getChildren().add(deskContents);
board = new Pane();
board.setPrefSize(700, 600);
board.setBackground(new Background(new BackgroundFill(Color.LIGHTCORAL, CornerRadii.EMPTY, Insets.EMPTY)));
// Symbolize the piece to be dragged
piece = new Rectangle();
piece.setTranslateX(500);
piece.setTranslateY(50);
piece.setWidth(50);
piece.setHeight(50);
piece.setFill(Color.BLACK);
board.getChildren().add(piece);
// Mover layer is always on top and is bound to the board (used to display the
// dragged items above all desk contents during dragging)
moverLayer = new Group();
moverLayer.translateXProperty().bind(board.translateXProperty());
moverLayer.translateYProperty().bind(board.translateYProperty());
moverLayer.scaleXProperty().bind(board.scaleXProperty());
moverLayer.scaleYProperty().bind(board.scaleYProperty());
moverLayer.layoutXProperty().bind(board.layoutXProperty());
moverLayer.layoutYProperty().bind(board.layoutYProperty());
board.setTranslateX(50);
board.setTranslateY(50);
Pane tray = new Pane();
tray.setPrefSize(400, 400);
tray.relocate(80, 80);
Pane header = new Pane();
header.setPrefHeight(30);
header.setBackground(new Background(new BackgroundFill(Color.LIGHTSLATEGRAY, CornerRadii.EMPTY, Insets.EMPTY)));
trayContents = new Pane();
trayContents.setBackground(new Background(new BackgroundFill(Color.BEIGE, CornerRadii.EMPTY, Insets.EMPTY)));
VBox layout = new VBox();
layout.getChildren().addAll(header, trayContents);
VBox.setVgrow(trayContents, Priority.ALWAYS);
layout.setPrefSize(400, 400);
tray.getChildren().add(layout);
deskContents.getChildren().addAll(board, tray, moverLayer, b);
Scene scene = new Scene(desk);
// Piece is draggable
piece.setOnMousePressed((me) -> startDrag(me));
piece.setOnMouseDragged((me) -> doDrag(me));
piece.setOnMouseReleased((me) -> endDrag(me));
primaryStage.setScene(scene);
}
// Changing the scale
private void setScale(double scale) {
this.scale = scale;
// Reseting piece position and parent if needed
if (piece.getParent() != board) {
piece.setTranslateX(500);
piece.setTranslateY(50);
trayContents.getChildren().remove(piece);
board.getChildren().add(piece);
}
deskContents.setScaleX(getScale());
deskContents.setScaleY(getScale());
}
private double getScale() {
return scale;
}
private void startDrag(MouseEvent me) {
// Saving drag options
startDragX = piece.getTranslateX();
startDragY = piece.getTranslateY();
dragAnchor = new Point2D(me.getSceneX(), me.getSceneY());
// Putting the item into the mover layer -- works fine with all zoom scale level
board.getChildren().remove(piece);
moverLayer.getChildren().add(piece);
me.consume();
}
// Doing the drag
private void doDrag(MouseEvent me) {
double newTranslateX = startDragX + (me.getSceneX() - dragAnchor.getX()) / getScale();
double newTranslateY = startDragY + (me.getSceneY() - dragAnchor.getY()) / getScale();
piece.setTranslateX(newTranslateX);
piece.setTranslateY(newTranslateY);
me.consume();
}
private void endDrag(MouseEvent me) {
// For MCVE's sake I take that the drop is over the tray.
Bounds op = piece.localToScreen(piece.getBoundsInLocal());
moverLayer.getChildren().remove(piece);
// One of my several tries: mapping the coordinates till the common parent.
// I also tried to use localtoScreen -> change parent -> screenToLocal
Bounds b = localToParentRecursive(trayContents, desk, trayContents.getBoundsInLocal());
Bounds b2 = localToParentRecursive(board, desk, board.getBoundsInLocal());
trayContents.getChildren().add(piece);
piece.setTranslateX(piece.getTranslateX() + b2.getMinX() - b.getMinX() * getScale());
piece.setTranslateY(piece.getTranslateY() + b2.getMinY() - b.getMinY() * getScale());
me.consume();
}
public static Point2D localToParentRecursive(Node n, Parent parent, double x, double y) {
// For simplicity I suppose that the n node is on the path of the parent
Point2D p = new Point2D(x, y);
Node cn = n;
while (true) {
if (cn == parent) {
break;
}
p = cn.localToParent(p);
cn = cn.getParent();
}
return p;
}
public static Bounds localToParentRecursive(Node n, Parent parent, Bounds bounds) {
Point2D p = localToParentRecursive(n, parent, bounds.getMinX(), bounds.getMinY());
return new BoundingBox(p.getX(), p.getY(), bounds.getWidth(), bounds.getHeight());
}
#Override
public void start(Stage primaryStage) throws Exception {
init(primaryStage);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Well. After a lot of debugging, calculation and try I was able to find a solution.
Here is the utility function I wrote for doing the parent swap:
/**
* Change the parent of a node.
*
* <p>
* The node should have a common ancestor with the new parent.
* </p>
*
* #param item
* The node to move.
* #param newParent
* The new parent.
*/
#SuppressWarnings("unchecked")
public static void changeParent(Node item, Parent newParent) {
try {
// HAve to use reflection, because the getChildren method is protected in common ancestor of all
// parent nodes.
// Checking old parent for public getChildren() method
Parent oldParent = item.getParent();
if ((oldParent.getClass().getMethod("getChildren").getModifiers() & Modifier.PUBLIC) != Modifier.PUBLIC) {
throw new IllegalArgumentException("Old parent has no public getChildren method.");
}
// Checking new parent for public getChildren() method
if ((newParent.getClass().getMethod("getChildren").getModifiers() & Modifier.PUBLIC) != Modifier.PUBLIC) {
throw new IllegalArgumentException("New parent has no public getChildren method.");
}
// Finding common ancestor for the two parents
Parent commonAncestor = findCommonAncestor(oldParent, newParent);
if (commonAncestor == null) {
throw new IllegalArgumentException("Item has no common ancestor with the new parent.");
}
// Bounds of the item
Bounds itemBoundsInParent = item.getBoundsInParent();
// Mapping coordinates to common ancestor
Bounds boundsInParentBeforeMove = localToParentRecursive(oldParent, commonAncestor, itemBoundsInParent);
// Swapping parent
((Collection<Node>) oldParent.getClass().getMethod("getChildren").invoke(oldParent)).remove(item);
((Collection<Node>) newParent.getClass().getMethod("getChildren").invoke(newParent)).add(item);
// Mapping coordinates back from common ancestor
Bounds boundsInParentAfterMove = parentToLocalRecursive(newParent, commonAncestor, boundsInParentBeforeMove);
// Setting new translation
item.setTranslateX(
item.getTranslateX() + (boundsInParentAfterMove.getMinX() - itemBoundsInParent.getMinX()));
item.setTranslateY(
item.getTranslateY() + (boundsInParentAfterMove.getMinY() - itemBoundsInParent.getMinY()));
} catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
throw new IllegalStateException("Error while switching parent.", e);
}
}
/**
* Finds the topmost common ancestor of two nodes.
*
* #param firstNode
* The first node to check.
* #param secondNode
* The second node to check.
* #return The common ancestor or null if the two node is on different
* parental tree.
*/
public static Parent findCommonAncestor(Node firstNode, Node secondNode) {
// Builds up the set of all ancestor of the first node.
Set<Node> parentalChain = new HashSet<>();
Node cn = firstNode;
while (cn != null) {
parentalChain.add(cn);
cn = cn.getParent();
}
// Iterates down through the second ancestor for common node.
cn = secondNode;
while (cn != null) {
if (parentalChain.contains(cn)) {
return (Parent) cn;
}
cn = cn.getParent();
}
return null;
}
/**
* Transitively converts the coordinates from the node to an ancestor's
* coordinate system.
*
* #param node
* The node the starting coordinates are local to.
* #param ancestor
* The ancestor to map the coordinates to.
* #param x
* The X of the point to be converted.
* #param y
* The Y of the point to be converted.
* #return The converted coordinates.
*/
public static Point2D localToParentRecursive(Node node, Parent ancestor, double x, double y) {
Point2D p = new Point2D(x, y);
Node cn = node;
while (cn != null) {
if (cn == ancestor) {
return p;
}
p = cn.localToParent(p);
cn = cn.getParent();
}
throw new IllegalStateException("The node is not a descedent of the parent.");
}
/**
* Transitively converts the coordinates of a bound from the node to an
* ancestor's coordinate system.
*
* #param node
* The node the starting coordinates are local to.
* #param ancestor
* The ancestor to map the coordinates to.
* #param bounds
* The bounds to be converted.
* #return The converted bounds.
*/
public static Bounds localToParentRecursive(Node node, Parent ancestor, Bounds bounds) {
Point2D p = localToParentRecursive(node, ancestor, bounds.getMinX(), bounds.getMinY());
return new BoundingBox(p.getX(), p.getY(), bounds.getWidth(), bounds.getHeight());
}
/**
* Transitively converts the coordinates from an ancestor's coordinate
* system to the nodes local.
*
* #param node
* The node the resulting coordinates should be local to.
* #param ancestor
* The ancestor the starting coordinates are local to.
* #param x
* The X of the point to be converted.
* #param y
* The Y of the point to be converted.
* #return The converted coordinates.
*/
public static Point2D parentToLocalRecursive(Node n, Parent parent, double x, double y) {
List<Node> parentalChain = new ArrayList<>();
Node cn = n;
while (cn != null) {
if (cn == parent) {
break;
}
parentalChain.add(cn);
cn = cn.getParent();
}
if (cn == null) {
throw new IllegalStateException("The node is not a descedent of the parent.");
}
Point2D p = new Point2D(x, y);
for (int i = parentalChain.size() - 1; i >= 0; i--) {
p = parentalChain.get(i).parentToLocal(p);
}
return p;
}
/**
* Transitively converts the coordinates of the bounds from an ancestor's
* coordinate system to the nodes local.
*
* #param node
* The node the resulting coordinates should be local to.
* #param ancestor
* The ancestor the starting coordinates are local to.
* #param bounds
* The bounds to be converted.
* #return The converted coordinates.
*/
public static Bounds parentToLocalRecursive(Node n, Parent parent, Bounds bounds) {
Point2D p = parentToLocalRecursive(n, parent, bounds.getMinX(), bounds.getMinY());
return new BoundingBox(p.getX(), p.getY(), bounds.getWidth(), bounds.getHeight());
}
The above solution works well, but I wonder if it is the simpliest way to do the task. For the sake of generality I had to use some reflection: the getChildren() method of the Parent class is protected, only its descedents make it public if they wish, so I can't call it directly through Parent.
The use of the above utility is simple: call changeParent( node, newParent ).
This utilities gives also function to find the common ancestor of two nodes and to recursively convert coordinates through the nodes ancestal chain to and from.
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);
}
}
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.
I'm using a GridPane to stock informations about cities (in a game), but sometimes I want to delete some lines. This is the Class I use for my GridPane :
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import com.franckyi.kingsim.city.City;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.layout.GridPane;
import javafx.scene.text.Text;
public class CityGrid extends GridPane {
private List<City> cities;
public CityGrid() {
super();
cities = new ArrayList<City>();
}
public List<City> getCities() {
return cities;
}
public void deleteCity(int row) {
cities.remove(row - 1);
removeNodes(getNodesFromRow(row));
int i = row;
while (!getNodesFromRow(i + 1).isEmpty()) {
moveNodes(i + 1, getNodesFromRow(i + 1));
removeNodes(getNodesFromRow(i + 1));
i++;
}
}
public void addCity(City city) {
cities.add(city);
int col = 0;
List<Node> rowNodes = new ArrayList<Node>();
Button mod = new Button("Modify");
mod.setOnAction(event -> {
});
Button del = new Button("Delete");
del.setOnAction(event -> {
deleteCity(getRowIndex(del));
});
rowNodes.addAll(Arrays.asList(new CheckBox(), new Text(city.getName()), new Text(city.getStatus().getText()),
new Text(city.getTrade() + ""), new Text(city.getCost() + ""), new Text(city.getTroops() + ""), mod,
del));
for (Node n : rowNodes) {
n.setId(cities.size() + "");
this.add(n, col, cities.size());
col++;
}
}
private List<Node> getNodesFromRow(int i) {
List<Node> list = new ArrayList<Node>();
for (Node n : getChildren()) {
if (getRowIndex(n).equals(i)) {
list.add(n);
}
}
System.out.println(list.size());
return list;
}
private void removeNodes(List<Node> list) {
for (Node node : list) {
this.getChildren().remove(getIndex(getColumnIndex(node), getRowIndex(node)));
}
}
private void moveNodes(int row, List<Node> nodes) {
int i = 0;
for (Node node : getNodesFromRow(row)) {
this.getChildren().set(getIndex(getColumnIndex(node), getRowIndex(node)),
nodes.get(i));
i++;
}
}
private int getIndex(int col, int row) {
int i = 0;
for (Node node : getChildren()) {
if (getColumnIndex(node) == col && getRowIndex(node) == row)
return i;
i++;
}
return 0;
}
}
The addCity function works perfectly. The deleteCity function also works when I delete the last city added. But when I delete a city, it automatically deletes ALL the cities added after the one I delete, and I don't want that.
You should also notice that everytime the getNodesFromRow(int i) method is executed, it prints the number of Nodes in the selected row. When I add two cities and I delete the first one, this is what I get in the console : 8, 0, 8, 8, 8, 8, 8, 0.
Can someone help me ? (Tell me if you want all the code needed to reproduce it at home)
private void moveNodes(int row, List<Node> nodes) {
int i = 0;
for (Node node : getNodesFromRow(row)) {
this.getChildren().set(getIndex(getColumnIndex(node), getRowIndex(node)),
nodes.get(i));
i++;
}
}
This does not work. The index in the child list has no meaning in a GridPaneother then the order in which they are drawn. The row/column index is saved to the properties map of each child. To modify these, you need to use the static GridPane.setRowIndex method.
Example
#Override
public void start(Stage primaryStage) {
Button btn = new Button("Delete");
TextField tf = new TextField();
TextFormatter<Integer> formatter = new TextFormatter<>(new IntegerStringConverter());
formatter.setValue(0);
tf.setTextFormatter(formatter);
btn.disableProperty().bind(IntegerExpression.integerExpression(formatter.valueProperty()).lessThan(0));
GridPane grid = new GridPane();
grid.setHgap(5);
grid.setVgap(5);
btn.setOnAction((ActionEvent event) -> {
deleteRow(grid, formatter.getValue());
});
for (int r = 0; r < 5; r++) {
for (int c = 0; c < 3; c++) {
grid.add(new Text(r+"_"+c), c, r);
}
}
Scene scene = new Scene(new VBox(new HBox(tf, btn), grid));
primaryStage.setScene(scene);
primaryStage.show();
}
static void deleteRow(GridPane grid, final int row) {
Set<Node> deleteNodes = new HashSet<>();
for (Node child : grid.getChildren()) {
// get index from child
Integer rowIndex = GridPane.getRowIndex(child);
// handle null values for index=0
int r = rowIndex == null ? 0 : rowIndex;
if (r > row) {
// decrement rows for rows after the deleted row
GridPane.setRowIndex(child, r-1);
} else if (r == row) {
// collect matching rows for deletion
deleteNodes.add(child);
}
}
// remove nodes from row
grid.getChildren().removeAll(deleteNodes);
}
I had the same task today with the added requirement to not only remove (hide) a single row but multiple rows and also to readd (unhide) them later. (My use case is a form that has to show varying subsets of a set of fields, depending on choices made by the user.)
Put on the right track by fabian, whom I'd like to thank, I designed the GridPaneRowHider class shown below.
To use it with a given GridPane,
GridPane myPane = ...
allocate an instance
GridPaneRowHider h = new GridPaneRowHider();
and call
h.hide(myPane,1,2,6);
to hide the specified rows and
h.unhide(myPane);
to reveal them again.
So here's the code:
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.IntStream;
import nnn.util.collection.Pair;
import javafx.scene.Node;
import javafx.scene.layout.GridPane;
/**
* An object that can hide and unhide rows in a GridPane.
*
* #author Tillmann
* #since 1.1.9
*/
public class GridPaneRowHider {
/**
* A container of the currently hidden Nodes, along with information about
* their positions; associates rows with Nodes in row; sorted by rows,
* ascending
*/
private Map<Integer,List<Pair<Node,Integer>>> hidden_ = new TreeMap<Integer,List<Pair<Node,Integer>>>(
(a, b) -> a.compareTo(b));
/**
* From the specified GridPane hides the specified rows.
*
* #param gp the GridPane
* #param rows the rows to hide
* #see #unhide(GridPane)
*/
public void hide(GridPane gp, int... rows) {
// to suit the algorithm, sort the rows in descending fashion (higher row
// numbers before lower, i.e. visually south to north)
Integer[] sRows = IntStream.of(rows).boxed().toArray(Integer[]::new);
Arrays.sort(sRows,(a, b) -> b.compareTo(a));
// row by row
for (int row : sRows) {
// the list of Nodes in this row
List<Pair<Node,Integer>> li = new ArrayList<Pair<Node,Integer>>();
hidden_.put(Integer.valueOf(row),li);
// look at all the Nodes
for (Iterator<Node> it = gp.getChildren().iterator(); it.hasNext();) {
Node n = it.next();
int r = GridPane.getRowIndex(n);
// if it's a Node in the row to hide
if (r == row) {
// save it in the list
li.add(new Pair<>(n,GridPane.getColumnIndex(n)));
// and remove it from the GridPane
it.remove();
}
// if it's a Node further down, move it a row upwards (to the North)
// to fill the visual gap
else if (r > row)
GridPane.setRowIndex(n,r - 1);
}
}
}
/**
* Shows the rows again that have been hidden by a previous call to
* <code>hide()</code>.
* <p>
*
* If no call to hide() took place before (or unhide() has been called
* already), nothing happens. If hide() has been called several times, the
* result is undefined.
*
* #param gp the GridPane
* #see #hide(GridPane, int...)
*/
public void unhide(GridPane gp) {
// walk along the rows from north to south
for (Map.Entry<Integer,List<Pair<Node,Integer>>> me : hidden_.entrySet()) {
// look at the Nodes in the GridPane
for (Node n : gp.getChildren()) {
int r = GridPane.getRowIndex(n);
// if the Node is in the row to unhide or further down, make room
// by moving it a row further down (to the south)
if (r >= me.getKey())
GridPane.setRowIndex(n,r + 1);
}
// readd the hidden Nodes to the GridPane at their saved locations
for (Pair<Node,Integer> p : me.getValue())
gp.add(p.t_,p.u_,me.getKey());
}
// and mark we're done
hidden_.clear();
}
}
i was trying to implement a custom context menu that the menu items will appear around the node, the idea was as follows, draw a virtual circle around the target node, and compute the circumference of the circle, first menu item will be at the top of the target node, and the rest will be distributed with equal distance between each one of the menu items,,,, i didnt write any line of code because i dont know how to start at least,,, plz help ?
maybe this image will explain better
in case someone is interested, i manged to do it , but the code is kinda dirty:
import javafx.scene.control.Button;
import javafx.scene.layout.StackPane;
import javafx.scene.shape.Circle;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
/**
* #author Asendar
*
*/
#NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class RadialMenuFactory {
public static final RadialMenuFactory instance = new RadialMenuFactory();
public StackPane construct(int itemsCount, int radius) {
StackPane pane = new StackPane();
double pheta = 270;
int counter = 0;
while (counter != itemsCount) {
double x = radius * Math.cos(Math.toRadians(pheta));
double y = radius * Math.sin(Math.toRadians(pheta));
Button btn = new Button("btn");
btn.setTranslateX(x);
btn.setTranslateY(y);
pane.getChildren().addAll(btn);
pheta += 360 / itemsCount;
pheta %= 360;
counter++;
}
pane.setMinHeight(300);
pane.setMinWidth(300);
return pane;
}
}
I'm new to Javafx and I'm trying to make a game with it.
For this I need a fluid motion of some objects on the screen.
I'm not sure, which is the best way.
I started a testfile with some rectangle. I wanted the rectangle to move along a path to the click position. I can make it appear there by just setting the position. So I thought I just could make smaller steps and then the motion would appear fluid. But it doesnt work this way. Either it is because the movement is to fast, so I would need to make the process wait (I wanted to use threads for that purpose) or it is because the java intepreter isn't sequentiell and therefore it just shows the final position. Maybe both or something I didn't come up with.
Now I would like to know weather my thoughts on this topic are right and if there is a more elegant way to achieve my goal.
I hope you can give me some advise!
regards Felix
What you need to do for your car game is to read Daniel Shiffman's The Nature of Code, especially chapter 6.3 The Steering Force.
The book is very easy to understand. You can apply the code to JavaFX. I'll not go into details, you have to learn JavaFX yourself. So here's just the code:
You need an AnimationTimer in which you apply forces, move your objects depending on the forces and show your JavaFX nodes in the UI depending on the location of your objects.
Main.java
package application;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.stage.Stage;
public class Main extends Application {
static Random random = new Random();
Layer playfield;
List<Attractor> allAttractors = new ArrayList<>();
List<Vehicle> allVehicles = new ArrayList<>();
AnimationTimer gameLoop;
Vector2D mouseLocation = new Vector2D( 0, 0);
Scene scene;
MouseGestures mouseGestures = new MouseGestures();
#Override
public void start(Stage primaryStage) {
// create containers
BorderPane root = new BorderPane();
// playfield for our Sprites
playfield = new Layer( Settings.SCENE_WIDTH, Settings.SCENE_HEIGHT);
// entire game as layers
Pane layerPane = new Pane();
layerPane.getChildren().addAll(playfield);
root.setCenter(layerPane);
scene = new Scene(root, Settings.SCENE_WIDTH, Settings.SCENE_HEIGHT);
primaryStage.setScene(scene);
primaryStage.show();
// add content
prepareGame();
// add mouse location listener
addListeners();
// run animation loop
startGame();
}
private void prepareGame() {
// add vehicles
for( int i = 0; i < Settings.VEHICLE_COUNT; i++) {
addVehicles();
}
// add attractors
for( int i = 0; i < Settings.ATTRACTOR_COUNT; i++) {
addAttractors();
}
}
private void startGame() {
// start game
gameLoop = new AnimationTimer() {
#Override
public void handle(long now) {
// currently we have only 1 attractor
Attractor attractor = allAttractors.get(0);
// seek attractor location, apply force to get towards it
allVehicles.forEach(vehicle -> {
vehicle.seek( attractor.getLocation());
});
// move sprite
allVehicles.forEach(Sprite::move);
// update in fx scene
allVehicles.forEach(Sprite::display);
allAttractors.forEach(Sprite::display);
}
};
gameLoop.start();
}
/**
* Add single vehicle to list of vehicles and to the playfield
*/
private void addVehicles() {
Layer layer = playfield;
// random location
double x = random.nextDouble() * layer.getWidth();
double y = random.nextDouble() * layer.getHeight();
// dimensions
double width = 50;
double height = width / 2.0;
// create vehicle data
Vector2D location = new Vector2D( x,y);
Vector2D velocity = new Vector2D( 0,0);
Vector2D acceleration = new Vector2D( 0,0);
// create sprite and add to layer
Vehicle vehicle = new Vehicle( layer, location, velocity, acceleration, width, height);
// register vehicle
allVehicles.add(vehicle);
}
private void addAttractors() {
Layer layer = playfield;
// center attractor
double x = layer.getWidth() / 2;
double y = layer.getHeight() / 2;
// dimensions
double width = 100;
double height = 100;
// create attractor data
Vector2D location = new Vector2D( x,y);
Vector2D velocity = new Vector2D( 0,0);
Vector2D acceleration = new Vector2D( 0,0);
// create attractor and add to layer
Attractor attractor = new Attractor( layer, location, velocity, acceleration, width, height);
// register sprite
allAttractors.add(attractor);
}
private void addListeners() {
// capture mouse position
scene.addEventFilter(MouseEvent.ANY, e -> {
mouseLocation.set(e.getX(), e.getY());
});
// move attractors via mouse
for( Attractor attractor: allAttractors) {
mouseGestures.makeDraggable(attractor);
}
}
public static void main(String[] args) {
launch(args);
}
}
Then you need a general sprite class in which you accumulate the forces for acceleration, apply acceleration to velocity, velocity to location. Just read the book. It's pretty much straightforward.
package application;
import javafx.scene.Node;
import javafx.scene.layout.Region;
public abstract class Sprite extends Region {
Vector2D location;
Vector2D velocity;
Vector2D acceleration;
double maxForce = Settings.SPRITE_MAX_FORCE;
double maxSpeed = Settings.SPRITE_MAX_SPEED;
Node view;
// view dimensions
double width;
double height;
double centerX;
double centerY;
double radius;
double angle;
Layer layer = null;
public Sprite( Layer layer, Vector2D location, Vector2D velocity, Vector2D acceleration, double width, double height) {
this.layer = layer;
this.location = location;
this.velocity = velocity;
this.acceleration = acceleration;
this.width = width;
this.height = height;
this.centerX = width / 2;
this.centerY = height / 2;
this.view = createView();
setPrefSize(width, height);
// add view to this node
getChildren().add( view);
// add this node to layer
layer.getChildren().add( this);
}
public abstract Node createView();
public void applyForce(Vector2D force) {
acceleration.add(force);
}
public void move() {
// set velocity depending on acceleration
velocity.add(acceleration);
// limit velocity to max speed
velocity.limit(maxSpeed);
// change location depending on velocity
location.add(velocity);
// angle: towards velocity (ie target)
angle = velocity.heading2D();
// clear acceleration
acceleration.multiply(0);
}
/**
* Move sprite towards target
*/
public void seek(Vector2D target) {
Vector2D desired = Vector2D.subtract(target, location);
// The distance is the magnitude of the vector pointing from location to target.
double d = desired.magnitude();
desired.normalize();
// If we are closer than 100 pixels...
if (d < Settings.SPRITE_SLOW_DOWN_DISTANCE) {
// ...set the magnitude according to how close we are.
double m = Utils.map(d, 0, Settings.SPRITE_SLOW_DOWN_DISTANCE, 0, maxSpeed);
desired.multiply(m);
}
// Otherwise, proceed at maximum speed.
else {
desired.multiply(maxSpeed);
}
// The usual steering = desired - velocity
Vector2D steer = Vector2D.subtract(desired, velocity);
steer.limit(maxForce);
applyForce(steer);
}
/**
* Update node position
*/
public void display() {
relocate(location.x - centerX, location.y - centerY);
setRotate(Math.toDegrees( angle));
}
public Vector2D getVelocity() {
return velocity;
}
public Vector2D getLocation() {
return location;
}
public void setLocation( double x, double y) {
location.x = x;
location.y = y;
}
public void setLocationOffset( double x, double y) {
location.x += x;
location.y += y;
}
}
In the demo my sprite is just a triangle, I implemented a utility method to create it.
Vehicle.java
package application;
import javafx.scene.Node;
public class Vehicle extends Sprite {
public Vehicle(Layer layer, Vector2D location, Vector2D velocity, Vector2D acceleration, double width, double height) {
super(layer, location, velocity, acceleration, width, height);
}
#Override
public Node createView() {
return Utils.createArrowImageView( (int) width);
}
}
The demo has an attractor, in your case it'll be just a mouse click. Just click on the circle and drag it. The vehicles will follow it.
package application;
import javafx.scene.Node;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
public class Attractor extends Sprite {
public Attractor(Layer layer, Vector2D location, Vector2D velocity, Vector2D acceleration, double width, double height) {
super(layer, location, velocity, acceleration, width, height);
}
#Override
public Node createView() {
double radius = width / 2;
Circle circle = new Circle( radius);
circle.setCenterX(radius);
circle.setCenterY(radius);
circle.setStroke(Color.GREEN);
circle.setFill(Color.GREEN.deriveColor(1, 1, 1, 0.3));
return circle;
}
}
Here's the code for dragging:
MouseGestures.java
package application;
import javafx.event.EventHandler;
import javafx.scene.input.MouseEvent;
public class MouseGestures {
final DragContext dragContext = new DragContext();
public void makeDraggable(final Sprite sprite) {
sprite.setOnMousePressed(onMousePressedEventHandler);
sprite.setOnMouseDragged(onMouseDraggedEventHandler);
sprite.setOnMouseReleased(onMouseReleasedEventHandler);
}
EventHandler<MouseEvent> onMousePressedEventHandler = new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
dragContext.x = event.getSceneX();
dragContext.y = event.getSceneY();
}
};
EventHandler<MouseEvent> onMouseDraggedEventHandler = new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
Sprite sprite = (Sprite) event.getSource();
double offsetX = event.getSceneX() - dragContext.x;
double offsetY = event.getSceneY() - dragContext.y;
sprite.setLocationOffset(offsetX, offsetY);
dragContext.x = event.getSceneX();
dragContext.y = event.getSceneY();
}
};
EventHandler<MouseEvent> onMouseReleasedEventHandler = new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
}
};
class DragContext {
double x;
double y;
}
}
The playfield layer would be just some race track:
Layer.java
package application;
import javafx.scene.layout.Pane;
public class Layer extends Pane {
public Layer(double width, double height) {
setPrefSize(width, height);
}
}
Then you need some settings class
Settings.java
package application;
public class Settings {
public static double SCENE_WIDTH = 1280;
public static double SCENE_HEIGHT = 720;
public static int ATTRACTOR_COUNT = 1;
public static int VEHICLE_COUNT = 10;
public static double SPRITE_MAX_SPEED = 2;
public static double SPRITE_MAX_FORCE = 0.1;
// distance at which the sprite moves slower towards the target
public static double SPRITE_SLOW_DOWN_DISTANCE = 100;
}
The utility class is for creating the arrow image and for mapping values:
Utils.java
package application;
import javafx.scene.SnapshotParameters;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.image.WritableImage;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.scene.shape.Polygon;
import javafx.scene.shape.StrokeLineCap;
import javafx.scene.shape.StrokeLineJoin;
public class Utils {
public static double map(double value, double currentRangeStart, double currentRangeStop, double targetRangeStart, double targetRangeStop) {
return targetRangeStart + (targetRangeStop - targetRangeStart) * ((value - currentRangeStart) / (currentRangeStop - currentRangeStart));
}
/**
* Create an imageview of a right facing arrow.
* #param size The width. The height is calculated as width / 2.0.
* #param height
* #return
*/
public static ImageView createArrowImageView( double size) {
return createArrowImageView(size, size / 2.0, Color.BLUE, Color.BLUE.deriveColor(1, 1, 1, 0.3), 1);
}
/**
* Create an imageview of a right facing arrow.
* #param width
* #param height
* #return
*/
public static ImageView createArrowImageView( double width, double height, Paint stroke, Paint fill, double strokeWidth) {
return new ImageView( createArrowImage(width, height, stroke, fill, strokeWidth));
}
/**
* Create an image of a right facing arrow.
* #param width
* #param height
* #return
*/
public static Image createArrowImage( double width, double height, Paint stroke, Paint fill, double strokeWidth) {
WritableImage wi;
double arrowWidth = width - strokeWidth * 2;
double arrowHeight = height - strokeWidth * 2;
Polygon arrow = new Polygon( 0, 0, arrowWidth, arrowHeight / 2, 0, arrowHeight); // left/right lines of the arrow
arrow.setStrokeLineJoin(StrokeLineJoin.MITER);
arrow.setStrokeLineCap(StrokeLineCap.SQUARE);
arrow.setStroke(stroke);
arrow.setFill(fill);
arrow.setStrokeWidth(strokeWidth);
SnapshotParameters parameters = new SnapshotParameters();
parameters.setFill(Color.TRANSPARENT);
int imageWidth = (int) width;
int imageHeight = (int) height;
wi = new WritableImage( imageWidth, imageHeight);
arrow.snapshot(parameters, wi);
return wi;
}
}
And of course the class for the vector calculations
Vector2D.java
package application;
public class Vector2D {
public double x;
public double y;
public Vector2D(double x, double y) {
this.x = x;
this.y = y;
}
public void set(double x, double y) {
this.x = x;
this.y = y;
}
public double magnitude() {
return (double) Math.sqrt(x * x + y * y);
}
public void add(Vector2D v) {
x += v.x;
y += v.y;
}
public void add(double x, double y) {
this.x += x;
this.y += y;
}
public void multiply(double n) {
x *= n;
y *= n;
}
public void div(double n) {
x /= n;
y /= n;
}
public void normalize() {
double m = magnitude();
if (m != 0 && m != 1) {
div(m);
}
}
public void limit(double max) {
if (magnitude() > max) {
normalize();
multiply(max);
}
}
static public Vector2D subtract(Vector2D v1, Vector2D v2) {
return new Vector2D(v1.x - v2.x, v1.y - v2.y);
}
public double heading2D() {
return Math.atan2(y, x);
}
}
Here's how it looks like.
The triangles (vehicles) will follow the circles (attractor) and slow down when they get close to it and stop then.