Related
The ask: How do I get the viewing rectangle in the coordinates of a transformed and scaled node?
The code is attached below, it is based upon the code from this answer: JavaFX 8 Dynamic Node scaling
The details:
I have a simple pane, BigGridPane that contains a collection of squares, all 50x50.
I have it within this PanAndZoomPane construct that was lifted from the answer referenced above. I can not honestly say I fully understand the PanAndZoomPane implementation. For example, it's not clear to me why it needs a ScrollPane at all, but I have not delved in to trying without it.
The PanAndZoomPane lets me pan and zoom my BigGridPane. This works just dandy.
There are 4 Panes involved in this total construct, in this heirarchy: ScrollPane contains PanAndZoomPane which contains Group which contains BigGridPane.
ScrollPane
PanAndZoomPane
Group
BigGridPane
I have put listeners on the boundsInLocalProperty and boundsInParentProperty of all of these, and the only one of these that changes while panning and zooming, is the boundsInParentProperty of the PanAndZoomPane. (For some reason I've seen it trigger on the scroll pane, but all of the values are the same, so I don't include that here).
Along with the boundsInParentProperty changes, the translateX, translateY, and myScale properties of the PanAndZoomPane change as things move around. This is expected, of course. myScale is bound to the scaleX and scaleY properties of the PanAndZoomPane.
This is what it looks like at startup.
If I pan the grid as shown, putting 2-2 in the upper left:
We can see the properties of the PanAndZoomPane.
panAndZoom in parent: BoundingBox [minX:-99.5, minY:-99.5, minZ:0.0,
width:501.5, height:501.5, depth:0.0,
maxX:402.0, maxY:402.0, maxZ:0.0]
paz scale = 1.0 - tx: -99.0 - ty: -99.0
Scale is 1 (no zoom), and we've translated ~100x100. That is, the origin of the BigGridPane is at -100,-100. This all makes complete sense. Similarly, the bounding box shows the same thing. The origin is at -100,-100.
In this scenario, I would like to derive a rectangle that shows me what I'm seeing in the window, in the coordinates of the BigGridPane. That would mean a rectangle of
x:100 y:100 width:250 height:250
Normally, I think, this would be the viewport of the ScrollPane, but since this code isn't actually using the ScrollPane for scrolling (again, I'm not quite exactly what it's role is here), the ScrollPane viewport never changes.
I should note that there are shenanigans happening right now because of the retina display on my mac. If you look at the rectangles, showing 5x5, they're 50x50 rectangles, so we should be seeing 10x10, but because of the retina display on my iMac, everything is doubled. What we're seeing in BigGridPane coordinates is a 250x250 block of 5 squares, offset by 100x100. The fact that this is being showing in a window of 500x500 is a detail (but unlikely one we can ignore).
But to reiterate what my question is, that's what I'm trying to get: that 250x250 square at 100x100.
It's odd that it's offset by 100x100 even though the frame is twice as big (500 vs 250). If I pan to where 1-1 is the upper left, the offset is -50,-50, like it should be.
Now, let's add zooming, and pan again to 2-2.
1 click of the scroll wheel and the scale jumps to 1.5.
panAndZoom in parent: BoundingBox [minX:-149.375, minY:-150.375, minZ:0.0,
width:752.25, height:752.25, depth:0.0,
maxX:602.875, maxY:601.875, maxZ:0.0]
paz scale = 1.5 - tx: -23.375 - ty: -24.375
What I want, again, in this case, is a rectangle in BigGridPane coordinates. Roughly:
x:100 y:100 w:150 h:150
We see we're offset by 2x2 boxes (100x100) and we see 3+ boxes (150x150).
So. Back to the bounding box. MinX and minY = -150,-150. This is good. 100 x 1.5 = 150. Similarly the width and height are 750. 500 x 1.5 = 750. So, that is good.
The translates are where we go off the rails. -23.375, -24.375. I have no idea where these numbers come from. I can't seem to correlate them to anything in regards to 100, 150, 1.5 zoom, etc.
Worse, if we pan (while still at 1.5 scale) to "0,0", before, at scale=1, tx and ty were both 0. That's good.
panAndZoom in parent: BoundingBox [minX:0.625, minY:0.625, minZ:0.0,
width:752.25, height:752.25, depth:0.0,
maxX:752.875, maxY:752.875, maxZ:0.0]
paz scale = 1.5 - tx: 126.625 - ty: 126.625
Now, they're 126.625 (probably should be rounded to 125). I have no idea where those numbers come from.
I've tried all sorts of runs on the numbers to see where these numbers come from.
JavaFX knows what the numbers are! (even if the whole retina thing is kind of messing with my head, I'm going to ignore it for the moment).
And I don't see anything in the transforms of any of the panes.
So, my coordinate systems are all over the map, and I'd like to know what part of my BigGridPane is being shown in my panned and scaled view.
Code:
package pkg;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.collections.ObservableList;
import javafx.event.EventHandler;
import javafx.geometry.Bounds;
import javafx.geometry.Point2D;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ScrollEvent;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Region;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Font;
import javafx.scene.text.Text;
import javafx.stage.Stage;
import javafx.util.Duration;
public class PanZoomTest extends Application {
private ScrollPane scrollPane = new ScrollPane();
private final DoubleProperty zoomProperty = new SimpleDoubleProperty(1.0d);
private final DoubleProperty deltaY = new SimpleDoubleProperty(0.0d);
private final Group group = new Group();
PanAndZoomPane panAndZoomPane = null;
BigGridPane1 bigGridPane = new BigGridPane1(10, 10, 50);
#Override
public void start(Stage primaryStage) throws Exception {
scrollPane.setPannable(true);
scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
group.getChildren().add(bigGridPane);
panAndZoomPane = new PanAndZoomPane();
zoomProperty.bind(panAndZoomPane.myScale);
deltaY.bind(panAndZoomPane.deltaY);
panAndZoomPane.getChildren().add(group);
SceneGestures sceneGestures = new SceneGestures(panAndZoomPane);
scrollPane.setContent(panAndZoomPane);
panAndZoomPane.toBack();
addListeners("panAndZoom", panAndZoomPane);
scrollPane.addEventFilter(MouseEvent.MOUSE_PRESSED, sceneGestures.getOnMousePressedEventHandler());
scrollPane.addEventFilter(MouseEvent.MOUSE_DRAGGED, sceneGestures.getOnMouseDraggedEventHandler());
scrollPane.addEventFilter(ScrollEvent.ANY, sceneGestures.getOnScrollEventHandler());
AnchorPane anchorPane = new AnchorPane();
anchorPane.getChildren().add(scrollPane);
anchorPane.setTopAnchor(scrollPane, 1.0d);
anchorPane.setRightAnchor(scrollPane, 1.0d);
anchorPane.setBottomAnchor(scrollPane, 1.0d);
anchorPane.setLeftAnchor(scrollPane, 1.0d);
BorderPane root = new BorderPane(anchorPane);
Label label = new Label("Pan and Zoom Test");
root.setTop(label);
Scene scene = new Scene(root, 250, 250);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
private void addListeners(String label, Node node) {
node.boundsInLocalProperty().addListener((o) -> {
System.out.println(label + " in local: " + node.getBoundsInLocal());
});
node.boundsInParentProperty().addListener((o) -> {
System.out.println(label + " in parent: " + node.getBoundsInParent());
System.out.println("paz scale = " + panAndZoomPane.getScale() + " - "
+ panAndZoomPane.getTranslateX() + " - "
+ panAndZoomPane.getTranslateY());
System.out.println(group.getTransforms());
});
}
class BigGridPane extends Region {
int rows;
int cols;
int size;
Font numFont = Font.font("sans-serif", 8);
FontMetrics numMetrics = new FontMetrics(numFont);
public BigGridPane(int cols, int rows, int size) {
this.rows = rows;
this.cols = cols;
this.size = size;
int sizeX = cols * size;
int sizeY = rows * size;
setMinSize(sizeX, sizeY);
setMaxSize(sizeX, sizeY);
setPrefSize(sizeX, sizeY);
populate();
}
#Override
protected void layoutChildren() {
System.out.println("grid layout");
super.layoutChildren();
}
private void populate() {
ObservableList<Node> children = getChildren();
children.clear();
for (int i = 0; i < cols; i++) {
for (int j = 0; j < rows; j++) {
Rectangle r = new Rectangle(i * size, j * size, size, size);
r.setFill(null);
r.setStroke(Color.BLACK);
String label = i + "-" + j;
Point2D p = new Point2D(r.getBoundsInLocal().getCenterX(), r.getBoundsInLocal().getCenterY());
Text t = new Text(label);
t.setX(p.getX() - numMetrics.computeStringWidth(label) / 2);
t.setY(p.getY() + numMetrics.getLineHeight() / 2);
t.setFont(numFont);
children.add(r);
children.add(t);
}
}
}
}
class PanAndZoomPane extends Pane {
public static final double DEFAULT_DELTA = 1.5d; //1.3d
DoubleProperty myScale = new SimpleDoubleProperty(1.0);
public DoubleProperty deltaY = new SimpleDoubleProperty(0.0);
private Timeline timeline;
public PanAndZoomPane() {
this.timeline = new Timeline(30);//60
// add scale transform
scaleXProperty().bind(myScale);
scaleYProperty().bind(myScale);
}
public double getScale() {
return myScale.get();
}
public void setScale(double scale) {
myScale.set(scale);
}
public void setPivot(double x, double y, double scale) {
// note: pivot value must be untransformed, i. e. without scaling
// timeline that scales and moves the node
timeline.getKeyFrames().clear();
timeline.getKeyFrames().addAll(
new KeyFrame(Duration.millis(200), new KeyValue(translateXProperty(), getTranslateX() - x)), //200
new KeyFrame(Duration.millis(200), new KeyValue(translateYProperty(), getTranslateY() - y)), //200
new KeyFrame(Duration.millis(200), new KeyValue(myScale, scale)) //200
);
timeline.play();
}
public double getDeltaY() {
return deltaY.get();
}
public void setDeltaY(double dY) {
deltaY.set(dY);
}
}
/**
* Mouse drag context used for scene and nodes.
*/
class DragContext {
double mouseAnchorX;
double mouseAnchorY;
double translateAnchorX;
double translateAnchorY;
}
/**
* Listeners for making the scene's canvas draggable and zoomable
*/
public class SceneGestures {
private DragContext sceneDragContext = new DragContext();
PanAndZoomPane panAndZoomPane;
public SceneGestures(PanAndZoomPane canvas) {
this.panAndZoomPane = canvas;
}
public EventHandler<MouseEvent> getOnMousePressedEventHandler() {
return onMousePressedEventHandler;
}
public EventHandler<MouseEvent> getOnMouseDraggedEventHandler() {
return onMouseDraggedEventHandler;
}
public EventHandler<ScrollEvent> getOnScrollEventHandler() {
return onScrollEventHandler;
}
private EventHandler<MouseEvent> onMousePressedEventHandler = new EventHandler<MouseEvent>() {
public void handle(MouseEvent event) {
sceneDragContext.mouseAnchorX = event.getX();
sceneDragContext.mouseAnchorY = event.getY();
sceneDragContext.translateAnchorX = panAndZoomPane.getTranslateX();
sceneDragContext.translateAnchorY = panAndZoomPane.getTranslateY();
}
};
private EventHandler<MouseEvent> onMouseDraggedEventHandler = new EventHandler<MouseEvent>() {
public void handle(MouseEvent event) {
panAndZoomPane.setTranslateX(sceneDragContext.translateAnchorX + event.getX() - sceneDragContext.mouseAnchorX);
panAndZoomPane.setTranslateY(sceneDragContext.translateAnchorY + event.getY() - sceneDragContext.mouseAnchorY);
event.consume();
}
};
/**
* Mouse wheel handler: zoom to pivot point
*/
private EventHandler<ScrollEvent> onScrollEventHandler = new EventHandler<ScrollEvent>() {
#Override
public void handle(ScrollEvent event) {
double delta = PanAndZoomPane.DEFAULT_DELTA;
double scale = panAndZoomPane.getScale(); // currently we only use Y, same value is used for X
double oldScale = scale;
panAndZoomPane.setDeltaY(event.getDeltaY());
if (panAndZoomPane.deltaY.get() < 0) {
scale /= delta;
} else {
scale *= delta;
}
double f = (scale / oldScale) - 1;
double dx = (event.getX() - (panAndZoomPane.getBoundsInParent().getWidth() / 2 + panAndZoomPane.getBoundsInParent().getMinX()));
double dy = (event.getY() - (panAndZoomPane.getBoundsInParent().getHeight() / 2 + panAndZoomPane.getBoundsInParent().getMinY()));
panAndZoomPane.setPivot(f * dx, f * dy, scale);
event.consume();
}
};
}
class FontMetrics {
final private Text internal;
public float lineHeight;
public FontMetrics(Font fnt) {
internal = new Text();
internal.setFont(fnt);
Bounds b = internal.getLayoutBounds();
lineHeight = (float) b.getHeight();
}
public float computeStringWidth(String txt) {
internal.setText(txt);
return (float) internal.getLayoutBounds().getWidth();
}
public float getLineHeight() {
return lineHeight;
}
}
}
Generally, you can get the bounds of node1 in the coordinate system of node2 if both are in the same scene using
node2.sceneToLocal(node1.localToScene(node1.getBoundsInLocal()));
I don't understand all the code you posted; I don't really know why you are using a scroll pane when you seem to be implementing all the panning and zooming yourself. Here is a simpler version of a PanZoomPane and then a test which shows how to use the idea above to get the bounds of the viewport in the coordinate system of the panning/zooming content. The "viewport" is just the bounds of the panning/zooming pane in the coordinate system of the content.
If you need the additional functionality in your version of panning and zooming, you should be able to adapt this idea to that; but it would take me too long to understand everything you are doing there.
import javafx.geometry.Point2D;
import javafx.scene.Node;
import javafx.scene.layout.Region;
import javafx.scene.shape.Rectangle;
import javafx.scene.transform.Affine;
import javafx.scene.transform.Transform;
public class PanZoomPane extends Region {
private final Node content ;
private final Rectangle clip ;
private Affine transform ;
private Point2D mouseDown ;
private static final double SCALE = 1.01 ; // zoom factor per pixel scrolled
public PanZoomPane(Node content) {
this.content = content ;
getChildren().add(content);
clip = new Rectangle();
setClip(clip);
transform = Affine.affine(1, 0, 0, 1, 0, 0);
content.getTransforms().setAll(transform);
content.setOnMousePressed(event -> mouseDown = new Point2D(event.getX(), event.getY()));
content.setOnMouseDragged(event -> {
double deltaX = event.getX() - mouseDown.getX();
double deltaY = event.getY() - mouseDown.getY();
translate(deltaX, deltaY);
});
content.setOnScroll(event -> {
double pivotX = event.getX();
double pivotY = event.getY();
double scale = Math.pow(SCALE, event.getDeltaY());
scale(pivotX, pivotY, scale);
});
}
public Node getContent() {
return content ;
}
#Override
protected void layoutChildren() {
clip.setWidth(getWidth());
clip.setHeight(getHeight());
}
public void scale(double pivotX, double pivotY, double scale) {
transform.append(Transform.scale(scale, scale, pivotX, pivotY));
}
public void translate(double x, double y) {
transform.append(Transform.translate(x, y));
}
public void reset() {
transform.setToIdentity();
}
}
import javafx.application.Application;
import javafx.beans.binding.Binding;
import javafx.beans.binding.ObjectBinding;
import javafx.geometry.Bounds;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.geometry.VPos;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.RowConstraints;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
public class PanZoomTest extends Application {
private Binding<Bounds> viewport ;
#Override
public void start(Stage stage) {
Node content = createContent(50, 50, 50) ;
PanZoomPane pane = new PanZoomPane(content);
viewport = new ObjectBinding<>() {
{
bind(
pane.localToSceneTransformProperty(),
pane.boundsInLocalProperty(),
content.localToSceneTransformProperty()
);
}
#Override
protected Bounds computeValue() {
return content.sceneToLocal(pane.localToScene(pane.getBoundsInLocal()));
}
};
viewport.addListener((obs, oldViewport, newViewport) -> System.out.println(newViewport));
BorderPane root = new BorderPane(pane);
Button reset = new Button("Reset");
reset.setOnAction(event -> pane.reset());
HBox buttons = new HBox(reset);
buttons.setAlignment(Pos.CENTER);
buttons.setPadding(new Insets(10));
root.setTop(buttons);
Scene scene = new Scene(root, 800, 800);
stage.setScene(scene);
stage.show();
}
private Node createContent(int columns, int rows, double cellSize) {
GridPane grid = new GridPane() ;
ColumnConstraints cc = new ColumnConstraints();
cc.setMinWidth(cellSize);
cc.setPrefWidth(cellSize);
cc.setMaxWidth(cellSize);
cc.setFillWidth(true);
cc.setHalignment(HPos.CENTER);
for (int column = 0 ; column < columns ; column++) {
grid.getColumnConstraints().add(cc);
}
RowConstraints rc = new RowConstraints();
rc.setMinHeight(cellSize);
rc.setPrefHeight(cellSize);
rc.setMaxHeight(cellSize);
rc.setFillHeight(true);
rc.setValignment(VPos.CENTER);
for (int row = 0 ; row < rows ; row++) {
grid.getRowConstraints().add(rc);
}
for (int x = 0 ; x < columns ; x++) {
for (int y = 0 ; y < rows ; y++) {
Label label = new Label(String.format("[%d, %d]", x, y));
label.setBackground(new Background(
new BackgroundFill(Color.BLACK, CornerRadii.EMPTY, Insets.EMPTY),
new BackgroundFill(Color.WHITE, CornerRadii.EMPTY, new Insets(1,1,0,0))
));
label.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);
grid.add(label, x, y);
}
}
return grid ;
}
public static void main(String[] args) {
launch();
}
}
I have the goal to create a field of hexagonal tiles. I have come as far as having a matrix of cells, each high enough to fit the complete hexagon image:
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.GridPane;
import javafx.stage.Stage;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
public class UITest extends Application {
final private static String TILE_IMAGE_LOCATION = System.getProperty("user.dir") + File.separatorChar +"resources"+ File.separatorChar + "blueTile.png";
final private static Image HEXAGON_IMAGE = initTileImage();
private static Image initTileImage() {
try {
return new Image(new FileInputStream(new File(TILE_IMAGE_LOCATION)));
} catch (FileNotFoundException e) {
throw new IllegalStateException(e);
}
}
public void start(Stage primaryStage) {
int height = 4;
int width = 6;
GridPane tileMap = new GridPane();
Scene content = new Scene(tileMap, 800, 600);
primaryStage.setScene(content);
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
ImageView tile = new ImageView(HEXAGON_IMAGE);
GridPane.setConstraints(tile, x, y);
tileMap.getChildren().add(tile);
}
}
primaryStage.show();
}
}
My problem is not the vertical gap, which I can surely figure out by adding the GridPane's vGap() to a proper value. The difficulty for me is shifting each second row half a cellwidth to the right.
I have attempted to lay two GridPanes over eachother, one containing the odd and one the even rows, with the goal to add padding to one of them, shifting it entirely. To my knowledge however, there is no way for this, as well as nesting GridPanes into on another.
How can I best achieve the shifting of only every second row?
(The image I reference in the code which is expected in the ${projectroot}/resources/ folder: )
It took me some time to figure it out. I hope it helps. I don't use an image. It's made of polygons, you can customize the stroke and fill color, as well as the width.
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.stage.Stage;
import javafx.scene.layout.AnchorPane;
import javafx.scene.paint.Paint;
import javafx.scene.shape.Polygon;
public class UITest extends Application {
public void start(Stage primaryStage) {
int height = 600;
int width = 800;
AnchorPane tileMap = new AnchorPane();
Scene content = new Scene(tileMap, width, height);
primaryStage.setScene(content);
double size = 50,v=Math.sqrt(3)/2.0;
for(double y=0;y<height;y+=size*Math.sqrt(3))
{
for(double x=-25,dy=y;x<width;x+=(3.0/2.0)*size)
{
Polygon tile = new Polygon();
tile.getPoints().addAll(new Double[]{
x,dy,
x+size,dy,
x+size*(3.0/2.0),dy+size*v,
x+size,dy+size*Math.sqrt(3),
x,dy+size*Math.sqrt(3),
x-(size/2.0),dy+size*v
});
tile.setFill(Paint.valueOf("#ffffff"));
tile.setStrokeWidth(2);
tile.setStroke(Paint.valueOf("#000000") );
tileMap.getChildren().add(tile);
dy = dy==y ? dy+size*v : y;
}
}
primaryStage.show();
}
public static void main(String[] args)
{
launch(args);
}
}
For other interested souls out there, I have used the accepted answer by Cthulhu and improved/documented the given code as a short standalone demonstration:
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.AnchorPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Polygon;
import javafx.stage.Stage;
public class UISolution extends Application {
private final static int WINDOW_WIDTH = 800;
private final static int WINDOW_HEIGHT = 600;
private final static double r = 20; // the inner radius from hexagon center to outer corner
private final static double n = Math.sqrt(r * r * 0.75); // the inner radius from hexagon center to middle of the axis
private final static double TILE_HEIGHT = 2 * r;
private final static double TILE_WIDTH = 2 * n;
public static void main(String[] args) {
launch(args);
}
public void start(Stage primaryStage) {
AnchorPane tileMap = new AnchorPane();
Scene content = new Scene(tileMap, WINDOW_WIDTH, WINDOW_HEIGHT);
primaryStage.setScene(content);
int rowCount = 4; // how many rows of tiles should be created
int tilesPerRow = 6; // the amount of tiles that are contained in each row
int xStartOffset = 40; // offsets the entire field to the right
int yStartOffset = 40; // offsets the entire fiels downwards
for (int x = 0; x < tilesPerRow; x++) {
for (int y = 0; y < rowCount; y++) {
double xCoord = x * TILE_WIDTH + (y % 2) * n + xStartOffset;
double yCoord = y * TILE_HEIGHT * 0.75 + yStartOffset;
Polygon tile = new Tile(xCoord, yCoord);
tileMap.getChildren().add(tile);
}
}
primaryStage.show();
}
private class Tile extends Polygon {
Tile(double x, double y) {
// creates the polygon using the corner coordinates
getPoints().addAll(
x, y,
x, y + r,
x + n, y + r * 1.5,
x + TILE_WIDTH, y + r,
x + TILE_WIDTH, y,
x + n, y - r * 0.5
);
// set up the visuals and a click listener for the tile
setFill(Color.ANTIQUEWHITE);
setStrokeWidth(1);
setStroke(Color.BLACK);
setOnMouseClicked(e -> System.out.println("Clicked: " + this));
}
}
}
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.
Given 2D scene with a node inside a group which contains a 2d rotate transformation. How do I position the node inside the group to the scene x and y coordinates of the mouse upon click?
The node that I am trying to move to the position of the click event is a circle which is located inside a group that has been rotated. The rotation happens at a pivot at the upper right corner of the group. The group has other nodes in it too.
I have been fiddling trying to achieve this for a while with no luck. It just does not position the node at the place where the click happened if the parent of the node is rotated. I have tried various techniques including the localToScene bounds with no luck.
Is there a way to do this? Thank you for your time =)
Here is some code showing a minimum verifiable example of the problem. Run it for a demo
You can drag the circle and select circles with mouse clicks. Do this to see it works fine as long as the group is not rotated.
In order to rotate the group use the left and right direction keys on your keyboard. After the group has been rotated the dragging and the mouse coordinates are no longer accurate!
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import javafx.animation.FadeTransition;
import javafx.animation.ParallelTransition;
import javafx.animation.ScaleTransition;
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Rectangle;
import javafx.scene.transform.Rotate;
import javafx.stage.Stage;
import javafx.util.Duration;
public class DemoBounds extends Application {
private static final int WIDTH = 600;
private static final int HEIGHT = 700;
private static final int CIRCLE_COUNT = 12;
private static final int RECTANGLE_COUNT = 3;
private static final int CIRCLE_DISTANCE = 150;
private static final int RECTANGLE_DISTANCE = 20;
private Color selectedColor = Color.RED;
private Color normalColor = Color.YELLOW;
private Rotate rotator = new Rotate();
private List<Circle> circles = new ArrayList<>();
private List<Rectangle> rectangles = new ArrayList<>();
public static void main(String[] args) {
Application.launch(args);
}
#Override
public void start(Stage stage) {
Rotate rotate = new Rotate();
Group root = new Group();
Pane pane = new Pane(root);
createRectangles();
createCircles();
root.getChildren().addAll(rectangles);
root.getChildren().addAll(circles);
root.getTransforms().add(rotate);
Scene scene = new Scene(pane, WIDTH, HEIGHT, Color.BLACK);
AddRotateControls(root);
assignActionHandling(pane);
stage.sizeToScene();
stage.setScene(scene);
stage.setTitle("Example");
stage.show();
}
private void AddRotateControls(Group root) {
root.getTransforms().add(rotator);
rotator.setPivotX(150);
rotator.setPivotY(150);
rotator.setAngle(0);
root.getScene().setOnKeyPressed(e -> {
switch(e.getCode()){
case RIGHT:
rotator.setAngle(rotator.getAngle() + 1);
break;
case LEFT:
rotator.setAngle(rotator.getAngle() - 1);
break;
default:
break;
}
});
}
private void assignActionHandling(Pane pane) {
pane.setOnMousePressed(e -> {
Circle circle = new Circle(e.getSceneX(), e.getSceneY(), 1, Color.DEEPSKYBLUE);
pane.getChildren().add(circle);
Duration duration = Duration.millis(350);
ScaleTransition scale = new ScaleTransition(duration, circle);
FadeTransition fade = new FadeTransition(duration, circle);
ParallelTransition pack = new ParallelTransition(circle, scale, fade);
scale.setFromX(1);
scale.setFromY(1);
scale.setToX(20);
scale.setToY(20);
fade.setFromValue(1);
fade.setToValue(0);
pack.setOnFinished(e2 -> {
pane.getChildren().remove(circle);
});
pack.play();
Circle selected = circles.stream().filter(c -> ((CircleData) c.getUserData()).isSelected()).findFirst().orElse(null);
if (selected != null) {
selected.setCenterX(e.getSceneX());
selected.setCenterY(e.getSceneY());
}
});
}
private void createRectangles() {
int width = 100;
int height = HEIGHT / 3;
int startX = ((WIDTH / 2) - (((width / 2) * 3) + (RECTANGLE_DISTANCE * 3))) + (RECTANGLE_DISTANCE * 2);
int startY = (HEIGHT / 2) - (height / 2);
for(int i = 0; i<RECTANGLE_COUNT; i++){
Rectangle rect = new Rectangle();
rect.setFill(Color.MEDIUMTURQUOISE);
rect.setWidth(width);
rect.setHeight(height);
rect.setX(startX);
rect.setY(startY);
rectangles.add(rect);
startX += (width + RECTANGLE_DISTANCE);
}
}
private void createCircles() {
Random randon = new Random();
int centerX = WIDTH / 2;
int centerY = HEIGHT / 2;
int minX = centerX - CIRCLE_DISTANCE;
int maxX = centerX + CIRCLE_DISTANCE;
int minY = centerY - CIRCLE_DISTANCE;
int maxY = centerY + CIRCLE_DISTANCE;
int minRadius = 10;
int maxRadius = 50;
for (int i = 0; i < CIRCLE_COUNT; i++) {
int x = minX + randon.nextInt(maxX - minX + 1);
int y = minY + randon.nextInt(maxY - minY + 1);
int radius = minRadius + randon.nextInt(maxRadius - minRadius + 1);
Circle circle = new Circle(x, y, radius, Color.ORANGE);
circle.setStroke(normalColor);
circle.setStrokeWidth(5);
circle.setUserData(new CircleData(circle, i, false));
circles.add(circle);
}
assignCircleActionHandling();
}
private double mouseX;
private double mouseY;
private void assignCircleActionHandling() {
for (Circle circle : circles) {
circle.setOnMousePressed(e -> {
mouseX = e.getSceneX() - circle.getCenterX();
mouseY = e.getSceneY() - circle.getCenterY();
((CircleData) circle.getUserData()).setSelected(true);
unselectRest(((CircleData) circle.getUserData()).getId());
});
circle.setOnMouseDragged(e -> {
double deltaX = e.getSceneX() - mouseX;
double deltaY = e.getSceneY() - mouseY;
circle.setCenterX(deltaX);
circle.setCenterY(deltaY);
});
circle.setOnMouseReleased(e -> {
e.consume();
});
}
}
private void unselectRest(int current) {
circles.stream().filter(c -> ((CircleData) c.getUserData()).getId() != current).forEach(c -> {
((CircleData) c.getUserData()).setSelected(false);
});
}
public class CircleData {
private int id;
private boolean selected;
private Circle circle;
public CircleData(Circle circle, int id, boolean selected) {
super();
this.id = id;
this.circle = circle;
this.selected = selected;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public boolean isSelected() {
return selected;
}
public void setSelected(boolean selected) {
this.selected = selected;
if (selected) {
circle.setStroke(selectedColor);
} else {
circle.setStroke(normalColor);
}
}
}
}
You don't give the details of your code but there may be a problem with the pivot of your rotation. This can drive you nuts if you try to understand the rotation behaviour in some cases if you are not aware of this mechanism. Every time when you move some nodes which are attached to your group, this pivot for the rotation is recomputed which can result in unwanted effects although in some cases it is just what you want.
If you want to have full control of your rotation you should use some code similar to the one described here: http://docs.oracle.com/javafx/8/3d_graphics/overview.htm
Update:
In your method assignActionHandling modify these few lines. In order for this to work you somehow have to make root available there.
if (selected != null) {
Point2D p = root.sceneToLocal(e.getSceneX(), e.getSceneY());
selected.setCenterX(p.getX());
selected.setCenterY(p.getY());
}
The reason for you problem is that you are mixing up coordinate systems. The center points of your circles are defined relative to the root coordinate system but that is rotated with respect to pane as well as the scene. So you have to transform the scene coordinates into the local root coordinates before you set the new center of the circle.
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.