I am trying to make a snake game using maven project on JavaFX. In my code snake was moving but the previous coordinates are not cleared on the scene. Are there any solution for this? Here is my code:
public class Main extends Application {
int sceneX = 600;
int sceneY = 400;
ArrayList <Point> snake = new ArrayList<>();
private GraphicsContext gc;
private void run(GraphicsContext gc) {
drawSnake(gc);
for (int i = snake.size()-1; i>=1; i--) {
snake.get(i).x = snake.get(i - 1).x;
snake.get(i).y = snake.get(i - 1).y;
}
snake.get(0).x++;
}
private void drawSnake(GraphicsContext gc) {
for (int i =0; i<3; i++) {
gc.fillRoundRect(snake.get(i).getX(), snake.get(i).getY(),5,5,3,3);
}
}
#Override
public void start(Stage primaryStage) {
primaryStage.setTitle("SNAKE");
Group root = new Group();
Canvas canvas = new Canvas(sceneX, sceneY);
root.getChildren().add(canvas);
Scene scene = new Scene (root);
primaryStage.setScene(scene);
primaryStage.show();
gc = canvas.getGraphicsContext2D();
for (int i = 0; i<3; i++) {
snake.add(new Point(300, 300));
}
Timeline timeline = new Timeline(new KeyFrame(Duration.millis(130), e -> run(gc)));
timeline.setCycleCount(Animation.INDEFINITE);
timeline.play();
}
I agree with what #jewelsea mentioned in the comments.
If you don't have very complex drawings/painting on the canvas, I think using scenegraph approach should make things simple.
If you opt for canvas, one possible drawback I can think of is : you need to redraw the entire canvas. Because clearing only part of the rectangle will not be a solution if you have some layered drawings (like background.. etc)
private void run(GraphicsContext gc) {
for (int i = 0; i < snake.size(); i++) {
gc.clearRect(snake.get(i).x, snake.get(i).y, size, size);
}
int lastIndex = snake.size() - 1;
for (int i = 0; i < lastIndex; i++) {
snake.get(i).x = snake.get(i + 1).x;
}
snake.get(lastIndex).x += size;
drawSnake(gc);
}
In the below gif for canvas approach, clearing the snake alone is not sufficient if I have some background.
On the other hand, if I use scenegraph approach, once we include the building blocks of the snake, all we need to do is to update the position of the building blocks.
private void run() {
int lastIndex = shapes.size() - 1;
for (int i = 0; i < lastIndex; i++) {
shapes.get(i).setLayoutX(shapes.get(i + 1).getLayoutX());
}
shapes.get(lastIndex).setLayoutX(shapes.get(lastIndex).getLayoutX() + size);
}
Below is the complete demo differentiating the two approaches:
import javafx.animation.Animation;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.Label;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import javafx.util.Duration;
import java.awt.Point;
import java.util.ArrayList;
import java.util.List;
public class SnakeGameDemo extends Application {
int width = 300;
int height = 200;
int startX = 100;
int startY = 100;
int size = 5;
int speed = 250;
List<Point> snake = new ArrayList<>();
List<Rectangle> shapes = new ArrayList<>();
#Override
public void start(Stage primaryStage) {
primaryStage.setTitle("SNAKE");
GridPane root = new GridPane();
root.setPadding(new Insets(10));
root.setHgap(10);
root.setVgap(10);
root.addRow(0, new Label("Canvas"), new Label("SceneGraph"));
root.addRow(1, canvasApproach(), scenegraphApproach());
Scene scene = new Scene(root);
primaryStage.setScene(scene);
primaryStage.show();
}
private Node canvasApproach() {
Canvas canvas = new Canvas(width, height);
GraphicsContext gc = canvas.getGraphicsContext2D();
gc.setFill(Color.RED);
gc.fillRoundRect(0, 0, width, height, 0, 0);
gc.setFill(Color.GREEN);
for (int i = 0; i < 3; i++) {
snake.add(new Point(startX + (i * size), startY));
}
Timeline timeline = new Timeline(new KeyFrame(Duration.millis(speed), e -> run(gc)));
timeline.setCycleCount(Animation.INDEFINITE);
timeline.play();
return canvas;
}
private void run(GraphicsContext gc) {
for (int i = 0; i < snake.size(); i++) {
gc.clearRect(snake.get(i).x, snake.get(i).y, size, size);
}
int lastIndex = snake.size() - 1;
for (int i = 0; i < lastIndex; i++) {
snake.get(i).x = snake.get(i + 1).x;
}
snake.get(lastIndex).x += size;
drawSnake(gc);
}
private void drawSnake(GraphicsContext gc) {
snake.forEach(block -> {
gc.fillRoundRect(block.getX(), block.getY(), size, size, 3, 3);
});
}
private Node scenegraphApproach() {
Pane pane = new Pane();
pane.setStyle("-fx-background-color:red;");
pane.setPrefSize(width, height);
for (int i = 0; i < 3; i++) {
Rectangle block = new Rectangle(size, size, Color.GREEN);
// Position the shapes in the Pane using the layoutX/Y properties.
block.setLayoutX(startX + (i * size));
block.setLayoutY(startY);
pane.getChildren().add(block);
shapes.add(block);
}
Timeline timeline = new Timeline(new KeyFrame(Duration.millis(speed), e -> run()));
timeline.setCycleCount(Animation.INDEFINITE);
timeline.play();
return pane;
}
private void run() {
int lastIndex = shapes.size() - 1;
for (int i = 0; i < lastIndex; i++) {
shapes.get(i).setLayoutX(shapes.get(i + 1).getLayoutX());
}
shapes.get(lastIndex).setLayoutX(shapes.get(lastIndex).getLayoutX() + size);
}
}
Related
package com.example.project;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.ComboBox;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Region;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import java.net.URL;
import java.util.ResourceBundle;
public class Scene2Controller implements Initializable {
#FXML
private ComboBox boardSizeCombo;
#FXML
private GridPane grid;
#FXML
private ComboBox colourCombo;
#Override
public void initialize(URL url, ResourceBundle rb) {
colourCombo.getItems().add("Red and White");
colourCombo.getItems().add("Orange and White");
boardSizeCombo.getItems().add("9x9");
boardSizeCombo.getItems().add("10x10");
boardSizeCombo.setOnAction((actionEvent ->{
int selectedIndex = boardSizeCombo.getSelectionModel().getSelectedIndex();
if (selectedIndex == 0){
int count = 0;
double s = 100; // side of rectangle
for (int i = 0; i < 8; i++) {
count++;
for (int j = 0; j < 8; j++) {
Rectangle r = new Rectangle(s, s, s, s);
if (count % 2 == 0)
r.setFill(Color.WHITE);
grid.add(r, j, i);
count++;
}
}
}
if (selectedIndex == 1){
int count = 0;
double s = 100; // side of rectangle
for (int i = 0; i < 8; i++) {
count++;
for (int j = 0; j < 8; j++) {
Rectangle r = new Rectangle(s, s, s, s);
if (count % 2 == 0)
r.setFill(Color.BLUE);
grid.add(r, j, i);
count++;
}
}
}
}
));
}
}
The user selects a option from the comboBox. In this case, the user selects a size for the board and then whatever size the user has selected, the outcome will be displayed on the gridpane. However, it is doing what is screenshotted. Can someone help with the constraints please. Thanks
You need to fix your loops.
E.g. for a 9x9 grid
for (int i = 0; i < 8; i++) {
That loops from 0 to 7 ... a total of 8 iterations, not 9. For the 10x10 grid you seem to have the same loop.
Here is a basic solution:
import javafx.scene.control.ComboBox;
import javafx.scene.layout.GridPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.application.Application;
import javafx.event.Event;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class GridExample extends Application {
private ComboBox boardSizeCombo;
private GridPane grid;
private ComboBox colourCombo;
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) throws Exception {
grid = new GridPane();
colourCombo = new ComboBox();
boardSizeCombo = new ComboBox();
colourCombo.getItems().add("Red and White");
colourCombo.getItems().add("Orange and White");
colourCombo.getSelectionModel().select(0);
colourCombo.setOnAction(this::createBoard);
boardSizeCombo.getItems().add("9x9");
boardSizeCombo.getItems().add("10x10");
boardSizeCombo.getSelectionModel().select(0);
boardSizeCombo.setOnAction(this::createBoard);
createBoard(null);
HBox topPane = new HBox(new Label("Try it..."));
VBox sidePane = new VBox(8, colourCombo, boardSizeCombo);
BorderPane root = new BorderPane(grid, topPane, null, null, sidePane);
root.setPadding(new Insets(8));
Scene scene = new Scene(root);
primaryStage.setScene(scene);
primaryStage.setMinWidth(700);
primaryStage.setMinHeight(600);
primaryStage.show();
}
void createBoard(Event e) {
int colourIndex = colourCombo.getSelectionModel().getSelectedIndex();
int sizeIndex = boardSizeCombo.getSelectionModel().getSelectedIndex();
int n = sizeIndex == 0 ? 9 : 10;
Color baseColour = colourIndex == 0 ? Color.RED : Color.ORANGE;
grid.getChildren().clear();
double s = 50; // side of rectangle
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
Rectangle r = new Rectangle(s, s, s, s);
r.setFill((((i ^ j) & 1) == 0) ? Color.WHITE : baseColour);
grid.add(r, j, i);
}
}
}
}
Does anyone know why transparency drawing on a Canvas works perfectly fine using drawImage(), but doesn't work at all with a PixelWriter? I initially thought it may have something to do with Blend or some other mode/setting on the canvas/context, but haven't had any luck with that yet.
I need per-pixel variable transparency, not a single transparency value for the entire draw operation. I'll be rendering a number of "layers" (similar to how GIMP layers work, with optional transparency per-pixel). An additional open question is whether I'm better off first drawing the FINAL intended output to a WritableImage and then just drawing to the Canvas, for performance reasons, but that seems to defeat the point of using a Canvas in the first place...
Below is an example which shows a partially transparent Color being first drawn to an Image and then to the Canvas, and directly to the Canvas with setColor(). The transparent area is the Image draw, the opaque area is the setColor part. How do we get setColor() to respect Color alpha transparency for each pixel?
import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.image.WritableImage;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.stage.Stage;
public class TransparencyTest extends Application {
private static final int width = 800;
private static final int height = 600;
private Scene scene;
private final Canvas canvas = new Canvas(width, height);
#Override
public void start(Stage stage) {
scene = new Scene(new Group(canvas));
stage.setScene(scene);
render();
stage.show();
exitOnEsc();
}
private void render() {
drawTransparentBg(canvas, 0, 0, width, height);
Color color = Color.web("#77000077");
WritableImage image = new WritableImage(200, 200);
for (int x = 0; x < 200; x++) {
for (int y = 0; y < 200; y++) {
image.getPixelWriter().setColor(x, y, color);
}
}
canvas.getGraphicsContext2D().drawImage(image, 50, 50);
for (int x = 0; x < 50; x++) {
for (int y = 0; y < 50; y++) {
canvas.getGraphicsContext2D().getPixelWriter().setColor(x, y, color);
}
}
}
public void drawTransparentBg(Canvas canvas, int xPos, int yPos, int width, int height) {
int gridSize = 8;
boolean darkX = true;
String darkCol = "#111111";
String lightCol = "#222266";
for (int x = xPos; x < canvas.getWidth(); x += gridSize) {
boolean dark = darkX;
darkX = !darkX;
if (x > width) {
break;
}
for (int y = yPos; y < canvas.getHeight(); y += gridSize) {
if (y > height) {
break;
}
dark = !dark;
String color;
if (dark) {
color = darkCol;
} else {
color = lightCol;
}
canvas.getGraphicsContext2D().setFill(Paint.valueOf(color));
canvas.getGraphicsContext2D().fillRect(x, y, gridSize, gridSize);
}
}
}
private void exitOnEsc() {
scene.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
if (event.getCode().equals(KeyCode.ESCAPE)) {
Platform.exit();
}
});
}
}
The GraphicsContext begins with the default BlendMode, and all forms of drawImage() use the current mode. In contrast, PixelWriter methods replace values, ignoring the BlendMode.
The example below lets you experiment with the supported BlendMode values to see the effect. Related examples are shown here and here.
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.ChoiceBox;
import javafx.scene.effect.BlendMode;
import javafx.scene.image.WritableImage;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
public class TransparencyTest extends Application {
private static final int S = 8;
private static final int W = S * 36;
private static final int H = W;
private final Canvas canvas = new Canvas(W, H);
private final GraphicsContext g = canvas.getGraphicsContext2D();
private BlendMode mode = g.getGlobalBlendMode();
#Override
public void start(Stage stage) {
render(mode);
BorderPane root = new BorderPane(new Pane(canvas));
ObservableList<BlendMode> modes
= FXCollections.observableArrayList(BlendMode.values());
ChoiceBox<BlendMode> cb = new ChoiceBox<>(modes);
cb.setValue(mode);
cb.valueProperty().addListener((o) -> {
render(cb.getValue());
});
root.setBottom(cb);
stage.setScene(new Scene(root));
stage.show();
}
private void render(BlendMode mode) {
drawBackground();
g.setGlobalBlendMode(mode);
Color color = Color.web("#7f00007f");
int s = 24 * 8;
WritableImage image = new WritableImage(s, s);
for (int x = 0; x < s; x++) {
for (int y = 0; y < s; y++) {
image.getPixelWriter().setColor(x, y, color);
}
}
s = 6 * 8;
g.drawImage(image, s, s);
for (int x = 0; x < s; x++) {
for (int y = 0; y < s; y++) {
g.getPixelWriter().setColor(x, y, color);
}
}
}
public void drawBackground() {
g.setGlobalBlendMode(BlendMode.SRC_OVER);
Color darkCol = Color.web("#333333");
Color lightCol = Color.web("#cccccc");
boolean dark = false;
for (int x = 0; x < W; x += S) {
dark = !dark;
for (int y = 0; y < H; y += S) {
dark = !dark;
g.setFill(dark ? darkCol : lightCol);
g.fillRect(x, y, S, S);
}
}
}
public static void main(String[] args) {
launch(args);
}
}
i wanna create pyramid left,right,center angle, using Circle shape in GridPane, i have already done with the left angle, but spent 2 days in creating right and center angle with no luck.
i will be thankful if anyone knows or can give me some algo ideas please help me!
Output already done left angle
With the following code
int i, j;
for (i = 0; i < n; i++) {
for (j = 0; j <= i; j++) {
Circle circle = new Circle();
circle.setStroke(Paint.valueOf(Color.BLACK.toString()));
circle.radiusProperty().bind(ballsModel.radiusProperty());
circle.strokeWidthProperty().bind(ballsModel.strokeProperty());
circle.fillProperty().bind(Bindings.createObjectBinding(() -> Paint.valueOf(ballsModel.getColor().name())));
grid.addRow(i, circle);
}
}
Need to figure out following patterns:
center angle
right angle
an example for the pyramid part
package pyramid;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.StackPane;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;
public class Pyramid extends Application {
void createPyramid(){
gridPane.setHgap(5);
gridPane.setVgap(5);
int center = 4 ;
for (int row = 0; row <= 4; row++ ) {
int range = (1 +(2* row));
int startColumn = center-range/2 ;
for(int i = 0 ; i<range; i++){
Circle circle = new Circle(20,javafx.scene.paint.Color.BLUE);
gridPane.add(circle,startColumn+i , row);
}
}}
#Override
public void start(Stage primaryStage) {
StackPane root = new StackPane();
root.getChildren().add(gridPane);
this.createPyramid();
Scene scene = new Scene(root, 600, 600);
primaryStage.setTitle("Pyramid");
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}}
Trying to add children to a GridPane with row and column indices. They show up in the gui, but using GridPane.getColumnIndex(node) always returns null.
GridPane grid = new GridPane();
for (int i = 0; i < 20; i++) {
for (int j = 0; j < 20; j++) {
grid.setGridLinesVisible(true);
Button myBtn = new Button("foo");
GridPane.setColumnIndex(myButton,i);
GridPane.setRowIndex(myButton,j);
grid.getChildren().add(myButton);
}
When I try to use this algorithm I got from the answer on javafx GridPane retrive specific Cell content, the program crashes with a NullPointerException.
private Node getNodeFromGridPane(GridPane gridPane, int col, int row)
{
for (Node node : gridPane.getChildren()) {
if (GridPane.getColumnIndex(node) == col && GridPane.getRowIndex(node) == row) {
return node;
}
}
return null;
}
Why are GridPane.getRowIndex() and GridPane.getColumnIndex() returning null even after calling setColumnIndex() and setRowIndex()?
So it seems I got it to work if you run it you will see that all the buttons are red white or blue this is because they were found
import javafx.application.Application;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.GridPane;
import javafx.stage.Stage;
public class Main extends Application {
#Override
public void start(Stage primaryStage) {
GridPane grid = new GridPane();
grid.setGridLinesVisible(true);
for (int i = 0; i < 20; i++) {
for (int j = 0; j < 20; j++) {
grid.add(new Button("foo"),i,j);
// Button myBtn = new Button("foo");
// GridPane.setColumnIndex(myBtn, i);
// GridPane.setRowIndex(myBtn, j);
// grid.getChildren().add(myBtn);
}
}
Scene scene = new Scene(grid);
Stage stage = new Stage();
stage.setScene(scene);
stage.show();
String[] colors = {"-fx-background-color: red;", "-fx-background-color: blue;", "-fx-background-color: white;"};
for (int i = 0; i < 20; i++) {
for (int j = 0; j < 20; j++) {
Node node = getNodeFromGridPane(grid,i,j);
if(node instanceof Button)
node.setStyle(colors[(int) (Math.random() * 3)]);
}
}
}
private Node getNodeFromGridPane(GridPane gridPane, int col, int row) {
for (Node node : gridPane.getChildren())
if (GridPane.getColumnIndex(node) != null
&& GridPane.getColumnIndex(node) != null
&& GridPane.getRowIndex(node) == row
&& GridPane.getColumnIndex(node) == col)
return node;
return null;
}
public static void main(String[] args) { launch(args); }
}
I have tried the this example given in another post to learn about zooming and panning relative to the mouse pointer. When everything is on the grid, zooming works as expected:
When zooming into the mouse pointer location on the top left image, it is zoomed into the exact location as seen in the top right image.
If something is dragged off the grid, e.g. the pivot starts to 'misbehave':
When zooming into the mouse pointer location on the bottom left image, it is zoomed into a location other than the one intended, seen in the bottom right image.
The bounds of the canvas inside the parent changes from 600x600 (without scale) to something like 600x700… Which affects the outcomes dx, dy of the following function.
double dx = (event.getSceneX() - (canvas.getBoundsInParent().getWidth()/2 + canvas.getBoundsInParent().getMinX()));
double dy = (event.getSceneY() - (canvas.getBoundsInParent().getHeight()/2 + canvas.getBoundsInParent().getMinY()));
When editing this function by changing .getWidth() to .getHeight() and then again move the rectangle out right… the zoom works correctly. However, if the rectangle is moved out vertically (to the bottom or top) and to the left the problem again is reproduced again.
Is the above function correct, what is it trying to do? Why does the zoom not work the same, as when everything was on the grid?
import javafx.application.Application;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.event.EventHandler;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.Label;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ScrollEvent;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
class PannableCanvas extends Pane {
DoubleProperty myScale = new SimpleDoubleProperty(1.0);
public PannableCanvas() {
setPrefSize(600, 600);
setStyle("-fx-background-color: lightgrey; -fx-border-color: blue;");
// add scale transform
scaleXProperty().bind(myScale);
scaleYProperty().bind(myScale);
}
/**
* Add a grid to the canvas, send it to back
*/
public void addGrid() {
double w = getBoundsInLocal().getWidth();
double h = getBoundsInLocal().getHeight();
// add grid
Canvas grid = new Canvas(w, h);
// don't catch mouse events
grid.setMouseTransparent(true);
GraphicsContext gc = grid.getGraphicsContext2D();
gc.setStroke(Color.GRAY);
gc.setLineWidth(1);
// draw grid lines
double offset = 50;
for( double i=offset; i < w; i+=offset) {
gc.strokeLine( i, 0, i, h);
gc.strokeLine( 0, i, w, i);
}
getChildren().add( grid);
grid.toBack();
}
public double getScale() {
return myScale.get();
}
public void setScale( double scale) {
myScale.set(scale);
}
public void setPivot( double x, double y) {
setTranslateX(getTranslateX()-x);
setTranslateY(getTranslateY()-y);
}
}
/**
* Mouse drag context used for scene and nodes.
*/
class DragContext {
double mouseAnchorX;
double mouseAnchorY;
double translateAnchorX;
double translateAnchorY;
}
/**
* Listeners for making the nodes draggable via left mouse button. Considers if parent is zoomed.
*/
class NodeGestures {
private DragContext nodeDragContext = new DragContext();
PannableCanvas canvas;
public NodeGestures( PannableCanvas canvas) {
this.canvas = canvas;
}
public EventHandler<MouseEvent> getOnMousePressedEventHandler() {
return onMousePressedEventHandler;
}
public EventHandler<MouseEvent> getOnMouseDraggedEventHandler() {
return onMouseDraggedEventHandler;
}
private EventHandler<MouseEvent> onMousePressedEventHandler = new EventHandler<MouseEvent>() {
public void handle(MouseEvent event) {
// left mouse button => dragging
if( !event.isPrimaryButtonDown())
return;
nodeDragContext.mouseAnchorX = event.getSceneX();
nodeDragContext.mouseAnchorY = event.getSceneY();
Node node = (Node) event.getSource();
nodeDragContext.translateAnchorX = node.getTranslateX();
nodeDragContext.translateAnchorY = node.getTranslateY();
}
};
private EventHandler<MouseEvent> onMouseDraggedEventHandler = new EventHandler<MouseEvent>() {
public void handle(MouseEvent event) {
// left mouse button => dragging
if( !event.isPrimaryButtonDown())
return;
double scale = canvas.getScale();
Node node = (Node) event.getSource();
node.setTranslateX(nodeDragContext.translateAnchorX + (( event.getSceneX() - nodeDragContext.mouseAnchorX) / scale));
node.setTranslateY(nodeDragContext.translateAnchorY + (( event.getSceneY() - nodeDragContext.mouseAnchorY) / scale));
event.consume();
}
};
}
/**
* Listeners for making the scene's canvas draggable and zoomable
*/
class SceneGestures {
private static final double MAX_SCALE = 10.0d;
private static final double MIN_SCALE = .1d;
private DragContext sceneDragContext = new DragContext();
PannableCanvas canvas;
public SceneGestures( PannableCanvas canvas) {
this.canvas = 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) {
// right mouse button => panning
if( !event.isSecondaryButtonDown())
return;
sceneDragContext.mouseAnchorX = event.getSceneX();
sceneDragContext.mouseAnchorY = event.getSceneY();
sceneDragContext.translateAnchorX = canvas.getTranslateX();
sceneDragContext.translateAnchorY = canvas.getTranslateY();
}
};
private EventHandler<MouseEvent> onMouseDraggedEventHandler = new EventHandler<MouseEvent>() {
public void handle(MouseEvent event) {
// right mouse button => panning
if( !event.isSecondaryButtonDown())
return;
canvas.setTranslateX(sceneDragContext.translateAnchorX + event.getSceneX() - sceneDragContext.mouseAnchorX);
canvas.setTranslateY(sceneDragContext.translateAnchorY + event.getSceneY() - 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 = 1.2;
double scale = canvas.getScale(); // currently we only use Y, same value is used for X
double oldScale = scale;
if (event.getDeltaY() < 0)
scale /= delta;
else
scale *= delta;
scale = clamp( scale, MIN_SCALE, MAX_SCALE);
double f = (scale / oldScale)-1;
double dx = (event.getSceneX() - (canvas.getBoundsInParent().getWidth()/2 + canvas.getBoundsInParent().getMinX()));
double dy = (event.getSceneY() - (canvas.getBoundsInParent().getHeight()/2 + canvas.getBoundsInParent().getMinY()));
canvas.setScale( scale);
// note: pivot value must be untransformed, i. e. without scaling
canvas.setPivot(f*dx, f*dy);
event.consume();
}
};
public static double clamp( double value, double min, double max) {
if( Double.compare(value, min) < 0)
return min;
if( Double.compare(value, max) > 0)
return max;
return value;
}
}
/**
* An application with a zoomable and pannable canvas.
*/
public class ZoomAndScrollApplication extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage stage) {
Group group = new Group();
// create canvas
PannableCanvas canvas = new PannableCanvas();
// we don't want the canvas on the top/left in this example => just
// translate it a bit
canvas.setTranslateX(100);
canvas.setTranslateY(100);
// create sample nodes which can be dragged
NodeGestures nodeGestures = new NodeGestures( canvas);
Label label1 = new Label("Draggable node 1");
label1.setTranslateX(10);
label1.setTranslateY(10);
label1.addEventFilter( MouseEvent.MOUSE_PRESSED, nodeGestures.getOnMousePressedEventHandler());
label1.addEventFilter( MouseEvent.MOUSE_DRAGGED, nodeGestures.getOnMouseDraggedEventHandler());
Label label2 = new Label("Draggable node 2");
label2.setTranslateX(100);
label2.setTranslateY(100);
label2.addEventFilter( MouseEvent.MOUSE_PRESSED, nodeGestures.getOnMousePressedEventHandler());
label2.addEventFilter( MouseEvent.MOUSE_DRAGGED, nodeGestures.getOnMouseDraggedEventHandler());
Label label3 = new Label("Draggable node 3");
label3.setTranslateX(200);
label3.setTranslateY(200);
label3.addEventFilter( MouseEvent.MOUSE_PRESSED, nodeGestures.getOnMousePressedEventHandler());
label3.addEventFilter( MouseEvent.MOUSE_DRAGGED, nodeGestures.getOnMouseDraggedEventHandler());
Circle circle1 = new Circle( 300, 300, 50);
circle1.setStroke(Color.ORANGE);
circle1.setFill(Color.ORANGE.deriveColor(1, 1, 1, 0.5));
circle1.addEventFilter( MouseEvent.MOUSE_PRESSED, nodeGestures.getOnMousePressedEventHandler());
circle1.addEventFilter( MouseEvent.MOUSE_DRAGGED, nodeGestures.getOnMouseDraggedEventHandler());
Rectangle rect1 = new Rectangle(100,100);
rect1.setTranslateX(450);
rect1.setTranslateY(450);
rect1.setStroke(Color.BLUE);
rect1.setFill(Color.BLUE.deriveColor(1, 1, 1, 0.5));
rect1.addEventFilter( MouseEvent.MOUSE_PRESSED, nodeGestures.getOnMousePressedEventHandler());
rect1.addEventFilter( MouseEvent.MOUSE_DRAGGED, nodeGestures.getOnMouseDraggedEventHandler());
canvas.getChildren().addAll(label1, label2, label3, circle1, rect1);
group.getChildren().add(canvas);
// create scene which can be dragged and zoomed
Scene scene = new Scene(group, 1024, 768);
SceneGestures sceneGestures = new SceneGestures(canvas);
scene.addEventFilter( MouseEvent.MOUSE_PRESSED, sceneGestures.getOnMousePressedEventHandler());
scene.addEventFilter( MouseEvent.MOUSE_DRAGGED, sceneGestures.getOnMouseDraggedEventHandler());
scene.addEventFilter( ScrollEvent.ANY, sceneGestures.getOnScrollEventHandler());
stage.setScene(scene);
stage.show();
canvas.addGrid();
}
}
As nobody answered the question up until now and I stumbled over the same problem, I will post my solution, which adds a simple calculation of the left/up/lower and right overhang of nodes.
If you replace the part of the zooming-code with the part attached below, you should be good to got.
//maxX = right overhang, maxY = lower overhang
double maxX = canvas.getBoundsInParent().getMaxX() - canvas.localToParent(canvas.getPrefWidth(), canvas.getPrefHeight()).getX();
double maxY = canvas.getBoundsInParent().getMaxY() - canvas.localToParent(canvas.getPrefWidth(), canvas.getPrefHeight()).getY();
// minX = left overhang, minY = upper overhang
double minX = canvas.localToParent(0,0).getX() - canvas.getBoundsInParent().getMinX();
double minY = canvas.localToParent(0,0).getY() - canvas.getBoundsInParent().getMinY();
// adding the overhangs together, as we only consider the width of canvas itself
double subX = maxX + minX;
double subY = maxY + minY;
// subtracting the overall overhang from the width and only the left and upper overhang from the upper left point
double dx = (event.getSceneX() - ((canvas.getBoundsInParent().getWidth()-subX)/2 + (canvas.getBoundsInParent().getMinX()+minX)));
double dy = (event.getSceneY() - ((canvas.getBoundsInParent().getHeight()-subY)/2 + (canvas.getBoundsInParent().getMinY()+minY)));
WARNING: The left and up overhang will always be computed correctly, but I did not find any working way, to compute the right and lower overhang of nodes without the use of the preferred height and width attributes. So keep in mind, that you need these.
Also, you can improve the performance by only computing the canvas.getBoundsInParent() thing only once before as well as the the other calculations that are computed multiple times.
Hope it helps someone.