How can I restrict the motion of the blue circle (due to a mouse drag) only on the red circle path? Should I use polar coordinates? (x=rcos(θ), y=rsin(θ))?
The code I created till now let me drag the blue point all over the stage. I want the center of the blue point to follow the red circle.
package circlemouse;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Line;
import javafx.stage.Stage;
public class CircleMouse extends Application {
private double initY;
private double dragAnchorY;
private double initX;
private double dragAnchorX;
#Override
public void start(Stage primaryStage) {
Pane pane = new Pane();
Scene scene = new Scene(pane, 500, 500);
primaryStage.setResizable(false);
//stage center
double x0 = pane.getWidth() / 2.0;
double y0 = pane.getHeight() / 2.0;
Line horizontalLine = new Line(0.0, y0, 2.0 * x0, y0);
Line vertical = new Line(x0, 0.0, x0, 2.0 * y0);
//red circle (path of point)
double r = 100.0;
Circle c = new Circle(x0, y0, r);
c.setFill(null);
c.setStroke(Color.RED);
//the point
double pointRadius = 15.0;
Circle point = new Circle(x0 + r, y0, pointRadius);
point.setFill(Color.BLUE);
point.setOnMousePressed((MouseEvent me) -> {
initY = point.getCenterY();
dragAnchorY = me.getSceneY();
initX = point.getCenterX();
dragAnchorX = me.getSceneX();
});
point.setOnMouseDragged((MouseEvent me) -> {
double dragY = me.getSceneY() - dragAnchorY;
double newY = initY + dragY;
point.setCenterY(newY);
double dragX = me.getSceneX() - dragAnchorX;
double newX = initX + dragX;
point.setCenterX(newX);
});
pane.getChildren().addAll(horizontalLine, vertical, c, point);
primaryStage.setTitle("Hello World!");
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
If you draw a line from the center of the red circle to where the mouse is, and then draw a line from the center of the red circle to where you want the point, they are obviously in the same direction, and the length of the line from the center of the red circle to where you want the point is just the radius of the line.
So in vector terminology, the vector from the center of the circle to the new point position is the radius of the circle times the unit vector in the direction from the center of the circle to the mouse.
The Point2D API allows you to interpret a Point2D as a vector, and has useful methods for computing the unit vector (normalize()), multiplying by a scalar, adding and subtracting other vectors, etc.
So:
point.setOnMouseDragged((MouseEvent me) -> {
Point2D redCenter = new Point2D(c.getCenterX(), c.getCenterY());
Point2D mouse = new Point2D(me.getX(), me.getY());
Point2D centerToMouse = mouse.subtract(redCenter);
Point2D centerToNewPoint = centerToMouse.normalize().multiply(c.getRadius());
Point2D newPoint = centerToNewPoint.add(redCenter);
point.setCenterX(newPoint.getX());
point.setCenterY(newPoint.getY());
});
Related
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. :)
As mentioned in the title, i have two Circle 's the first is draggable and the second is fixed, I would rotate (with the drag) the first one around the second without overlapping them but my Circle reacts oddly, I'm sure the error comes from the drag condition but I don't know how to solve it, that's why I need your help, here is a minimal and testable code :
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;
public class Collision extends Application{
private Pane root = new Pane();
private Scene scene;
private Circle CA = new Circle(20);
private Circle CB = new Circle(20);
private double xOffset = 0;
private double yOffset = 0;
#Override
public void start(Stage stage) throws Exception{
initCircles();
scene = new Scene(root,500,500);
stage.setScene(scene);
stage.show();
}
private void initCircles(){
CA.setCenterX(100);
CA.setCenterY(100);
CA.setFill(Color.rgb(255, 0, 0,0.2));
CA.setStroke(Color.BLACK);
CB.setCenterX(250);
CB.setCenterY(200);
CB.setFill(Color.rgb(255, 0, 0,0.2));
CB.setStroke(Color.BLACK);
CA.setOnMousePressed(evt->{
xOffset = CA.getCenterX() - evt.getSceneX();
yOffset = CA.getCenterY() - evt.getSceneY();
});
CA.setOnMouseDragged(evt->{
//get Scene coordinate from MouseEvent
drag(evt.getSceneX(),evt.getSceneY());
});
root.getChildren().addAll(CA,CB);
}
private void drag(double x, double y){
/* calculate the distance between
* the center of the first and the second circle
*/
double distance = Math.sqrt (Math.pow(CA.getCenterX() - CB.getCenterX(),2) + Math.pow(CA.getCenterY() - CB.getCenterY(),2));
if (!(distance < (CA.getRadius() + CB.getRadius()))){
CA.setCenterX(x + xOffset);
CA.setCenterY(y + yOffset);
}else{
/**************THE PROBLEM :Condition to drag************/
CA.setCenterX(CA.getCenterX() - (CB.getCenterX()-CA.getCenterX()));
CA.setCenterY(CA.getCenterY() - (CB.getCenterY()-CA.getCenterY()));
/*What condition must be established for the
* circle to behave correctly
*/
/********************************************************/
}
}
public static void main(String[] args) {
launch(args);
}
}
Here is a brief overview :
Note:
for my defense, i searched and found several subject close to mine but which have no precise or exact solution, among which:
-The circle remains blocked at the time of the collision
-Two circle that push each other
-JavaScript, Difficult to understand and convert to java
Thank you for your help !
Point2D can be interpreted as a 2D vector, and has useful methods for creating new vectors from it, etc. You can do:
private void drag(double x, double y){
// place drag wants to move circle to:
Point2D newCenter = new Point2D(x + xOffset, y+yOffset);
// center of fixed circle:
Point2D fixedCenter = new Point2D(CB.getCenterX(), CB.getCenterY());
// minimum distance between circles:
double minDistance = CA.getRadius() + CB.getRadius() ;
// if they overlap, adjust newCenter:
if (newCenter.distance(fixedCenter) < minDistance) {
// vector between fixedCenter and newCenter:
Point2D newDelta = newCenter.subtract(fixedCenter);
// adjust so that length of delta is distance between two centers:
Point2D adjustedDelta = newDelta.normalize().multiply(minDistance);
// move newCenter to match adjusted delta:
newCenter = fixedCenter.add(adjustedDelta);
}
CA.setCenterX(newCenter.getX());
CA.setCenterY(newCenter.getY());
}
Obviously, you could do all this without using Point2D and just doing the computation, but I think the API calls make the code easier to understand.
I am currently writing a program in JavaFX, which visualises a graph in 3D. The user should be able to translate and rotate it.
As the common rule for movement in 3D is, to move the camera instead of the whole graph, I followed this rule.
I created a StackPane, which contains two Panes (bottomPane and topPane). The bottomPane contains the SubScene, in which the graph is displayed and which uses a PerspectiveCamera. In the topPane I put a Group containing the Labels for specific nodes.
I did this setup to prevent labels being hidden by the graph, when their corresponding nodes are behind other nodes or edges.
To get the positioning of the labels right, I bind a function to the translateX/Y/Z properties of the camera, which updates the positions of the labels accordingly (rearrangeLabels()).
This function calculates the screen coordinates of the nodes and then calculates these back to the coordinates of the group containing the labels.
This works perfectly for translation (e.g. moving the graph up/down/left/right). It also works, if I rotate the camera only around the x-axis OR the y-axis.
But when I want to rotate in both axis the same time (e.g. diagonal) the labels are not moving accordingly.
I tried a lot of different approaches and came to the conclusion, that this might be a bug in JavaFX, or more precise in either the localToScreen() or screenToLocal() function.
I attached a demo program, which depicts the mentioned problem.
Just press Start and use your primary mouse button to rotate the camera.
So my question is, does anyone of you know, if this is a known bug? And if yes, is there a workaround? Or am I doing something wrong?
EDIT: I narrowed the error down to being the rotation around the Z-axis. When rotating ONLY around the z-axis, the weird behaviour happens, whereas rotating around either the X-axis or the Y-axis alone, everything moves as expected.
import javafx.application.Application;
import javafx.geometry.Point2D;
import javafx.geometry.Point3D;
import javafx.scene.*;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Cylinder;
import javafx.scene.shape.Sphere;
import javafx.scene.transform.Rotate;
import javafx.scene.transform.Transform;
import javafx.scene.transform.Translate;
import javafx.stage.Stage;
public class RotateCamWithLabel extends Application{
Sphere node1;
Sphere node2;
Cylinder edge;
Label nodeLabel1;
Label nodeLabel2;
Group labelGroup;
Group graphGroup;
Pane topPane;
Pane bottomPane;
PerspectiveCamera cam;
double oldPosX;
double oldPosY;
Transform camTransform = new Rotate();
#Override
public void start(Stage primaryStage) throws Exception {
// Setting up the root of the whole Scene
topPane = new Pane();
topPane.setPickOnBounds(false);
bottomPane = new Pane();
StackPane root = new StackPane();
root.getChildren().addAll(bottomPane, topPane);
Button startButton = new Button("Start");
startButton.setOnAction(event -> {
setUpSubSceneAndCam();
addLabels();
});
topPane.getChildren().add(startButton);
// Starting the Scene
Scene scene = new Scene(root, 700, 500);
primaryStage.setScene(scene);
primaryStage.show();
}
private void setUpSubSceneAndCam() {
createGraph();
SubScene subScene = new SubScene(graphGroup, 700, 500, true, SceneAntialiasing.BALANCED);
subScene.setFill(Color.LIGHTGRAY);
cam = new PerspectiveCamera(true);
cam.setTranslateZ(-1000);
cam.setNearClip(0.1);
cam.setFarClip(Double.MAX_VALUE);
subScene.setCamera(cam);
bottomPane.getChildren().add(subScene);
createCameraLabelBinding();
subScene.setOnMousePressed(event -> {
oldPosX = event.getSceneX();
oldPosY = event.getSceneY();
});
subScene.setOnMouseDragged(event -> {
double deltaX = event.getSceneX() - oldPosX;
double deltaY = event.getSceneY() - oldPosY;
if (event.isPrimaryButtonDown()) {
rotateCamera(deltaX, deltaY);
}
oldPosX = event.getSceneX();
oldPosY = event.getSceneY();
});
}
// Rotation is done by calculating the local camera position in the subscene and out of these,
// the rotation axis is calculated as well as the degree.
// Then the camera is repositioned on the pivot point, turned based on the axis and degree, and put
// back along its local backwards vector based on the starting ditance between the camera and the pivot point
private void rotateCamera(double deltaX, double deltaY) {
// Calculate rotation-axis
Point3D leftVec = getCamLeftVector().multiply(deltaX);
Point3D upVec = getCamUpVector().multiply(deltaY);
Point3D dragVec = leftVec.add(upVec).multiply(-1);
Point3D backVec = getCamBackwardVector();
Point3D axis = dragVec.crossProduct(backVec);
//Point3D axis = Rotate.Z_AXIS; //Does not work
//Point3D axis = Rotate.Y_AXIS; //Works
//Point3D axis = Rotate.X_AXIS; //Works
Rotate r = new Rotate(dragVec.magnitude(), axis);
// set camera on pivot point
Point3D pivot = Point3D.ZERO;
Point3D camCurrent = new Point3D(cam.getTranslateX(), cam.getTranslateY(), cam.getTranslateZ());
Point3D camPivVec = pivot.subtract(camCurrent);
setCameraPosition(pivot);
// rotate camera
camTransform = r.createConcatenation(camTransform);
cam.getTransforms().setAll(camTransform);
// put camera back along the local backwards vector
double length = camPivVec.magnitude();
setCameraPosition(getCamBackwardVector().multiply(length).add(pivot));
}
private void addLabels() {
if (labelGroup != null) {
labelGroup.getChildren().remove(nodeLabel1);
labelGroup.getChildren().remove(nodeLabel2);
}
labelGroup = new Group();
topPane.getChildren().add(labelGroup);
nodeLabel1 = new Label("Hello");
nodeLabel2 = new Label("Bye");
labelGroup.getChildren().addAll(nodeLabel1, nodeLabel2);
rearrangeLabels();
}
private void createCameraLabelBinding() {
cam.translateXProperty().addListener((observable, oldValue, newValue) -> rearrangeLabels());
cam.translateYProperty().addListener((observable, oldValue, newValue) -> rearrangeLabels());
cam.translateZProperty().addListener((observable, oldValue, newValue) -> rearrangeLabels());
}
// TODO: Here is probably a bug
// I calculate the screen coordinates of the nodes, then turn them back to local positions
// in the label-group
private void rearrangeLabels() {
Point2D screenCoord1 = node1.localToScreen(Point2D.ZERO);
Point2D screenCoord2 = node2.localToScreen(Point2D.ZERO);
Point2D groupCoord1 = labelGroup.screenToLocal(screenCoord1);
Point2D groupCoord2 = labelGroup.screenToLocal(screenCoord2);
nodeLabel1.setTranslateX(groupCoord1.getX());
nodeLabel1.setTranslateY(groupCoord1.getY());
nodeLabel2.setTranslateX(groupCoord2.getX());
nodeLabel2.setTranslateY(groupCoord2.getY());
}
private void createGraph() {
graphGroup = new Group();
node1 = new Sphere(5);
node2 = new Sphere(5);
node1.setTranslateX(-50);
node1.setTranslateY(-50);
node2.setTranslateX(150);
node2.setTranslateY(50);
edge = new Cylinder(2, 10);
connectNodesWithEdge(new Point3D(node1.getTranslateX(), node1.getTranslateY(), node1.getTranslateZ()),
new Point3D(node2.getTranslateX(), node2.getTranslateY(), node2.getTranslateZ()));
graphGroup.getChildren().addAll(node1, node2, edge);
}
private void connectNodesWithEdge(Point3D origin, Point3D target) {
Point3D yAxis = new Point3D(0, 1, 0);
Point3D diff = target.subtract(origin);
double height = diff.magnitude();
Point3D mid = target.midpoint(origin);
Translate moveToMidpoint = new Translate(mid.getX(), mid.getY(), mid.getZ());
Point3D axisOfRotation = diff.crossProduct(yAxis);
double angle = Math.acos(diff.normalize().dotProduct(yAxis));
Rotate rotateAroundCenter = new Rotate(-Math.toDegrees(angle), axisOfRotation);
this.edge.setHeight(height);
edge.getTransforms().addAll(moveToMidpoint, rotateAroundCenter);
}
private Point3D getCamLeftVector() {
Point3D left = cam.localToScene(-1, 0, 0);
Point3D current = cam.localToScene(0, 0, 0);
return left.subtract(current);
}
private Point3D getCamUpVector() {
Point3D up = cam.localToScene(0, -1, 0);
Point3D current = cam.localToScene(0, 0, 0);
return up.subtract(current);
}
private Point3D getCamBackwardVector() {
Point3D backward = cam.localToScene(0, 0, -1);
Point3D current = cam.localToScene(0, 0, 0);
return backward.subtract(current);
}
private void setCameraPosition(Point3D position) {
cam.setTranslateX(position.getX());
cam.setTranslateY(position.getY());
cam.setTranslateZ(position.getZ());
}
public static void main(String[] args) {
launch(args);
}
}
I'm drawing a Path in JavaFX and I would like to draw path (lineTo and arcTo) of this type:
Is there any simple way how to connect arcs with lines(and other arcs) to get a smooth path like this?
I don't know the perimeter of an arc, I know only the beginning point and ending point. In the picture are only half circles, I need to draw another types of arcTo as well.
The only idea, I've had so far was getting the direction of the end of the path and then counting and joining another arcTo / lineTo in this direction. However, I didn't found any method to do this as well.
A Cubic Bezier Curve is defined by four points, start, end, and two "control points" control1 and control2. It has the property that
it starts and ends at start and end
initially (i.e. at start) it is tangential to the line segment between start and control1
at end it is tangential to the line segment between control2 and end
The shape of the curve is also determined by the sizes of the line segments from start to control1 and control2 to end: roughly speaking these control the "speed" with which the line approaches the control points before turning towards the end points.
So to join two line segments with a smooth curve, you can use a cubic curve whose start is the end of the first line segment and whose end is the start of the second line segment. Compute the control points just by extending each line beyond its end (first line) or start (second line). Using the same length for the extensions will give a balanced look.
Here is an example. Run this code: drag the mouse across the pane to draw one line, then again to draw a second line, and the two lines will be connected by a cubic curve.
import javafx.application.Application;
import javafx.geometry.Point2D;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.CubicCurve;
import javafx.scene.shape.Line;
import javafx.stage.Stage;
public class JoinLineSegmentsWithCubic extends Application {
private Line unconnectedLine = null ;
private Line currentDraggingLine = null ;
#Override
public void start(Stage primaryStage) {
Pane pane = new Pane();
pane.setOnDragDetected(e -> {
currentDraggingLine = new Line(e.getX(), e.getY(), e.getX(), e.getY());
pane.getChildren().add(currentDraggingLine);
});
pane.setOnMouseDragged(e -> {
if (currentDraggingLine != null) {
currentDraggingLine.setEndX(e.getX());
currentDraggingLine.setEndY(e.getY());
}
});
pane.setOnMouseReleased(e -> {
if (currentDraggingLine != null) {
currentDraggingLine.setEndX(e.getX());
currentDraggingLine.setEndY(e.getY());
if (unconnectedLine != null) {
connect(unconnectedLine, currentDraggingLine, pane);
}
unconnectedLine = currentDraggingLine ;
currentDraggingLine = null ;
}
});
Scene scene = new Scene(pane, 600, 600);
primaryStage.setScene(scene);
primaryStage.show();
}
private void connect(Line line1, Line line2, Pane parent) {
Point2D line1Start = new Point2D(line1.getStartX(), line1.getStartY());
Point2D line1End = new Point2D(line1.getEndX(), line1.getEndY());
Point2D line2Start = new Point2D(line2.getStartX(), line2.getStartY());
Point2D line2End = new Point2D(line2.getEndX(), line2.getEndY());
double line1Length = line1End.subtract(line1Start).magnitude();
double line2Length = line2End.subtract(line2Start).magnitude();
// average length:
double aveLength = (line1Length + line2Length) / 2 ;
// extend line1 in direction of line1 for aveLength:
Point2D control1 = line1End.add(line1End.subtract(line1Start).normalize().multiply(aveLength));
// extend line2 in (reverse) direction of line2 for aveLength:
Point2D control2 = line2Start.add(line2Start.subtract(line2End).normalize().multiply(aveLength));
CubicCurve cc = new CubicCurve(
line1End.getX(), line1End.getY(),
control1.getX(), control1.getY(),
control2.getX(), control2.getY(),
line2Start.getX(), line2Start.getY());
cc.setStroke(Color.BLACK);
cc.setFill(null);
parent.getChildren().add(cc);
}
public static void main(String[] args) {
launch(args);
}
}
You can also incorporate a cubic Bezier curve into a path, using the path element CubicCurveTo. Here is an example using a Path, with vertical line segments connected by cubic bezier curves (the method generating the path will work for arbitrary line segments):
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.shape.CubicCurveTo;
import javafx.scene.shape.LineTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.Path;
import javafx.stage.Stage;
public class SmoothPathWithCubicBezier extends Application {
#Override
public void start(Stage primaryStage) {
double[] points = new double[24];
for (int i = 0; i < 24 ; i+=8) {
double x = (1 + i/8) * 200 ;
points[i] = x ;
points[i+1] = 200 ;
points[i+2] = x ;
points[i+3] = 400 ;
points[i+4] = x + 100 ;
points[i+5] = 400 ;
points[i+6] = x+ 100 ;
points[i+7] = 200 ;
}
Pane pane = new Pane();
pane.getChildren().add(createPath(points));
Scene scene = new Scene(pane, 800, 800);
primaryStage.setScene(scene);
primaryStage.show();
}
// points should be an array of length a multiple of four,
// defining a set of lines {startX1, startY1, endX1, endY1, startX2, ...}
// The path will consist of the straight line segments, joined by
// cubic beziers
private Path createPath(double[] points) {
Path path = new Path();
for (int i = 0 ; i < points.length; i+=4) {
double startX = points[i];
double startY = points[i+1];
double endX = points[i+2];
double endY = points[i+3];
if (i==0) {
MoveTo moveTo = new MoveTo(startX, startY);
moveTo.setAbsolute(true);
path.getElements().add(moveTo);
} else {
double lastStartX = points[i-4];
double lastStartY = points[i-3];
double lastEndX = points[i-2];
double lastEndY = points[i-1];
double lastLength = Math.sqrt((lastEndX-lastStartX)*(lastEndX-lastStartX)
+ (lastEndY-lastStartY)*(lastEndY-lastStartY));
double length = Math.sqrt((endX-startX)*(endX-startX)
+ (endY-startY)*(endY-startY));
double aveLength = (lastLength+length)/2;
double control1X = lastEndX + (lastEndX-lastStartX)*aveLength/lastLength ;
double control1Y = lastEndY + (lastEndY-lastStartY)*aveLength/lastLength ;
double control2X = startX - (endX-startX)*aveLength/length ;
double control2Y = startY - (endY-startY)*aveLength/length ;
CubicCurveTo cct = new CubicCurveTo(control1X, control1Y, control2X, control2Y, startX, startY);
cct.setAbsolute(true);
path.getElements().add(cct);
}
LineTo lineTo = new LineTo(endX, endY);
lineTo.setAbsolute(true);
path.getElements().add(lineTo);
}
return path ;
}
public static void main(String[] args) {
launch(args);
}
}
This gives
I have a line which has points (x1,y1) and (x2,y2). I wanted to attach a Button to it, it should align with the line by rotating based on the line segment points. I need some help in calculating the rotation angle for the Button.
You can use the Math.atan2(dy, dx) to get the angle theta from the conversion of rectangular coordinates (x, y) to polar coordinates (r, theta). Later use it to convert it to degrees.
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.StackPane;
import javafx.scene.shape.Line;
import javafx.stage.Stage;
public class Main extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) throws Exception {
double startX = 100;
double endX = 250;
double startY = 150;
double endY = 250;
Line line = new Line(startX, startY, endX, endY);
Button button = new Button("Button");
double rad = Math.atan2(endY - startY, endX - startX);
double degree = rad * 180/Math.PI;
button.setRotate(degree);
StackPane box = new StackPane(line, button);
Scene scene = new Scene(box, 500, 500);
primaryStage.setScene(scene);
primaryStage.show();
}
}
OutPut