Updating an animation in javafx: Path Transition over an adjustable curve - java

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...

Related

Getting x y coordinates of path on screen in JavaFX

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();
}
}

How to implement bindings of multiple properties to all listen to each other?

I have two Circle nodes that are dragable, and I want to connect them with a Line that connects the nodes (not from their center, but from their perimeter). But as one Circle changes location as it is getting dragged, the Line's startX and startY values change so the Line becomes the shortest line between the two Circles, collinear with the line connecting their radii.
My problem is that it seems overly verbose to have the Line's startX, startY, endX, and endY each individually listen to or bind to both Circles' centerXProperty and centerYProperty (or rather, bind to a calculation with those properties' values), as that would result in 16 total bindings/listeners.
I was wondering if there exists an easier or more convenient way to accomplish this. I was considering creating a SimpleDoubleProperty that would be the slope of the Line object (y2 - y1)/(x2 - x1), binding to the two centerXPropertys and centerYPropertys, and have startX, startY, endX, and endY each listen to that property, but I'm also not sure how to have a single property bind to the resultant calculation of those four properties.
Here is how I'm currently constructing my Line. I was experimenting for the startXProperty and startYProperty binding and realized it properly updated the Line but ONLY when the source Circle source was moved, which prompted me to ask this question. The endXProperty and endYProperty still both have the Line anchored at the target Circle's center. I can provide my entire code if needed although I think this should suffice for what I'm trying to accomplish.
public GraphEdge(GraphNode source, GraphNode target) {
this.source = source;
this.target = target;
this.setFill(Color.BLACK);
this.startXProperty().bind(Bindings.createDoubleBinding(() -> {
slope = (target.getCenterY() - source.getCenterY())/(target.getCenterX() - source.getCenterX());
return source.getCenterX() + Math.cos(Math.atan(slope)) * source.getRadius();
}, source.boundsInParentProperty()));
this.startYProperty().bind(Bindings.createDoubleBinding(() -> {
slope = (target.getCenterY() - source.getCenterY())/(target.getCenterX() - source.getCenterX());
return source.getCenterY() + Math.sin(Math.atan(slope)) * source.getRadius();
}, source.boundsInParentProperty()));
this.endXProperty().bind(Bindings.createDoubleBinding(() -> {
Bounds b = target.getBoundsInParent();
return b.getMinX() + b.getWidth() / 2;
}, target.boundsInParentProperty()));
this.endYProperty().bind(Bindings.createDoubleBinding(() -> {
Bounds b = target.getBoundsInParent();
return b.getMinY() + b.getHeight() / 2;
}, target.boundsInParentProperty()));
}
createDoubleBinding accepts a list of dependencies. You should list all of the properties each line property depends on.
this.startXProperty().bind(Bindings.createDoubleBinding(
() -> {
double slope = (target.getCenterY() - source.getCenterY())/(target.getCenterX() - source.getCenterX());
return source.getCenterX() + Math.cos(Math.atan(slope)) * source.getRadius();
},
source.centerXProperty(),
source.centerYProperty(),
target.centerXProperty(),
target.centerYProperty(),
source.radiusProperty(),
));
Repeat for the three other line properties.
I agree with #John Kugelman answer.
I gave a quick try and looks like your actual formula (using sin/tan) is not working for me as expected.
So I gave a rethink about the logic and came up with the below solution. This solution is based on the concept of "Given line AB, find a point C on line which is at a distance d". Here A and B are the centers of the circles.
So the idea is :
We build a DoubleBinding to get the length(l) of the line between centers of circles.
Then we calculate the points which are at a distance of radius 'r' and 'l-r'. The points sits on the edges of the two circles.
Lastly, we build a line by binding with the new points.
Please find the below code:
class GraphEdge extends Line {
public GraphEdge(GraphNode source, GraphNode target) {
DoubleBinding lineLength = Bindings.createDoubleBinding(() -> {
double xDiffSqu = (target.getCenterX() - source.getCenterX()) * (target.getCenterX() - source.getCenterX());
double yDiffSqu = (target.getCenterY() - source.getCenterY()) * (target.getCenterY() - source.getCenterY());
return Math.sqrt(xDiffSqu + yDiffSqu);
}, source.centerXProperty(), source.centerYProperty(), target.centerXProperty(), target.centerYProperty());
DoubleBinding sTx = pointBinding(source, target, lineLength, false, Circle::getCenterX);
DoubleBinding sTy = pointBinding(source, target, lineLength, false, Circle::getCenterY);
DoubleBinding eTx = pointBinding(source, target, lineLength, true, Circle::getCenterX);
DoubleBinding eTy = pointBinding(source, target, lineLength, true, Circle::getCenterY);
setStroke(Color.BLUE);
setStrokeWidth(2);
startXProperty().bind(sTx);
startYProperty().bind(sTy);
endXProperty().bind(eTx);
endYProperty().bind(eTy);
}
private DoubleBinding pointBinding(Circle startDot, Circle endDot, DoubleBinding lineLength, boolean isFarEnd, Function<Circle, Double> refPoint) {
return Bindings.createDoubleBinding(() -> {
double dt = isFarEnd ? lineLength.get() - endDot.getRadius() : startDot.getRadius();
double t = dt / lineLength.get();
double startPoint = refPoint.apply(startDot);
double endPoint = refPoint.apply(endDot);
double dy = ((1 - t) * startPoint) + (t * endPoint);
return dy;
}, lineLength);
}
}
Below is the complete working demo:
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.DoubleBinding;
import javafx.event.EventHandler;
import javafx.geometry.Bounds;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Line;
import javafx.stage.Stage;
import java.util.function.Function;
public class DoubleBindingsDemo extends Application {
#Override
public void start(Stage stage) throws Exception {
StackPane root = new StackPane();
root.setPadding(new Insets(20));
Pane pane = new Pane();
pane.setStyle("-fx-border-width:1px;-fx-border-color:black;");
root.getChildren().add(pane);
Scene sc = new Scene(root, 600, 600);
stage.setScene(sc);
stage.show();
GraphNode greenNode = new GraphNode("green");
GraphNode redNode = new GraphNode("red");
GraphEdge edge = new GraphEdge(greenNode, redNode);
pane.getChildren().addAll(greenNode, redNode, edge);
}
class GraphNode extends Circle {
double sceneX, sceneY, centerX, centerY;
public GraphNode(String color) {
double radius = 30;
setRadius(radius);
setStyle("-fx-fill:" + color + ";-fx-stroke-width:2px;-fx-stroke:black;-fx-opacity:.5");
setCenterX(radius);
setCenterY(radius);
setOnMousePressed(e -> {
sceneX = e.getSceneX();
sceneY = e.getSceneY();
centerX = getCenterX();
centerY = getCenterY();
});
EventHandler<MouseEvent> dotOnMouseDraggedEventHandler = e -> {
// Offset of drag
double offsetX = e.getSceneX() - sceneX;
double offsetY = e.getSceneY() - sceneY;
// Taking parent bounds
Bounds parentBounds = getParent().getLayoutBounds();
double dotRadius = getRadius();
double maxCx = parentBounds.getWidth() - dotRadius;
double maxCy = parentBounds.getHeight() - dotRadius;
double cxOffset = centerX + offsetX;
double cyOffset = centerY + offsetY;
if (cxOffset < dotRadius) {
setCenterX(dotRadius);
} else if (cxOffset < maxCx) {
setCenterX(cxOffset);
} else {
setCenterX(maxCx);
}
if (cyOffset < dotRadius) {
setCenterY(dotRadius);
} else if (cyOffset < maxCy) {
setCenterY(cyOffset);
} else {
setCenterY(maxCy);
}
};
setOnMouseDragged(dotOnMouseDraggedEventHandler);
}
}
class GraphEdge extends Line {
public GraphEdge(GraphNode source, GraphNode target) {
DoubleBinding lineLength = Bindings.createDoubleBinding(() -> {
double xDiffSqu = (target.getCenterX() - source.getCenterX()) * (target.getCenterX() - source.getCenterX());
double yDiffSqu = (target.getCenterY() - source.getCenterY()) * (target.getCenterY() - source.getCenterY());
return Math.sqrt(xDiffSqu + yDiffSqu);
}, source.centerXProperty(), source.centerYProperty(), target.centerXProperty(), target.centerYProperty());
DoubleBinding sTx = pointBinding(source, target, lineLength, false, Circle::getCenterX);
DoubleBinding sTy = pointBinding(source, target, lineLength, false, Circle::getCenterY);
DoubleBinding eTx = pointBinding(source, target, lineLength, true, Circle::getCenterX);
DoubleBinding eTy = pointBinding(source, target, lineLength, true, Circle::getCenterY);
setStroke(Color.BLUE);
setStrokeWidth(2);
startXProperty().bind(sTx);
startYProperty().bind(sTy);
endXProperty().bind(eTx);
endYProperty().bind(eTy);
}
private DoubleBinding pointBinding(Circle startDot, Circle endDot, DoubleBinding lineLength, boolean isFarEnd, Function<Circle, Double> refPoint) {
return Bindings.createDoubleBinding(() -> {
double dt = isFarEnd ? lineLength.get() - endDot.getRadius() : startDot.getRadius();
double t = dt / lineLength.get();
double startPoint = refPoint.apply(startDot);
double endPoint = refPoint.apply(endDot);
double dy = ((1 - t) * startPoint) + (t * endPoint);
return dy;
}, lineLength);
}
}
}

3D Scatter Chart JavaFX: How to show legend and measures near axis

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.)

bullet trajectory

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.

How to keep moving circles inside the stage?

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.

Categories

Resources