Reputation: 3655
I don't understand why this is happening. Currently I'm trying to drag a node (Canvas) by right clicking it, holding right click down, and moving the mouse. At first it's smooth, but then it starts getting this weird jitter. This only occurs while I hold down the mouse button. If you release it, then it goes back to normal (but can very quickly get jittery again).
With some debugging, it appears that the more you move it, you gain this 'imprecision' between each drag event where the mouse position goes out of sync more and more. The end result is that it flip flops back and forth, which looks very bad.
Code being used:
Main.java
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
public class Main extends Application {
@Override
public void start(Stage primaryStage) {
try {
FXMLLoader fxmlLoader = new FXMLLoader(Main.class.getResource("CanvasPane.fxml"));
Pane root = fxmlLoader.load();
Scene scene = new Scene(root, 512, 512);
primaryStage.setScene(scene);
primaryStage.show();
} catch(Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
launch(args);
}
}
Controller.java
import javafx.fxml.FXML;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.StrokeLineCap;
import javafx.scene.shape.StrokeLineJoin;
public class Controller {
@FXML
private Pane rootPane;
@FXML
private Canvas canvas;
private double mouseX;
private double mouseY;
@FXML
private void initialize() {
GraphicsContext gc = canvas.getGraphicsContext2D();
// Set the writing pen.
gc.setLineCap(StrokeLineCap.ROUND);
gc.setLineJoin(StrokeLineJoin.ROUND);
gc.setLineWidth(1);
gc.setStroke(Color.BLACK);
// Set the background to be transparent.
gc.setFill(Color.BLUE);
gc.fillRect(0, 0, 256, 256);
// Handle moving the canvas.
canvas.setOnMousePressed(e -> {
if (e.getButton() == MouseButton.PRIMARY) {
gc.moveTo(e.getX(), e.getY());
} else if (e.getButton() == MouseButton.SECONDARY) {
mouseX = e.getX();
mouseY = e.getY();
}
});
canvas.setOnMouseDragged(e -> {
if (e.getButton() == MouseButton.PRIMARY) {
gc.lineTo(e.getX(), e.getY());
gc.stroke();
} else if (e.getButton() == MouseButton.SECONDARY) {
double newMouseX = e.getX();
double newMouseY = e.getY();
double deltaX = newMouseX - mouseX;
double deltaY = newMouseY - mouseY;
canvas.setLayoutX(canvas.getLayoutX() + deltaX);
canvas.setLayoutY(canvas.getLayoutY() + deltaY);
mouseX = newMouseX;
mouseY = newMouseY;
// Debug: Why is this happening?
if (Math.abs(deltaX) > 4 || Math.abs(deltaY) > 4)
System.out.println(deltaX + " " + deltaY);
}
});
}
}
CanvasPane.fxml
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.canvas.*?>
<?import java.lang.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.layout.Pane?>
<Pane fx:id="rootPane" maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" minHeight="0.0" minWidth="0.0" prefHeight="512.0" prefWidth="512.0" xmlns="http://javafx.com/javafx/8.0.40" xmlns:fx="http://javafx.com/fxml/1" fx:controller="your-controller-here">
<children>
<Canvas fx:id="canvas" height="256.0" layoutX="122.0" layoutY="107.0" width="256.0" />
</children>
</Pane>
USAGE INSTRUCTIONS
1) Run application
2) Right click on the canvas and move it very slightly (like only one pixel per second)
3) Move it a bit faster so the debug prints
4) When you go back to moving slow, for some reason you'll see it hopping back and forth (like the delta movement between the old and new position is greater than 5 pixels) even though you only moved it one pixel.
It then appears to try to fix itself next time you move, which gives it an ugly jittery look.
The reason I'm confused is because there are times where it works great, and then the accuracy just drops as you continue dragging.
Did I code something wrong, or is this a potential bug?
Upvotes: 1
Views: 876
Reputation: 209340
The coordinates you are measuring with MouseEvent.getX()
and MouseEvent.getY()
are relative to the node on which the event occurred: i.e. relative to your Canvas
. Since you then move the canvas, the old coordinates are now incorrect (since they were relative to the old position, not the new one). Consequently your values for deltaX
and deltaY
are incorrect.
To fix this, just measure relative to something that is "fixed", e.g. the Scene
. You can do this by using MouseEvent.getSceneX()
and MouseEvent.getSceneY()
.
import javafx.fxml.FXML;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.StrokeLineCap;
import javafx.scene.shape.StrokeLineJoin;
public class CanvasDragController {
@FXML
private Pane rootPane;
@FXML
private Canvas canvas;
private double mouseX;
private double mouseY;
@FXML
private void initialize() {
GraphicsContext gc = canvas.getGraphicsContext2D();
// Set the writing pen.
gc.setLineCap(StrokeLineCap.ROUND);
gc.setLineJoin(StrokeLineJoin.ROUND);
gc.setLineWidth(1);
gc.setStroke(Color.BLACK);
// Set the background to be transparent.
gc.setFill(Color.BLUE);
gc.fillRect(0, 0, 256, 256);
// Handle moving the canvas.
canvas.setOnMousePressed(e -> {
if (e.getButton() == MouseButton.PRIMARY) {
gc.moveTo(e.getX(), e.getY());
} else if (e.getButton() == MouseButton.SECONDARY) {
mouseX = e.getSceneX();
mouseY = e.getSceneY();
}
});
canvas.setOnMouseDragged(e -> {
if (e.getButton() == MouseButton.PRIMARY) {
gc.lineTo(e.getX(), e.getY());
gc.stroke();
} else if (e.getButton() == MouseButton.SECONDARY) {
double newMouseX = e.getSceneX();
double newMouseY = e.getSceneY();
double deltaX = newMouseX - mouseX;
double deltaY = newMouseY - mouseY;
canvas.setLayoutX(canvas.getLayoutX() + deltaX);
canvas.setLayoutY(canvas.getLayoutY() + deltaY);
mouseX = newMouseX;
mouseY = newMouseY;
// Why is this happening?
if (Math.abs(deltaX) > 4 || Math.abs(deltaY) > 4)
System.out.println(deltaX + " " + deltaY);
}
});
}
}
An alternative approach (which is not quite as intuitive, to me at least) is to use MouseEvent.getX()
and MouseEvent.getY()
but not to update the "last coordinates" of the mouse. The way to think of this is that, as the node is dragged around, you want it not to be moving in relation to the mouse - in other words you want the node to move so that the coordinates of the mouse remain fixed relative to it. This version looks like:
import javafx.fxml.FXML;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.StrokeLineCap;
import javafx.scene.shape.StrokeLineJoin;
public class CanvasDragController {
@FXML
private Pane rootPane;
@FXML
private Canvas canvas;
private double mouseX;
private double mouseY;
@FXML
private void initialize() {
GraphicsContext gc = canvas.getGraphicsContext2D();
// Set the writing pen.
gc.setLineCap(StrokeLineCap.ROUND);
gc.setLineJoin(StrokeLineJoin.ROUND);
gc.setLineWidth(1);
gc.setStroke(Color.BLACK);
// Set the background to be transparent.
gc.setFill(Color.BLUE);
gc.fillRect(0, 0, 256, 256);
// Handle moving the canvas.
canvas.setOnMousePressed(e -> {
if (e.getButton() == MouseButton.PRIMARY) {
gc.moveTo(e.getX(), e.getY());
} else if (e.getButton() == MouseButton.SECONDARY) {
mouseX = e.getX();
mouseY = e.getY();
}
});
canvas.setOnMouseDragged(e -> {
if (e.getButton() == MouseButton.PRIMARY) {
gc.lineTo(e.getX(), e.getY());
gc.stroke();
} else if (e.getButton() == MouseButton.SECONDARY) {
double newMouseX = e.getX();
double newMouseY = e.getY();
double deltaX = newMouseX - mouseX;
double deltaY = newMouseY - mouseY;
canvas.setLayoutX(canvas.getLayoutX() + deltaX);
canvas.setLayoutY(canvas.getLayoutY() + deltaY);
// Why is this happening?
if (Math.abs(deltaX) > 4 || Math.abs(deltaY) > 4)
System.out.println(deltaX + " " + deltaY);
}
});
}
}
Upvotes: 2