Is there any way in JavaFX to get the x,y coordinates of the path i draw on screen on a Canvas?
I have an app that draw some Circles on a Canvas and then it connects the Circles with Lines.
After that i can change the positions of the Circles by dragging the nodes and the program is always redrawing the Lines between the Circles on the Canvas
But now i want to draw a a QuadCurve between those Circles.
public void drawQuadCurveOnCanvas(double startX, double startY, double endX, double endY, double controlX, double controlY) {
// Set line width
lineDraw.setLineWidth(Constants.LINE_THICKNESS);
// Set the Color
lineDraw.setStroke(Constants.DRAG_COLOR);
// Start the Path
lineDraw.beginPath();
lineDraw.moveTo(startX, startY);
lineDraw.quadraticCurveTo(controlX, controlY, endX, endY);
// Draw the Path
lineDraw.stroke();
}
I draw the QuadCurve with one of the Circles as the control point and other two Circles as the start and end coordinates.
So i wanted to get the x,y coordinates along the QuadCurve i just draw so i can create some Circles on those coordinates and after that when i connect those Circles with Lines i get the same QuadCurve.
I don't know if i explained myself well, anyone with any idea how to accomplish what i want?
For a Quadratic Bézier Curve, any point on the curve can be written as
x = (1-t)*(1-t)*startX + 2*t*(1-t)*controlX + t*t*endX
y = (1-t)*(1-t)*startY + 2*t*(1-t)*controlY + t*t*endY
where 0≤t≤1.
To plot a number of circles along a Quadratic Bézier Curve, just pick some values between 0 and 1, and create a bunch of circles whose centers are given by the coordinates in the formula above. You can use bindings if the control points might change. Here's a quick example that creates circles bound to the coordinates along the curve, with line segments whose end points are in turn bound to the circles.
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.Spinner;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Line;
import javafx.stage.Stage;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public class BezierCurve extends Application {
private Circle start;
private Circle end;
private Circle control;
private Pane drawingPane;
private List<Circle> points;
private List<Line> segments;
private Circle createPoint(double t) {
Circle c = new Circle(4);
c.setFill(Color.BLACK);
c.centerXProperty().bind(Bindings.createDoubleBinding(
() -> (1-t)*(1-t)*start.getCenterX()
+ 2*t*(1-t)*control.getCenterX()
+ t*t*end.getCenterX(),
start.centerXProperty(),
end.centerXProperty(),
control.centerXProperty())
);
c.centerYProperty().bind(Bindings.createDoubleBinding(
() -> (1-t)*(1-t)*start.getCenterY()
+ 2*t*(1-t)*control.getCenterY()
+ t*t*end.getCenterY(),
start.centerYProperty(),
end.centerYProperty(),
control.centerYProperty())
);
return c;
}
private Line createSegment(Circle start, Circle end) {
Line segment = new Line();
segment.startXProperty().bind(start.centerXProperty());
segment.startYProperty().bind(start.centerYProperty());
segment.endXProperty().bind(end.centerXProperty());
segment.endYProperty().bind(end.centerYProperty());
return segment ;
}
#Override
public void start(Stage stage) throws IOException {
drawingPane = new Pane();
points = new ArrayList<>();
segments = new ArrayList<>();
start = new Circle(100,100, 10, Color.GREEN);
end = new Circle(700, 100, 10, Color.GREEN);
control = new Circle(400, 500, 10, Color.GREEN);
for (Circle c : List.of(start, end, control)) setUpDragging(c);
Spinner<Integer> numSegmentsSpinner = new Spinner(5, Integer.MAX_VALUE, 25, 5);
numSegmentsSpinner.setEditable(true);
numSegmentsSpinner.valueProperty().addListener(
(obs, oldValue, newValue) -> populatePointsAndSegments(newValue)
);
HBox ctrls = new HBox(5, new Label("Number of segments:"), numSegmentsSpinner);
populatePointsAndSegments(numSegmentsSpinner.getValue()); ;
drawingPane.getChildren().addAll(start, end, control);
BorderPane root = new BorderPane();
root.setCenter(drawingPane);
root.setTop(ctrls);
Scene scene = new Scene(root, 800, 800);
stage.setScene(scene);
stage.show();
}
private void populatePointsAndSegments(int numSegments) {
drawingPane.getChildren().removeAll(points);
drawingPane.getChildren().removeAll(segments);
points.clear();
segments.clear();
Circle previousCircle = start ;
for (int i = 1 ; i < numSegments; i++) {
double t = 1.0 * i / numSegments ;
Circle c = createPoint(t);
points.add(c);
segments.add(createSegment(previousCircle, c));
previousCircle = c ;
}
segments.add(createSegment(previousCircle, end));
drawingPane.getChildren().addAll(points);
drawingPane.getChildren().addAll(segments);
}
private void setUpDragging(Circle c) {
c.setOnMouseDragged(e -> {
c.setCenterX(e.getX());
c.setCenterY(e.getY());
});
}
public static void main(String[] args) {
launch();
}
}
Related
Looking this post, I've tried to implement in javaFX, with many difficulties, a Scatter Chart 3D where the grid is my x,y and z axis and the spheres are my points.
How Can I put a legend, axis labels and the range numbers along the axis? I can use only javaFX without external library.
I'm desperate.. I'm trying for days..without results
Please:help me
Thanks.
Code
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.scene.Group;
import javafx.scene.PerspectiveCamera;
import javafx.scene.Scene;
import javafx.scene.SceneAntialiasing;
import javafx.scene.input.ScrollEvent;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.scene.paint.PhongMaterial;
import javafx.scene.shape.Line;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.Sphere;
import javafx.scene.transform.Rotate;
import javafx.stage.Stage;
public class GraphingData extends Application {
private static Random rnd = new Random();
// size of graph
int graphSize = 400;
// variables for mouse interaction
private double mousePosX, mousePosY;
private double mouseOldX, mouseOldY;
private final Rotate rotateX = new Rotate(150, Rotate.X_AXIS);
private final Rotate rotateY = new Rotate(120, Rotate.Y_AXIS);
#Override
public void start(Stage primaryStage) {
// create axis walls
Group grid = createGrid(graphSize);
// initial cube rotation
grid.getTransforms().addAll(rotateX, rotateY);
// add objects to scene
StackPane root = new StackPane();
root.getChildren().add(grid);
root.setStyle( "-fx-border-color: red;");
// create bars
double gridSizeHalf = graphSize / 2;
double size = 30;
//Drawing a Sphere
Sphere sphere = new Sphere();
//Setting the properties of the Sphere
sphere.setRadius(10.0);
sphere.setTranslateX(-50);
sphere.setTranslateY(-50);
//Preparing the phong material of type specular color
PhongMaterial material6 = new PhongMaterial();
//setting the specular color map to the material
material6.setDiffuseColor(Color.GREEN);
sphere.setMaterial(material6);
grid.getChildren().addAll(sphere);
// scene
Scene scene = new Scene(root, 1600, 900, true, SceneAntialiasing.BALANCED);
scene.setCamera(new PerspectiveCamera());
scene.setOnMousePressed(me -> {
mouseOldX = me.getSceneX();
mouseOldY = me.getSceneY();
});
scene.setOnMouseDragged(me -> {
mousePosX = me.getSceneX();
mousePosY = me.getSceneY();
rotateX.setAngle(rotateX.getAngle() - (mousePosY - mouseOldY));
rotateY.setAngle(rotateY.getAngle() + (mousePosX - mouseOldX));
mouseOldX = mousePosX;
mouseOldY = mousePosY;
});
makeZoomable(root);
primaryStage.setResizable(false);
primaryStage.setScene(scene);
primaryStage.show();
}
/**
* Axis wall
*/
public static class Axis extends Pane {
Rectangle wall;
public Axis(double size) {
// wall
// first the wall, then the lines => overlapping of lines over walls
// works
wall = new Rectangle(size, size);
getChildren().add(wall);
// grid
double zTranslate = 0;
double lineWidth = 1.0;
Color gridColor = Color.RED;
for (int y = 0; y <= size; y += size / 10) {
Line line = new Line(0, 0, size, 0);
line.setStroke(gridColor);
line.setFill(gridColor);
line.setTranslateY(y);
line.setTranslateZ(zTranslate);
line.setStrokeWidth(lineWidth);
getChildren().addAll(line);
}
for (int x = 0; x <= size; x += size / 10) {
Line line = new Line(0, 0, 0, size);
line.setStroke(gridColor);
line.setFill(gridColor);
line.setTranslateX(x);
line.setTranslateZ(zTranslate);
line.setStrokeWidth(lineWidth);
getChildren().addAll(line);
}
}
public void setFill(Paint paint) {
wall.setFill(paint);
}
}
public void makeZoomable(StackPane control) {
final double MAX_SCALE = 20.0;
final double MIN_SCALE = 0.1;
control.addEventFilter(ScrollEvent.ANY, new EventHandler<ScrollEvent>() {
#Override
public void handle(ScrollEvent event) {
double delta = 1.2;
double scale = control.getScaleX();
if (event.getDeltaY() < 0) {
scale /= delta;
} else {
scale *= delta;
}
scale = clamp(scale, MIN_SCALE, MAX_SCALE);
control.setScaleX(scale);
control.setScaleY(scale);
event.consume();
}
});
}
/**
* Create axis walls
*
* #param size
* #return
*/
private Group createGrid(int size) {
Group cube = new Group();
// size of the cube
Color color = Color.LIGHTGRAY;
List<Axis> cubeFaces = new ArrayList<>();
Axis r;
// back face
r = new Axis(size);
r.setFill(color.deriveColor(0.0, 1.0, (1 - 0.5 * 1), 1.0));
r.setTranslateX(-0.5 * size);
r.setTranslateY(-0.5 * size);
r.setTranslateZ(0.5 * size);
cubeFaces.add(r);
// bottom face
r = new Axis(size);
r.setFill(color.deriveColor(0.0, 1.0, (1 - 0.4 * 1), 1.0));
r.setTranslateX(-0.5 * size);
r.setTranslateY(0);
r.setRotationAxis(Rotate.X_AXIS);
r.setRotate(90);
cubeFaces.add(r);
// right face
r = new Axis(size);
r.setFill(color.deriveColor(0.0, 1.0, (1 - 0.3 * 1), 1.0));
r.setTranslateX(-1 * size);
r.setTranslateY(-0.5 * size);
r.setRotationAxis(Rotate.Y_AXIS);
r.setRotate(90);
// cubeFaces.add( r);
// left face
r = new Axis(size);
r.setFill(color.deriveColor(0.0, 1.0, (1 - 0.2 * 1), 1.0));
r.setTranslateX(0);
r.setTranslateY(-0.5 * size);
r.setRotationAxis(Rotate.Y_AXIS);
r.setRotate(90);
cubeFaces.add(r);
// top face
r = new Axis(size);
r.setFill(color.deriveColor(0.0, 1.0, (1 - 0.1 * 1), 1.0));
r.setTranslateX(-0.5 * size);
r.setTranslateY(-1 * size);
r.setRotationAxis(Rotate.X_AXIS);
r.setRotate(90);
// cubeFaces.add( r);
// front face
r = new Axis(size);
r.setFill(color.deriveColor(0.0, 1.0, (1 - 0.1 * 1), 1.0));
r.setTranslateX(-0.5 * size);
r.setTranslateY(-0.5 * size);
r.setTranslateZ(-0.5 * size);
// cubeFaces.add( r);
cube.getChildren().addAll(cubeFaces);
return cube;
}
public static double normalizeValue(double value, double min, double max, double newMin, double newMax) {
return (value - min) * (newMax - newMin) / (max - min) + newMin;
}
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;
}
public static Color randomColor() {
return Color.rgb(rnd.nextInt(255), rnd.nextInt(255), rnd.nextInt(255));
}
public static void main(String[] args) {
launch(args);
}
}
Here's a basic idea to create some measures on the axes. It is not production-ready but should give you enough to start with.
private Group createGrid(int size) {
// existing code omitted...
cube.getChildren().addAll(cubeFaces);
double gridSizeHalf = size / 2;
double labelOffset = 30 ;
double labelPos = gridSizeHalf - labelOffset ;
for (double coord = -gridSizeHalf ; coord < gridSizeHalf ; coord+=50) {
Text xLabel = new Text(coord, labelPos, String.format("%.0f", coord));
xLabel.setTranslateZ(labelPos);
xLabel.setScaleX(-1);
Text yLabel = new Text(labelPos, coord, String.format("%.0f", coord));
yLabel.setTranslateZ(labelPos);
yLabel.setScaleX(-1);
Text zLabel = new Text(labelPos, labelPos, String.format("%.0f", coord));
zLabel.setTranslateZ(coord);
cube.getChildren().addAll(xLabel, yLabel, zLabel);
zLabel.setScaleX(-1);
}
return cube;
}
I would just place a legend outside the graph, which would just be a 2D grid pane not rotating...
I know this question is getting old but 2D labels in a JavaFX 3D scene is a topic that comes up a lot and I never see it answered "the right way".
Translating the labels like in James_D's answer will translate into 3D space a 2D label which will look correct until you move the camera. Assuming you want a scatter chart that doesn't move or rotate then this will be fine. Other wise you will need to automatically transform the 2D labels whenever you move your camera. (ie... the mouse handler). You could remove your scatter chart and readd the whole thing to the scene each time but that will be murder on your heap memory and won't be feasible for data sets of any real useful size.
The right way to do it is to use OpenGL or DirectDraw text renders which redraw the labels on each render loop pass but JavaFX 3D doesn't give you access (currently). So the "right way in JavaFX" is to float 2D labels on top of a 3D subscene and then translate them whenever the camera moves. This requires that you transform the 3D coordinate projection of the 3D location you want the label to a 2D screen projection.
To generically manage 2D labels connected to a Point3D in JavaFX 3D you need to do a transform along the following:
Point3D coordinates = node.localToScene(javafx.geometry.Point3D.ZERO);
SubScene oldSubScene = NodeHelper.getSubScene(node);
coordinates = SceneUtils.subSceneToScene(oldSubScene, coordinates);
double x = coordinates.getX();
double y = coordinates.getY();
label.getTransforms().setAll(new Translate(x, y));
Where the node is some actual 3D object already in the 3D subscene. For my applications I simply use a Sphere of an extremely small size it cannot be seen. If you were to follow James_D's example, you could translate the sphere(s) to the same locations that you translated the original axis labels.
The label is a standard JavaFX 2D label that you add to your scene... typically through a StackPane such that the labels are floating on top of the 3D subscene.
Now whenever the camera moves/rotates, this causes this transform to be called which slides the label on the 2D layer. Without direct access to the underlying GL or DD calls this is pretty much the only way to do something like this in JavaFX 3D but it works pretty well.
Here is a video example of it working.
Here is an open source example of implementing a simple version of floating 2D labels. (Warning, I'm the contributing author for the sample, not trying to promote the library.)
First of all, I want to shoot a plane with a cannon.
I've setted this Timeline for the trajectory, but I don't see the bullet on my Scene. It's very likely that my trajectory's code isn't correct. I tried to look on the internet about formula for projectile motion, but I understand nothing about physics;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Bounds;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.image.ImageView;
import javafx.scene.layout.AnchorPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;
import javafx.util.Duration;
public class Game_1 extends Application {
private final double gravity = 9.81;
private Timeline timeline;
private ImageView plane;
private Circle circle;
private AnchorPane ap;
#Override
public void start(Stage primaryStage) {
primaryStage.setTitle("Test");
Group group = new Group();
Scene scene = new Scene(group, 600, 350);
scene.setFill(Color.BLACK);
primaryStage.setScene(scene);
primaryStage.show();
}
private void shoot() {
double x = 65.0f;
double y = 408;
double speed = 200;
double t = 2;
double angle = -45;
double dx = Math.cos(angle) * speed;
double dy = Math.sin(angle) * speed;
circle = new Circle(x, y, 5, Color.BLACK);
double x2 = x + dx * t;
double y2 = (Math.tan(angle) * y - (gravity / (2 * Math.pow(speed, 2) * Math.cos(angle))) * Math.pow(x, 2));
timeline = new Timeline();
KeyValue xKV = new KeyValue(circle.centerXProperty(), x2);
KeyValue yKV = new KeyValue(circle.centerYProperty(), y2, new Interpolator() {
#Override
protected double curve(double t) {
return y + dy * t - 0.5 * gravity * t * t;
}
});
KeyFrame xKF = new KeyFrame(Duration.seconds(t), xKV);
KeyFrame yKF = new KeyFrame(Duration.seconds(t), yKV);
timeline.getKeyFrames().addAll(xKF, yKF);
ap.getChildren().add(circle);
timeline.play();
collision();
}
private void collision() {
circle.boundsInParentProperty().addListener((ObservableValue<? extends Bounds> arg0, Bounds oldValue2, Bounds newValue2) -> {
if (circle.getBoundsInParent().intersects(plane.getBoundsInParent())) {
timeline.stop();
ap.getChildren().remove(circle);
}
});
}
}
The curve method should map to the interval [0, 1]. Your method however maps to much higher values. The value val at time t of a animation from t0 to t1 for a interpolator i given start value val0 and end value val1 is calculated as follows:
val = val0 + (val1 - val0) * i.curve((t - t0) / (t1 - t0))
The parameter of the curve method is the relative position in the time interval (0 = start of animation; 1 = end of animation). The result of the method is used to determine how close the value is to the end value (0 = still at the start value; 1 = at the end value).
Therefore you should probably calculate the top point hMax in the cannonball's curve (as described e.g. here on Wikipedia) and use a different interpolator:
Interpolator interpolator = new Interpolator() {
#Override
protected double curve(double t) {
// parabola with zeros at t=0 and t=1 and a maximum of 1 at t=0.5
return 4 * t * (1 - t);
}
};
KeyValue yKV = new KeyValue(circle.centerYProperty(), hMax, interpolator);
Note that upward movement means decreasing the y coordinate for the UI so in this case hMax should be smaller than the y value at the start.
Appart from that your shoot method is never called and some fields are not initialized which would result in a NPE in case it was called. Furthermore if those 2 issues are fixed, a black circle on a black background will be hard to see...
Example
Note that this is not using any physical fromulae and instead just uses some values chosen by me:
#Override
public void start(Stage primaryStage) {
Circle circle = new Circle(10);
circle.setManaged(false);
Pane pane = new Pane(circle);
circle.setCenterX(20);
circle.setCenterY(800);
Timeline timeline = new Timeline(new KeyFrame(Duration.ZERO,
new KeyValue(circle.centerXProperty(), 20),
new KeyValue(circle.centerYProperty(), 800)
), new KeyFrame(Duration.seconds(3),
new KeyValue(circle.centerXProperty(), 380),
new KeyValue(circle.centerYProperty(), 10, new Interpolator() {
#Override
protected double curve(double t) {
// parabola with zeros at t=0 and t=1 and a maximum of 1 at t=0.5
return 4 * t * (1 - t);
}
})
)
);
Scene scene = new Scene(pane, 400, 800);
scene.setOnMouseClicked(evt -> timeline.playFromStart());
primaryStage.setScene(scene);
primaryStage.show();
}
Note that Interpolator.curve is supposed to return 0 for parameter 0 and 1 for parameter 1. Anything else will probably result in jumps, should the property be animated further. Maybe the y-movement in 2 parts would be more appropriate, in case you want to move the ball around after the animation is finished.
I.e.
Interpolator 1: t * (2 - t)
Interpolator 2: t * t
using half the time interval each with end values of the top and start y coordinate of the curve respectively.
I'm new to Javafx and I'm experimenting with animations. Following this, I've created a curve with two anchor points. Moving the anchor points changes the shape of the curve. Next, I followed this to create an animation where a square follows the curve from one end point to the other.
Combining those two works fine, except when I move one of the anchor points! My square keeps following the original trajectory. Any suggestions on how to fix this? I don't want to restart the animation; the square should just continue moving along its path without visible interruption.
Here's a complete working example:
import javafx.animation.PathTransition;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.beans.property.DoubleProperty;
import javafx.scene.Cursor;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.paint.Color;
import javafx.scene.shape.*;
import javafx.stage.Stage;
import javafx.util.Duration;
public class CurveAnimation extends Application {
public static void main(String[] args) throws Exception { launch(args); }
#Override
public void start(final Stage stage) throws Exception {
//Create a curve
CubicCurve curve = new CubicCurve();
curve.setStartX(100);
curve.setStartY(100);
curve.setControlX1(150);
curve.setControlY1(50);
curve.setControlX2(250);
curve.setControlY2(150);
curve.setEndX(300);
curve.setEndY(100);
curve.setStroke(Color.FORESTGREEN);
curve.setStrokeWidth(4);
curve.setFill(Color.CORNSILK.deriveColor(0, 1.2, 1, 0.6));
//Create anchor points at each end of the curve
Anchor start = new Anchor(Color.PALEGREEN, curve.startXProperty(), curve.startYProperty());
Anchor end = new Anchor(Color.TOMATO, curve.endXProperty(), curve.endYProperty());
//Create object that follows the curve
Rectangle rectPath = new Rectangle (0, 0, 40, 40);
rectPath.setArcHeight(25);
rectPath.setArcWidth(25);
rectPath.setFill(Color.ORANGE);
//Create the animation
PathTransition pathTransition = new PathTransition();
pathTransition.setDuration(Duration.millis(2000));
pathTransition.setPath(curve);
pathTransition.setNode(rectPath);
pathTransition.setOrientation(PathTransition.OrientationType.ORTHOGONAL_TO_TANGENT);
pathTransition.setCycleCount(Timeline.INDEFINITE);
pathTransition.setAutoReverse(true);
pathTransition.play();
Group root = new Group();
root.getChildren().addAll(curve, start, end, rectPath);
stage.setScene(new Scene( root, 400, 400, Color.ALICEBLUE));
stage.show();
}
/**
* Create draggable anchor points
*/
class Anchor extends Circle {
Anchor(Color color, DoubleProperty x, DoubleProperty y) {
super(x.get(), y.get(), 10);
setFill(color.deriveColor(1, 1, 1, 0.5));
setStroke(color);
setStrokeWidth(2);
setStrokeType(StrokeType.OUTSIDE);
x.bind(centerXProperty());
y.bind(centerYProperty());
enableDrag();
}
// make a node movable by dragging it around with the mouse.
private void enableDrag() {
final Delta dragDelta = new Delta();
setOnMousePressed(mouseEvent -> {
// record a delta distance for the drag and drop operation.
dragDelta.x = getCenterX() - mouseEvent.getX();
dragDelta.y = getCenterY() - mouseEvent.getY();
getScene().setCursor(Cursor.MOVE);
});
setOnMouseReleased(mouseEvent -> getScene().setCursor(Cursor.HAND));
setOnMouseDragged(mouseEvent -> {
double newX = mouseEvent.getX() + dragDelta.x;
if (newX > 0 && newX < getScene().getWidth()) {
setCenterX(newX);
}
double newY = mouseEvent.getY() + dragDelta.y;
if (newY > 0 && newY < getScene().getHeight()) {
setCenterY(newY);
}
});
setOnMouseEntered(mouseEvent -> {
if (!mouseEvent.isPrimaryButtonDown()) {
getScene().setCursor(Cursor.HAND);
}
});
setOnMouseExited(mouseEvent -> {
if (!mouseEvent.isPrimaryButtonDown()) {
getScene().setCursor(Cursor.DEFAULT);
}
});
}
// records relative x and y co-ordinates.
private class Delta { double x, y; }
}
}
The PathTransition apparently just copies the values from the path when you call setPath, and doesn't observe them if they change.
To do what you want, you will need to use a Transition and implement the interpolation yourself. The interpolation must take a value double t and set the translateX and translateY properties of the node so that its center is on the curve with parameter t. If you want the ORTHOGONAL_TO_TANGENT orientation, you will also need to set the rotate property of the node to the angle of the tangent of the cubic curve to the positive horizontal. By computing these in the interpolate method, you can simply refer to the current control points of the curve.
To do the computation, you need to know a bit of geometry. The point on a linear Bezier curve with control points (i.e. start and end) P0 and P1 at parameter t is given by
B(t; P0, P1) = (1-t)*P0 + t*P1
You can compute higher order Bezier curves recursively by
B(t; P0, P1, ..., Pn) = (1-t)*B(P0, P1, ..., P(n-1); t) + t*B(P1, P2, ..., Pn;t)
and just differentiate both of those to get the tangent for the linear curve (which is, from geometrical consideration, obviously just P1-P0) and for the recursive relationship:
B'(t; P0, P1) = -P0 + P1
and
B'(t; P0, P1, ..., Pn) = -B(t; P0, ..., P(n-1)) + (1-t)B'(t; P0, ..., P(n-1))
+ B(t; P1, ..., Pn) + tB'(t; P1, ..., Pn)
Here is this implemented in code:
import javafx.animation.Animation;
import javafx.animation.PathTransition;
import javafx.animation.Timeline;
import javafx.animation.Transition;
import javafx.application.Application;
import javafx.beans.property.DoubleProperty;
import javafx.beans.value.ChangeListener;
import javafx.geometry.Point2D;
import javafx.scene.Cursor;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.CubicCurve;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.StrokeType;
import javafx.stage.Stage;
import javafx.util.Duration;
public class CurveAnimation extends Application {
public static void main(String[] args) throws Exception { launch(args); }
#Override
public void start(final Stage stage) throws Exception {
//Create a curve
CubicCurve curve = new CubicCurve();
curve.setStartX(100);
curve.setStartY(100);
curve.setControlX1(150);
curve.setControlY1(50);
curve.setControlX2(250);
curve.setControlY2(150);
curve.setEndX(300);
curve.setEndY(100);
curve.setStroke(Color.FORESTGREEN);
curve.setStrokeWidth(4);
curve.setFill(Color.CORNSILK.deriveColor(0, 1.2, 1, 0.6));
//Create anchor points at each end of the curve
Anchor start = new Anchor(Color.PALEGREEN, curve.startXProperty(), curve.startYProperty());
Anchor end = new Anchor(Color.TOMATO, curve.endXProperty(), curve.endYProperty());
//Create object that follows the curve
Rectangle rectPath = new Rectangle (0, 0, 40, 40);
rectPath.setArcHeight(25);
rectPath.setArcWidth(25);
rectPath.setFill(Color.ORANGE);
Transition transition = new Transition() {
{
setCycleDuration(Duration.millis(2000));
}
#Override
protected void interpolate(double frac) {
Point2D start = new Point2D(curve.getStartX(), curve.getStartY());
Point2D control1 = new Point2D(curve.getControlX1(), curve.getControlY1());
Point2D control2 = new Point2D(curve.getControlX2(), curve.getControlY2());
Point2D end = new Point2D(curve.getEndX(), curve.getEndY());
Point2D center = bezier(frac, start, control1, control2, end);
double width = rectPath.getBoundsInLocal().getWidth() ;
double height = rectPath.getBoundsInLocal().getHeight() ;
rectPath.setTranslateX(center.getX() - width /2);
rectPath.setTranslateY(center.getY() - height / 2);
Point2D tangent = bezierDeriv(frac, start, control1, control2, end);
double angle = Math.toDegrees(Math.atan2(tangent.getY(), tangent.getX()));
rectPath.setRotate(angle);
}
};
transition.setCycleCount(Animation.INDEFINITE);
transition.setAutoReverse(true);
transition.play();
Group root = new Group();
root.getChildren().addAll(curve, start, end, rectPath);
stage.setScene(new Scene( root, 400, 400, Color.ALICEBLUE));
stage.show();
}
private Point2D bezier(double t, Point2D... points) {
if (points.length == 2) {
return points[0].multiply(1-t).add(points[1].multiply(t));
}
Point2D[] leftArray = new Point2D[points.length - 1];
System.arraycopy(points, 0, leftArray, 0, points.length - 1);
Point2D[] rightArray = new Point2D[points.length - 1];
System.arraycopy(points, 1, rightArray, 0, points.length - 1);
return bezier(t, leftArray).multiply(1-t).add(bezier(t, rightArray).multiply(t));
}
private Point2D bezierDeriv(double t, Point2D... points) {
if (points.length == 2) {
return points[1].subtract(points[0]);
}
Point2D[] leftArray = new Point2D[points.length - 1];
System.arraycopy(points, 0, leftArray, 0, points.length - 1);
Point2D[] rightArray = new Point2D[points.length - 1];
System.arraycopy(points, 1, rightArray, 0, points.length - 1);
return bezier(t, leftArray).multiply(-1).add(bezierDeriv(t, leftArray).multiply(1-t))
.add(bezier(t, rightArray)).add(bezierDeriv(t, rightArray).multiply(t));
}
/**
* Create draggable anchor points
*/
class Anchor extends Circle {
Anchor(Color color, DoubleProperty x, DoubleProperty y) {
super(x.get(), y.get(), 10);
setFill(color.deriveColor(1, 1, 1, 0.5));
setStroke(color);
setStrokeWidth(2);
setStrokeType(StrokeType.OUTSIDE);
x.bind(centerXProperty());
y.bind(centerYProperty());
enableDrag();
}
// make a node movable by dragging it around with the mouse.
private void enableDrag() {
final Delta dragDelta = new Delta();
setOnMousePressed(mouseEvent -> {
// record a delta distance for the drag and drop operation.
dragDelta.x = getCenterX() - mouseEvent.getX();
dragDelta.y = getCenterY() - mouseEvent.getY();
getScene().setCursor(Cursor.MOVE);
});
setOnMouseReleased(mouseEvent -> getScene().setCursor(Cursor.HAND));
setOnMouseDragged(mouseEvent -> {
double newX = mouseEvent.getX() + dragDelta.x;
if (newX > 0 && newX < getScene().getWidth()) {
setCenterX(newX);
}
double newY = mouseEvent.getY() + dragDelta.y;
if (newY > 0 && newY < getScene().getHeight()) {
setCenterY(newY);
}
});
setOnMouseEntered(mouseEvent -> {
if (!mouseEvent.isPrimaryButtonDown()) {
getScene().setCursor(Cursor.HAND);
}
});
setOnMouseExited(mouseEvent -> {
if (!mouseEvent.isPrimaryButtonDown()) {
getScene().setCursor(Cursor.DEFAULT);
}
});
}
// records relative x and y co-ordinates.
private class Delta { double x, y; }
}
}
I don't know if it's the code, the math, or just the animation, but this is somehow deeply satisfying...
I am trying to layout my nodes like this:
Here is my current layout, called CircularPane:
import javafx.geometry.HPos;
import javafx.geometry.VPos;
import javafx.scene.Node;
import javafx.scene.layout.Pane;
public class CircularPane extends Pane {
#Override
protected void layoutChildren() {
final int radius = 50;
final double increment = 360 / getChildren().size();
double degreese = 0;
for (Node node : getChildren()) {
double x = radius * Math.cos(Math.toRadians(degreese)) + getWidth() / 2;
double y = radius * Math.sin(Math.toRadians(degreese)) + getHeight() / 2;
layoutInArea(node, x - node.getBoundsInLocal().getWidth() / 2, y - node.getBoundsInLocal().getHeight() / 2, getWidth(), getHeight(), 0.0, HPos.LEFT, VPos.TOP);
degreese += increment;
}
}
}
Here is my main class:
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.stage.Stage;
public class Main extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage stage) {
CircularPane pane = new CircularPane();
for(int i = 0; i < 6; i++) {
Button button = new Button("" + i);
pane.getChildren().add(button);
}
stage.setScene(new Scene(pane));
stage.show();
}
}
And here is my current display:
The nodes are not at the bottom touching, they are equally spread out around the circle. I want to make it so they go to the bottom, but can't figure out how to.
Your approach to layout the buttons over a circle is correct, but in this line you are defining how they will be layouted:
final double increment = 360 / getChildren().size();
This gives the same angle between any two buttons refered from the center of the circle! And that's why you get your current display.
If you want to layout the nodes like in your figure, if I get it right, these are the conditions:
Every node has its center over the circle
The nodes are equally separated in horizontal: the horizontal gap goes from 0 to some value.
The initial gap from the circle to the first node goes from 0 to some value.
The size of each node may be adjusted to fulfill the previous conditions
So let's define some fields for those values, and adjust the size of the pane:
class CircularPane extends Pane {
private final double radius;
private final double ext_gap;
private final double int_gap;
public CircularPane(double radius, double ext_gap, double int_gap){
this.radius=radius;
this.ext_gap=ext_gap;
this.int_gap=int_gap;
setMinSize(2*radius, 2d*radius);
setPrefSize(2*radius, 2d*radius);
setMaxSize(2*radius, 2d*radius);
}
}
And now, given any n buttons, the above conditions can be turned into one single equation that solves the size of the node. If the total available length (2*radius) minus two exterior gaps (2*ext_gap) is the same as n buttons of size buttonSize and n-1 interior gaps (int_size), then, the size of every button has to be:
#Override
protected void layoutChildren() {
int n=getChildren().size();
double buttonSize = (2*radius-2*ext_gap-(n-1)*int_gap)/n;
}
Finally, now you can set the size of the button and layout every node, just by increasing the x coordinate (by the size of the button plus an inner gap), and then getting the y coordinate from the circle equation:
#Override
protected void layoutChildren() {
int n=getChildren().size();
double buttonSize = (2*radius-2*ext_gap-(n-1)*int_gap)/n;
double x=ext_gap+buttonSize/2d, y;
for (Node node : getChildren()) {
((Button)node).setMinSize(buttonSize, buttonSize);
((Button)node).setPrefSize(buttonSize, buttonSize);
((Button)node).setMaxSize(buttonSize, buttonSize);
node.setStyle("-fx-font-size: "+Math.round(buttonSize/3));
node.setManaged(false);
y=getHeight()/2d+Math.sqrt(radius*radius-Math.pow(x-radius,2d));
layoutInArea(node, x-buttonSize/2d, y-buttonSize/2d, getWidth(), getHeight(), 0.0, HPos.LEFT, VPos.TOP);
x+=buttonSize+int_gap;
}
}
Note that you can also change the size of the font, to get a visible number for any size of the button.
Note also that node.setManaged(false); avoids the calls to layoutChildren() when you click the buttons (due to changes in the size of the clicked button when being focused or clicked).
Finally this will create the circular pane and draw a circle:
#Override
public void start(Stage primaryStage) {
CircularPane pane = new CircularPane(200,20,10);
for(int i = 0; i < 6; i++) {
Button button = new Button("" + (i+1));
pane.getChildren().add(button);
}
Circle circle = new Circle(200);
circle.setFill(null);
circle.setStroke(Color.BLACK);
StackPane stack=new StackPane(circle,pane);
Scene scene = new Scene(stack, 500, 500);
primaryStage.setScene(scene);
primaryStage.show();
}
With this result:
So ive got this code where im trying to get the moving circles to bounce on the walls so they dont go outside the stage. Ive tried to do it with the moveCircle method but i feel really out of my comfort zone.
import javafx.animation.Animation.Status;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.geometry.Orientation;
import javafx.geometry.Rectangle2D;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollBar;
import javafx.scene.effect.Light;
import javafx.scene.effect.Lighting;
import javafx.scene.input.KeyCode;
import javafx.scene.layout.BorderPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.stage.Screen;
import javafx.stage.Stage;
import javafx.util.Duration;
public class TimelineSample extends Application {
Timeline timeline;
private void init(Stage primaryStage) {
double height = primaryStage.getHeight();
double width = primaryStage.getWidth();
BorderPane root = new BorderPane();
primaryStage.setScene(new Scene(root, width, height));
double radius = 30;
Circle circle = new Circle(radius, radius, radius, Color.BLUE);
Circle circle2 = new Circle(radius, radius, radius, Color.RED);
Light.Distant light = new Light.Distant();
light.setAzimuth(-135.0);
Label label = new Label(
"Space för starta spelet\nSpace för att pausa spelet\nTryck på cirklarna för att byta färg på dem");
Label label2 = new Label("44");
root.setStyle("-fx-background-color: green;");
label2.setStyle(("-fx-padding : 100;"));
root.setBottom(label2);
root.setCenter(label);
Screen screen = Screen.getPrimary();
Rectangle2D bounds = screen.getVisualBounds();
ScrollBar sbSpeed = new ScrollBar();
sbSpeed.setMax(50);
sbSpeed.setValue(25);
sbSpeed.setOrientation(Orientation.VERTICAL);
circle.opacityProperty().bind(sbSpeed.valueProperty().divide(30));
circle2.opacityProperty().bind(sbSpeed.valueProperty().divide(30));
sbSpeed.setOnScroll(e -> {
circle.setTranslateX(+50);
});
circle.centerXProperty().bind(root.widthProperty().divide(2));
circle.centerYProperty().bind(root.heightProperty().divide(2));
circle.radiusProperty().bind(Bindings.min(root.widthProperty().divide(10),
root.heightProperty().divide(10)));
circle2.centerXProperty().bind(root.widthProperty().divide(2));
circle2.centerYProperty().bind(root.heightProperty().divide(2));
circle2.radiusProperty().bind(Bindings.min(root.widthProperty().divide(10),
root.heightProperty().divide(10)));
root.setTop(sbSpeed);
primaryStage.setWidth(bounds.getWidth() * 0.40);
primaryStage.setHeight(bounds.getHeight() * 0.40);
Lighting lighting = new Lighting();
lighting.setLight(light);
lighting.setSurfaceScale(5.0);
circle.setEffect(lighting);
circle2.setEffect(lighting);
timeline = new Timeline();
timeline.setCycleCount(Timeline.INDEFINITE);
timeline.setAutoReverse(true);
timeline.getKeyFrames().addAll
(new KeyFrame(Duration.ZERO, new KeyValue(circle.translateXProperty(),
0)),
new KeyFrame(new Duration(5000), new KeyValue(circle
.translateXProperty(), width - (radius * 2))));
timeline.getKeyFrames().addAll(
new KeyFrame(Duration.ZERO, new KeyValue(
circle2.translateYProperty(), 0)),
new KeyFrame(new Duration(5000), new KeyValue(circle2
.translateYProperty(), height - (radius * 2))));
timeline.play();
root.getChildren().addAll(circle, circle2);
boolean a = true;
root.requestFocus();
root.setOnKeyPressed(e -> {
if (e.getCode().equals(KeyCode.SPACE)) {
if (timeline.statusProperty().getValue().equals(Status.RUNNING)) {
timeline.pause();
} else
timeline.play();
}
});
circle.setOnMousePressed(event -> {
if (circle.getFill().equals(Color.BLACK))
circle.setFill(Color.YELLOW);
else if (circle.getFill().equals(Color.BLUE))
circle.setFill(Color.BROWN);
else if (circle.getFill().equals(Color.YELLOW))
circle.setFill(Color.BROWN);
else if (circle.getFill().equals(Color.BROWN))
circle.setFill(Color.BLACK);
else
circle.setFill(Color.BLUE);
});
circle2.setOnMousePressed(event -> {
if (circle2.getFill().equals(Color.BLACK))
circle2.setFill(Color.YELLOW);
else if (circle2.getFill().equals(Color.BLUE))
circle2.setFill(Color.BROWN);
else if (circle2.getFill().equals(Color.YELLOW))
circle2.setFill(Color.BROWN);
else if (circle2.getFill().equals(Color.BROWN))
circle2.setFill(Color.BLACK);
else
circle2.setFill(Color.BLUE);
// }
});
}
protected void moveCircle(Circle circle) {
if (circle.getCenterX() < circle.getRadius() ||
circle.getCenterX() > circle.getCenterY() - circle.getRadius()) {
circle.translateYProperty();
}
if (circle.getCenterY() < circle.getRadius() ||
circle.getCenterY() > circle.getCenterX() - circle.getRadius()) {
circle.translateXProperty();}
}
public void pause() {
timeline.pause();
}
#Override
public void start(Stage primaryStage) throws Exception {
init(primaryStage);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
just need to start the balls and it will work.
Now this is general pseudo code for something like this, although not specific to your problem this is the general way of making this "bounce" off the edge of the screen.
First off you need 5 pieces of information, the circles velocity (on x and y, as variables or a vector2), the circles radius (or diameter), the circle position (on x and y, as variables or a vector2) and the screens width and height.
Also depending on if the origin point of the circle is the center (you will need radius), bottom left or top left (you will need diameter). In my example we assume the origin is bang on the middle of the circle.
The generally idea goes something like this:
int windowWidth = 800, windowHeight = 600;
Circle c;
// Check if the left or right side of the circle leaves the screen bounds
// if so, reverse the velocity (mirror it)
if(c.x - c.radius < 0 || c.x + c.radius > windowWidth)
c.velocityX = -x.velocityX;
// Check if the top or bottom side of the circle leaves the screen bounds
// if so, reverse the velocity (mirror it)
if(c.y - c.radius < 0 || c.y + c.radius > windowHeight)
c.velocityY = -c.velocityY;
Hope that makes sense, it is a case of checking if the circle is passed the screen boundaries and simply mirroring the velocity on that given axis. So if the ball is moving at a velocity of 5,0 (directly right) and then goes beyond the window width, we want to take the velocity on x and negate it, so now the velocity becomes -5, 0 (directly left).
The one issue with this is the linear look, you can easily add some other variables like acceleration, restitution, friction and drag to give it a more realistic feel.