Detect click in a diamond - math

I want to detect if I click inside a diamond.
The only thing I have are the coordinates of the click (x,y), the center of the diamond (x,y) and the width/height of the diamond.
I found this, but the problem is different.
pixel coordinates on diamond

You can formulate a distance measure based upon the l(1) norm within which points of fixed distance from some center point form an axially aligned diamond with vertices equidistant from the center.
In this case you will need to apply a suitable affine transformation to place your diamond into a canonical form centered at the origin with the vertices of the diamond placed on the coordinate axes equidistant from the origin; call this distance r. Depending upon the form of the original diamond, this may require translation (if the diamond is not centered on the origin), rotation (if the diagonals of the diamond are not axially aligned) and scaling (if the diagonals are not of equal length) operations which form the basis of the affine transformation you will apply. You then apply this same affine transformation to your mouse click and sum the absolute value of each component of the resulting point; call this sum d. If r > d then the point lies interior to the diamond. If d > r the point lies exterior to the diamond, and if r = d the point lies on an edge of the diamond.

The answer that you linked actually contains everything you need: You can do the "Direct point position check" to detect whether a point is inside the diamond.
I assume that the diamonds can not be rotated or so, otherwise, the question would have been horribly imprecise.
Here is an MCVE, implemented in Java/Swing as an example:
The relevant part is actually the Diamond#contains method at the bottom, which consists of the 4 lines of code taken from the other answer....
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.RenderingHints;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;
import java.awt.geom.Path2D;
import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
public class DiamondClickTest
{
public static void main(String[] args)
{
SwingUtilities.invokeLater(new Runnable()
{
#Override
public void run()
{
createAndShowGUI();
}
});
}
private static void createAndShowGUI()
{
JFrame f = new JFrame();
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
List<Diamond> diamonds = new ArrayList<Diamond>();
diamonds.add(new Diamond("A", new Point(100,100), 180, 140));
diamonds.add(new Diamond("B", new Point(300,100), 110, 160));
diamonds.add(new Diamond("C", new Point(100,300), 110, 180));
diamonds.add(new Diamond("D", new Point(300,300), 130, 150));
DiamondClickTestPanel p = new DiamondClickTestPanel(diamonds);
f.getContentPane().add(p);
f.setSize(400,430);
f.setLocationRelativeTo(null);
f.setVisible(true);
}
}
class DiamondClickTestPanel extends JPanel implements MouseMotionListener
{
private List<Diamond> diamonds;
private Diamond highlighedDiamond = null;
DiamondClickTestPanel(List<Diamond> diamonds)
{
this.diamonds = diamonds;
addMouseMotionListener(this);
}
#Override
protected void paintComponent(Graphics gr)
{
super.paintComponent(gr);
Graphics2D g = (Graphics2D)gr;
g.setRenderingHint(
RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
for (Diamond diamond : diamonds)
{
draw(g, diamond);
}
}
private void draw(Graphics2D g, Diamond diamond)
{
Point2D c = diamond.getCenter();
double x0 = c.getX() + diamond.getWidth() * 0.5;
double y0 = c.getY();
double x1 = c.getX();
double y1 = c.getY() - diamond.getHeight() * 0.5;
double x2 = c.getX() - diamond.getWidth() * 0.5;
double y2 = c.getY();
double x3 = c.getX();
double y3 = c.getY() + diamond.getHeight() * 0.5;
Path2D p = new Path2D.Double();
p.moveTo(x0, y0);
p.lineTo(x1, y1);
p.lineTo(x2, y2);
p.lineTo(x3, y3);
p.closePath();
if (diamond == highlighedDiamond)
{
g.setColor(Color.RED);
g.fill(p);
}
g.setColor(Color.BLACK);
g.draw(p);
g.drawString(diamond.getName(), (int)c.getX()-4, (int)c.getY()+8);
}
#Override
public void mouseDragged(MouseEvent e)
{
}
#Override
public void mouseMoved(MouseEvent e)
{
double x = e.getX();
double y = e.getY();
highlighedDiamond = null;
for (Diamond diamond : diamonds)
{
if (diamond.contains(x, y))
{
highlighedDiamond = diamond;
}
}
repaint();
}
}
class Diamond
{
private String name;
private Point2D center;
private double width;
private double height;
Diamond(String name, Point2D center, double width, double height)
{
this.name = name;
this.center = center;
this.width = width;
this.height = height;
}
String getName()
{
return name;
}
Point2D getCenter()
{
return center;
}
double getWidth()
{
return width;
}
double getHeight()
{
return height;
}
boolean contains(double x, double y)
{
double dx = Math.abs(x - center.getX());
double dy = Math.abs(y - center.getY());
double d = dx / width + dy / height;
return d <= 0.5;
}
}

Related

Rotate a polygon around its center using Rotate

I want to track my points in a Polygon class after transformations. My problem with using the Rotate class from JavaFX, is to get the points position after the rotation, by doing something like this in the Piece class:
double x = this.getPoints().get(0) + this.getLocalToParentTransform().getTx()
double y = this.getPoints().get(1) + this.getLocalToParentTransform().getTy()
It works using the Translate class, but when I rotate, the coordinates will rotate itself with the rotation with a weird pivot point, and not follow the polygon at all, unless it is rotated 360 degrees. Piece is a subclass of Polygon.
private double originalX;
private double originalY;
private double centerX;
private double centerY;
private void initializePiece(Piece piece, Pane pane, int i) {
piece.setOnMousePressed(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
originalX = event.getSceneX();
originalY = event.getSceneY();
centerX = piece.getCenterX();
centerY = piece.getCenterY();
}
});
piece.setOnMouseDragged(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
if (event.getButton() == MouseButton.PRIMARY) {
// Position of piece wrt. picked up position
double deltaX = event.getSceneX() - originalX;
double deltaY = event.getSceneY() - originalY;
Translate translate = new Translate();
translate.setX(deltaX);
translate.setY(deltaY);
piece.getTransforms().addAll(translate);
originalX = event.getSceneX();
originalY = event.getSceneY();
}
if (event.getButton() == MouseButton.SECONDARY) {
double deltaY = event.getSceneY() - originalY;
Rotate rotation = new Rotate(deltaY, centerX, centerY);
piece.getTransforms().add(rotation);
originalY = event.getSceneY();
}
}
});
}
And this is a snippet from the Piececlass.
public double getCenterX() {
double avg = 0;
for (int i = 0; i < this.getPoints().size(); i += 2) {
avg += this.getPoints().get(i) + this.getLocalToParentTransform().getTx();
}
avg = avg / (this.getPoints().size() / 2);
return avg;
}
public double getCenterY() {
double avg = 0;
for (int i = 1; i < this.getPoints().size(); i += 2) {
avg += this.getPoints().get(i) + this.getLocalToParentTransform().getTy();
}
avg = avg / (this.getPoints().size() / 2);
return avg;
}
All transformations are applied in local coordinates, so the pivot point should be given in the local coordinate system of the polygon, which is "unaware" of the transformations applied to it. I.e. you should just compute the center of the polygon in its own coordinate system, not its parent's coordinate system.
The mouse event's getX() and getY() methods will give the coordinates in the coordinate system of the event's source node, so using these makes the computations relatively easy. Here's a simple example (I calculated the current angle between the mouse press and the center of the polygon, and then the change in angle on dragging, instead of just using the y-coordinate; this feels more natural.)
import javafx.application.Application;
import javafx.geometry.Point2D;
import javafx.scene.Scene;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Polygon;
import javafx.scene.transform.Rotate;
import javafx.scene.transform.Translate;
import javafx.stage.Stage;
public class DraggingTransforms extends Application {
public static void main(String[] args) {
Application.launch(args);
}
private double pressX ;
private double pressY ;
private double angle ;
#Override
public void start(Stage stage) throws Exception {
Polygon poly = new Polygon(10, 10, 50, 10, 50, 0, 70, 20, 50, 40, 50, 30, 10, 30, 10, 10);
poly.setFill(Color.web("#00b140"));
Pane root = new Pane(poly);
poly.setOnMousePressed(e -> {
pressX = e.getX();
pressY = e.getY();
angle = computeAngle(poly, e);
});
poly.setOnMouseDragged(e -> {
if (e.getButton() == MouseButton.PRIMARY) {
poly.getTransforms().add(new Translate(e.getX() - pressX, e.getY()-pressY));
} else {
double delta = computeAngle(poly, e) - angle;
Rotate rotation = new Rotate(delta, computeCenterX(poly), computeCenterY(poly));
poly.getTransforms().add(rotation);
}
});
Scene scene = new Scene(root, 800, 800);
stage.setScene(scene);
stage.show();
}
private double computeAngle(Polygon poly, MouseEvent e) {
return new Point2D(computeCenterX(poly), computeCenterY(poly))
.angle(new Point2D(e.getX(), e.getY()));
}
private double computeCenter(int offset, Polygon poly) {
double total = 0 ;
for (int i = offset ; i < poly.getPoints().size(); i+=2) {
total += poly.getPoints().get(i);
}
return total / (poly.getPoints().size() / 2);
}
private double computeCenterX(Polygon poly) {
return computeCenter(0, poly);
}
private double computeCenterY(Polygon poly) {
return computeCenter(1, poly);
}
}
Note that if you do need the coordinates of a point in the shape transformed to the parent's coordinate system, all you need to do is, for example,
poly.getLocalToParentTransform()
.transform(
poly.getPoints().get(0),
poly.getPoints().get(1)
)

lost in 3D space - tilt values (euler?) from rotation matrix (javafx affine) only works partially

it is a while ago that I asked this question:
javafx - How to apply yaw, pitch and roll deltas (not euler) to a node in respect to the nodes rotation axes instead of the scene rotation axes?
Today I want to ask, how I can get the tilt (fore-back and sideways) relative to the body (not to the room) from the rotation matrix. To make the problem understandable, I took the final code from the fantastic answer of José Pereda and basicly added a method "getEulersFromRotationMatrix". This is working a bit, but at some point freaks out.
Attached find the whole working example. The problem becomes clear with the following click path:
// right after start
tilt fore
tilt left // all right
tilt right
tilt back // all right
// right after start
turn right
turn right
turn right
tilt fore
tilt back // all right
tilt left // bang, tilt values are completely off
While the buttons move the torso as expected, the tilt values (printed out under the buttons) behave wrong at some point.
import javafx.application.Application;
import javafx.application.Platform;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Point3D;
import javafx.scene.Group;
import javafx.scene.Parent;
import javafx.scene.PerspectiveCamera;
import javafx.scene.Scene;
import javafx.scene.SceneAntialiasing;
import javafx.scene.SubScene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.paint.PhongMaterial;
import javafx.scene.shape.Box;
import javafx.scene.transform.Affine;
import javafx.scene.transform.Rotate;
import javafx.stage.Stage;
public class PuppetTestApp extends Application {
private final int width = 800;
private final int height = 500;
private XGroup torsoGroup;
private final double torsoX = 50;
private final double torsoY = 80;
private Label output = new Label();
public Parent createRobot() {
Box torso = new Box(torsoX, torsoY, 20);
torso.setMaterial(new PhongMaterial(Color.RED));
Box head = new Box(20, 20, 20);
head.setMaterial(new PhongMaterial(Color.YELLOW.darker()));
head.setTranslateY(-torsoY / 2 -10);
Box x = new Box(200, 2, 2);
x.setMaterial(new PhongMaterial(Color.BLUE));
Box y = new Box(2, 200, 2);
y.setMaterial(new PhongMaterial(Color.BLUEVIOLET));
Box z = new Box(2, 2, 200);
z.setMaterial(new PhongMaterial(Color.BURLYWOOD));
torsoGroup = new XGroup();
torsoGroup.getChildren().addAll(torso, head, x, y, z);
return torsoGroup;
}
public Parent createUI() {
HBox buttonBox = new HBox();
Button b;
buttonBox.getChildren().add(b = new Button("Exit"));
b.setOnAction( (ActionEvent arg0) -> { Platform.exit(); } );
buttonBox.getChildren().add(b = new Button("tilt fore"));
b.setOnAction(new TurnAction(torsoGroup.rx, 15) );
buttonBox.getChildren().add(b = new Button("tilt back"));
b.setOnAction(new TurnAction(torsoGroup.rx, -15) );
buttonBox.getChildren().add(b = new Button("tilt left"));
b.setOnAction(new TurnAction(torsoGroup.rz, 15) );
buttonBox.getChildren().add(b = new Button("tilt right"));
b.setOnAction(new TurnAction(torsoGroup.rz, -15) );
buttonBox.getChildren().add(b = new Button("turn left"));
b.setOnAction(new TurnAction(torsoGroup.ry, -28) ); // not 30 degree to avoid any gymbal lock problems
buttonBox.getChildren().add(b = new Button("turn right"));
b.setOnAction(new TurnAction(torsoGroup.ry, 28) ); // not 30 degree to avoid any gymbal lock problems
VBox vbox = new VBox();
vbox.getChildren().add(buttonBox);
vbox.getChildren().add(output);
return vbox;
}
class TurnAction implements EventHandler<ActionEvent> {
final Rotate rotate;
double deltaAngle;
public TurnAction(Rotate rotate, double targetAngle) {
this.rotate = rotate;
this.deltaAngle = targetAngle;
}
#Override
public void handle(ActionEvent arg0) {
addRotate(torsoGroup, rotate, deltaAngle);
}
}
private void addRotate(XGroup node, Rotate rotate, double angle) {
Affine affine = node.getTransforms().isEmpty() ? new Affine() : new Affine(node.getTransforms().get(0));
double A11 = affine.getMxx(), A12 = affine.getMxy(), A13 = affine.getMxz();
double A21 = affine.getMyx(), A22 = affine.getMyy(), A23 = affine.getMyz();
double A31 = affine.getMzx(), A32 = affine.getMzy(), A33 = affine.getMzz();
Rotate newRotateX = new Rotate(angle, new Point3D(A11, A21, A31));
Rotate newRotateY = new Rotate(angle, new Point3D(A12, A22, A32));
Rotate newRotateZ = new Rotate(angle, new Point3D(A13, A23, A33));
affine.prepend(rotate.getAxis() == Rotate.X_AXIS ? newRotateX :
rotate.getAxis() == Rotate.Y_AXIS ? newRotateY : newRotateZ);
EulerValues euler= getEulersFromRotationMatrix(affine);
output.setText(String.format("tilt fore/back=%3.0f tilt sideways=%3.0f", euler.forward, euler.leftSide));
node.getTransforms().setAll(affine);
}
public class XGroup extends Group {
public Rotate rx = new Rotate(0, Rotate.X_AXIS);
public Rotate ry = new Rotate(0, Rotate.Y_AXIS);
public Rotate rz = new Rotate(0, Rotate.Z_AXIS);
}
#Override
public void start(Stage stage) throws Exception {
Parent robot = createRobot();
SubScene subScene = new SubScene(robot, width, height, true, SceneAntialiasing.BALANCED);
PerspectiveCamera camera = new PerspectiveCamera(true);
camera.setNearClip(0.01);
camera.setFarClip(100000);
camera.setTranslateZ(-400);
subScene.setCamera(camera);
Parent ui = createUI();
StackPane combined = new StackPane(ui, subScene);
combined.setStyle("-fx-background-color: linear-gradient(to bottom, cornsilk, midnightblue);");
Scene scene = new Scene(combined, width, height);
stage.setScene(scene);
stage.show();
}
/**
* Shall return the tilt values relative to the body (not relative to the room)
* (Maybe euler angles are not the right term here, but anyway)
*/
private EulerValues getEulersFromRotationMatrix(Affine rot) {
double eulerX; // turn left/right
double eulerY; // tilt fore/back
double eulerZ; // tilt sideways
double r11 = rot.getMxx();
double r12 = rot.getMxy();
double r13 = rot.getMxz();
double r21 = rot.getMyx();
double r31 = rot.getMzx();
double r32 = rot.getMzy();
double r33 = rot.getMzz();
// used instructions from https://www.gregslabaugh.net/publications/euler.pdf
if (r31 != 1.0 && r31 != -1.0) {
eulerX = -Math.asin(r31); // already tried with the 2nd solution as well
double cosX = Math.cos(eulerX);
eulerY = Math.atan2(r32/cosX, r33/cosX);
eulerZ = Math.atan2(r21/cosX, r11/cosX);
}
else {
eulerZ = 0;
if (r31 == -1) {
eulerX = Math.PI / 2;
eulerY = Math.atan2(r12, r13);
}
else {
eulerX = -Math.PI / 2;
eulerY = Math.atan2(-r12, -r13);
}
}
return new EulerValues(
eulerY / Math.PI * 180.0,
eulerZ / Math.PI * 180.0,
-eulerX / Math.PI * 180.0);
}
public class EulerValues {
public double leftTurn;
public double forward;
public double leftSide;
public EulerValues(double forward, double leftSide, double leftTurn) {
this.forward = forward;
this.leftSide = leftSide;
this.leftTurn = leftTurn;
}
}
public static void main(String[] args) {
launch(args);
}
}
PS: This may look like I have close to no progress, but this is only because I try to reduce the question to the possible minimum. If you want to see how this stuff is embedded in my main project, you can watch this little video I just uploaded (but does not add anything to the question): https://www.youtube.com/watch?v=R3t8BIHeo7k
I think I got it by myself now: What I computed was the "default" euler angles, sometimes refered to as z x' z'', where the 1st and 3th rotation is around the same axis. But what I am looking for are the angles that can be applied to the z, y' and x'' achses (in that order) to reach the position presented by the rotation matrix. (and then ignore the z rotation).
Or even better compute the z y' x'' eulers and the z x' y'' eulers and
only use the x' and y' values.
Added:
No, that was wrong. I indeed calculated the Tait-Bryan x y z rotations. So this was not the solution.
Ok, new explanation:
The rotation axes wthat I calculate are room relative rotations (not object relative rotations), and the 2nd rotation is at the vertical axe (which I am not interested in). But because it is "in the middle", it can cancel out the 1st and 3th rotation, and this is what happens.
So the solution should be the change the rotation order, that comes out of my matrix-to-euler algorithm. But how to do this?
I just exchanged all "y" and "z":
r11 = rot.getMxx();
r12 = rot.getMxz();
r13 = rot.getMxy();
r21 = rot.getMzx();
r31 = rot.getMyx();
r32 = rot.getMyz();
r33 = rot.getMyy();
and now it really does what I want. :)

JavaFX - avoiding a retracing of common hexagon edges in a hexagonal map

private void addHexagonsToScreen() {
for(Hex hex : map.getMap()) {
Point[] corners = screen.polygonCorners(hex);
int i = 0;
Double[] points = new Double[12];
for(Point point : corners) {
points[i] = point.getX();
points[i+1] = point.getY();
i += 2;
}
this.root.getChildren().add(drawHexagon(points));
}
}
private Polygon drawHexagon(Double[] points) {
Polygon polygon = new Polygon();
polygon.getPoints().addAll(points);
polygon.setStroke(Color.BLACK);
polygon.setFill(Color.TRANSPARENT);
return polygon;
}
This is how I define a point.
public class Point {
private final double x, y;
public Point(double x, double y) {
this.x = x;
this.y = y;
}
/**
* #return the screen x-coordinate.
*/
public double getX() {
return x;
}
/**
* #return the screen y-coordinate.
*/
public double getY() {
return y;
} }
This is currently how I am drawing my hexagons on a Pane object. As you can see in the picture below, I'm fairly certain that most of the lines are actually being retraced because each edge (except the outer edges) are obviously common to two hexagons. In order to solve this, I instead calculated all the points at once then passed that into the Polygon() function as a double [] with the intention of drawing just one polygon rather than many.
private void addHexagonsToScreen() {
int i = 0;
Double[] points = new Double[map.getMap().size() * 12];
for(Hex hex : map.getMap()) {
Point[] corners = screen.polygonCorners(hex);
for(Point point : corners) {
points[i] = point.getX();
points[i+1] = point.getY();
i += 2;
}
}
this.root.getChildren().add(drawHexagon(points));
}
This resulted in the second picture. I think the solution requires that I specify "only connect each point to its closest other point", but I am unsure of how to do this. Is there a simple solution to my problem?
The reason the hex map is off in the top left corner is because I'm specifying the origin of my hex map to be 0,0 and calculating a pixel location as such. Obviously I have done something wrong in converting my 0,0 hexagon location to correspond with the center of the Pane and growing Y+ in the up direction and X+ in the right direction.

How to position node inside a rotated group at mouse event coordinates?

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.

move objects on screen in javafx

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.

Resources