I am trying to get around 8-12 circles rotate in a circle behind each other.
Here is the code I am trying
import javafx.animation.PathTransition;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.CubicCurveTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.Path;
import javafx.stage.Stage;
import javafx.util.Duration;
public class Cirlces extends Application {
#Override
public void start(final Stage stage) throws Exception {
final Group group = new Group();
final Scene scene = new Scene(group, 500, 500, Color.WHITE);
stage.setScene(scene);
stage.setTitle("Circles");
stage.show();
final Circle circle = new Circle(20, 20, 15);
circle.setFill(Color.DARKRED);
Circle path = new Circle(250,250,200);
path.setFill(Color.WHITE);
group.getChildren().add(path);
group.getChildren().add(circle);
final PathTransition pathTransition = new PathTransition();
pathTransition.setDuration(Duration.seconds(2.0));
pathTransition.setDelay(Duration.ZERO);
pathTransition.setPath(path);
pathTransition.setNode(circle);
pathTransition
.setOrientation(PathTransition.OrientationType.ORTHOGONAL_TO_TANGENT);
pathTransition.setCycleCount(Timeline.INDEFINITE);
//pathTransition.setAutoReverse(true);
pathTransition.play();
}
public static void main(final String[] arguments) {
Application.launch(arguments);
}
}
On compiling and executing this code, I get a circle which goes on a circular path. But after each rotation, there is a slight delay before the next rotation starts, even though I have set the delay to zero.
So using this code, when I create multiple circles on the path (I take a different path for each circle, so it looks like they are on the same path and start each individual from a later time phase using playFrom() to synchronize their position), it gets really messy.
What I want is, multiple circles moving on a circular path uniformly, but that slight delay makes it look very bad.
So my questions are
What can I do to remove that delay? Would constructing the path in a different way help? Is there any way to remove that delay?
The rotation of the circle starts from 3'o clock position on the path, is there any way to change the initial position of the node on the path?
Can I add multiple nodes to the same path? Say 5 circle nodes to the path, with different initial positions.
Call pathTransition.setInterpolator(Interpolator.LINEAR);
You can rotate the path: path.setRotate(-90);
I don't think there's an easy way to do this (with different starting positions), still using the PathTransition. You can achieve a similar effect using a Timeline and binding a rotation for each circle to a property you "animate" with the timeline:
-
import javafx.animation.Animation;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.transform.Rotate;
import javafx.stage.Stage;
import javafx.util.Duration;
public class CircleTransitions extends Application {
#Override
public void start(final Stage stage) throws Exception {
final Group group = new Group();
final Scene scene = new Scene(group, 500, 500, Color.WHITE);
stage.setScene(scene);
stage.setTitle("Circles");
stage.show();
Circle path = new Circle(250,250,200);
path.setFill(Color.WHITE);
DoubleProperty angle = new SimpleDoubleProperty();
Timeline timeline = new Timeline(new KeyFrame(Duration.seconds(2), new KeyValue(angle, 360)));
timeline.setCycleCount(Animation.INDEFINITE);
timeline.play();
group.getChildren().add(path);
for (int i=0; i<5; i++) {
Circle circle = new Circle(250, 450, 15);
circle.setFill(Color.DARKRED);
Rotate rotate = new Rotate(0, 250, 250);
circle.getTransforms().add(rotate);
rotate.angleProperty().bind(angle.add(360.0 * i / 5));
group.getChildren().add(circle);
}
}
public static void main(final String[] arguments) {
Application.launch(arguments);
}
}
You can use multiple path transitions (one per node), with the starting time of each transition offset by the distance along the path that you want a node to start at (using jumpTo).
This is a (simplified) variation on the solution to: "How to write text along a Bezier Curve?". The solution will work for nodes traveling along an arbitrary path, not just nodes traveling in a circle.
import javafx.animation.*;
import javafx.application.Application;
import javafx.scene.*;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.shape.*;
import javafx.stage.Stage;
import javafx.util.Duration;
public class CirclePathTransitions extends Application {
private static final int NUM_NODES = 5;
private static final double W = 200;
private static final double H = 200;
private static final double NODE_SIZE = H / 8.0;
#Override
public void start(final Stage stage) throws Exception {
Pane content = new Pane();
content.setMinSize(Pane.USE_PREF_SIZE, Pane.USE_PREF_SIZE);
content.setPrefSize(W, H);
content.setMaxSize(Pane.USE_PREF_SIZE, Pane.USE_PREF_SIZE);
content.setStyle("-fx-background-color: coral;");
Shape path = new Circle(W/2, H/2, H * 3 / 8.0 - NODE_SIZE);
content.getChildren().add(new Circle(W/2, H/2, H * 3 / 8.0 - NODE_SIZE, Color.PALEGREEN));
for (int i = 0; i < NUM_NODES; i++) {
Node node = new Circle(NODE_SIZE / 2, Color.MIDNIGHTBLUE);
content.getChildren().add(node);
final Transition transition = createPathTransition(path, node);
transition.jumpTo(Duration.seconds(10).multiply(i * 1.0 / NUM_NODES));
transition.play();
}
stage.setScene(new Scene(new StackPane(content), W, H, Color.ALICEBLUE));
stage.show();
}
private PathTransition createPathTransition(Shape shape, Node node) {
final PathTransition transition = new PathTransition(
Duration.seconds(10),
shape,
node
);
transition.setAutoReverse(false);
transition.setCycleCount(PathTransition.INDEFINITE);
transition.setInterpolator(Interpolator.LINEAR);
return transition;
}
public static void main(String[] args) throws Exception {
launch(args);
}
}
Related
I'm trying to force the update of a custom XYChart in a timer method, but the only thing that seems to work is resizing the window.
import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.Shape;
import javafx.scene.transform.Affine;
import javafx.scene.transform.Transform;
import javafx.stage.Stage;
import java.util.*;
import java.util.stream.Collectors;
public class TestCustomLayoutUpdate extends Application {
private LineChart<Number, Number> chart;
private NumberAxis xAxis;
private NumberAxis yAxis;
private ShopItem currentShopItem;
class ShopItem {
private double price;
public ShopItem(double price) {
this.price = price;
}
}
#Override
public void start(Stage primaryStage) {
createChart();
Scene scene = new Scene(chart, 600, 400);
primaryStage.setScene(scene);
primaryStage.setHeight(600);
primaryStage.setWidth(400);
primaryStage.show();
Random rng = new Random();
// Note since this is a regular timer not javafx timer that we should use platform run later.
TimerTask repeatedTask = new TimerTask() {
public void run() {
currentShopItem = new ShopItem(rng.nextDouble() * 100);
Platform.runLater(() -> {
chart.layout();
chart.requestLayout();
xAxis.layout();
});
}
};
Timer timer = new Timer("Timer");
long delay = 1000L;
long period = 1000L;
timer.scheduleAtFixedRate(repeatedTask, delay, period);
}
public void createChart() {
xAxis = new NumberAxis();
yAxis = new NumberAxis();
xAxis.setAutoRanging(false);
xAxis.setUpperBound(100);
chart = new LineChart<Number, Number>(xAxis, yAxis) {
private List<Shape> shapes = new ArrayList<>();
#Override
public void layoutPlotChildren() {
super.layoutPlotChildren();
getPlotChildren().removeAll(shapes);
shapes.clear();
if (currentShopItem != null) {
Rectangle rect = new Rectangle(0, 0, 10, currentShopItem.price);
rect.getTransforms().setAll(chartDisplayTransform(xAxis, yAxis));
rect.setFill(Color.RED);
shapes.add(rect);
getPlotChildren().addAll(shapes);
}
}
};
}
private Transform chartDisplayTransform(NumberAxis xAxis, NumberAxis yAxis) {
return new Affine(xAxis.getScale(), 0, xAxis.getDisplayPosition(0), 0, yAxis.getScale(),
yAxis.getDisplayPosition(0));
}
public static void main(String[] args) {
Application.launch(args);
}
}
JavaFX will automatically layout and redraw any parts of the scene graph if properties of any of the nodes that are part of the graph change. The problem with
the way you have structured the code is that you only change the scene graph (change the dimensions of the rectangle and/or change the plot children of the chart) in the layoutPlotChildren() method, which (I believe) is called as part of the layout process. So when you request a layout, JavaFX checks to see if anything in the scene graph has changed, sees that it hasn't, and so doesn't perform a layout. Thus layoutPlotChildren() isn't called, and so the scene graph isn't changed...
So to fix this, you just need to make sure the existing rectangle is updated, or that the list of plot children change, when the underlying data change. You can accomplish this by using JavaFX properties, and observing them from your chart subclass. (There are other ways too, I suppose, such as defining a method in the chart subclass that updates the rectangle, and invoking it from the animation loop. But observing a JavaFX property is the API-preferred way to do this.)
As an aside, if you want to change anything periodically that updates graphics, the preferred way to do this in JavaFX is with a Timeline, which operates entirely on the JavaFX thread and avoids the need to think about synchronization of variables, etc.
Here's a version of your example with these changes, which works as desired:
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import javafx.animation.Animation;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.Scene;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.Shape;
import javafx.scene.transform.Affine;
import javafx.scene.transform.Transform;
import javafx.stage.Stage;
import javafx.util.Duration;
public class TestCustomLayoutUpdate extends Application {
private LineChart<Number, Number> chart;
private NumberAxis xAxis;
private NumberAxis yAxis;
private ObjectProperty<ShopItem> currentShopItem;
class ShopItem {
private double price;
public ShopItem(double price) {
this.price = price;
}
}
#Override
public void start(Stage primaryStage) {
currentShopItem = new SimpleObjectProperty<>();
createChart();
Scene scene = new Scene(chart, 600, 400);
primaryStage.setScene(scene);
primaryStage.setHeight(600);
primaryStage.setWidth(400);
primaryStage.show();
Random rng = new Random();
Timeline timeline = new Timeline(new KeyFrame(Duration.seconds(1),
evt -> currentShopItem.set(new ShopItem(rng.nextDouble() * 100))
));
timeline.setCycleCount(Animation.INDEFINITE);
timeline.play();
}
public void createChart() {
xAxis = new NumberAxis();
yAxis = new NumberAxis();
xAxis.setAutoRanging(false);
xAxis.setUpperBound(100);
chart = new LineChart<Number, Number>(xAxis, yAxis) {
private List<Shape> shapes = new ArrayList<>();
private Rectangle rect ;
// anonymous class constructor:
{
rect = new Rectangle(0,0, Color.RED);
currentShopItem.addListener((obs, oldItem, newItem) -> {
if (newItem == null) {
rect.setWidth(0);
rect.setHeight(0);
} else {
rect.setWidth(10);
rect.setHeight(newItem.price);
}
});
}
#Override
public void layoutPlotChildren() {
super.layoutPlotChildren();
getPlotChildren().removeAll(shapes);
shapes.clear();
if (currentShopItem != null) {
rect.getTransforms().setAll(chartDisplayTransform(xAxis, yAxis));
shapes.add(rect);
getPlotChildren().addAll(shapes);
}
}
};
}
private Transform chartDisplayTransform(NumberAxis xAxis, NumberAxis yAxis) {
return new Affine(xAxis.getScale(), 0, xAxis.getDisplayPosition(0), 0, yAxis.getScale(),
yAxis.getDisplayPosition(0));
}
public static void main(String[] args) {
Application.launch(args);
}
}
The root is Group and 3 children has been added to the group: a square from Rectangle class, a triangle from Polygon class and a circle from Circle class. These 3 objects are all built in different colors.
Task: when the mouse is on a specific object, change the color of that object. The color will change back if the mouse is out of that object.
Here's what I did:
import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.input.MouseEvent;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Polygon;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
public class ColouredShapes extends Application{
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) {
Group group = new Group();
// square
Rectangle square = new Rectangle(40,40);
square.setFill(Color.BLUE);
// triangle
Polygon triangle = new Polygon();
triangle.setLayoutX(80);
triangle.getPoints().addAll(
40.0,0.0,
80.0,40.0,
0.0,40.0
);
triangle.setFill(Color.RED);
//circle
Circle circle = new Circle(20);
circle.setLayoutX(240);
circle.setCenterY(20);
// ************** where everything happens *****************
group.onMouseMovedProperty().set(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
System.out.printf("coordinate X: %.2f, coordinate Y: %.2f\n",event.getX(),event.getY());
System.out.println(event.getSource());
if (event.getSource() instanceof Rectangle) {
square.setFill(Color.MAGENTA);
} else square.setFill(Color.BLUE);
}
});
group.getChildren().add(circle);
group.getChildren().add(triangle);
group.getChildren().add(square);
Scene scene = new Scene(group,700,500);
primaryStage.setScene(scene);
primaryStage.show();
} }
The problem is event.getSource() returns a result of Group#52d9d54c[styleClass=root] rather than the class of its children. As a result, the child cannot be identified, color won't change when mouse moved in to that child.
event.getSource() returns the node which triggered the event, which in this case is the Group, since that's the node on which you registered the handler.
Use onMouseEntered and onMouseExited handlers on each of the individual nodes instead:
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Polygon;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.Shape;
import javafx.stage.Stage;
public class ColoredShapes extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) {
Group group = new Group();
// square
Rectangle square = new Rectangle(40, 40);
square.setFill(Color.BLUE);
// triangle
Polygon triangle = new Polygon();
triangle.setLayoutX(80);
triangle.getPoints().addAll(40.0, 0.0, 80.0, 40.0, 0.0, 40.0);
triangle.setFill(Color.RED);
// circle
Circle circle = new Circle(20);
circle.setLayoutX(240);
circle.setCenterY(20);
registerHandler(square, Color.BLUE, Color.MAGENTA);
registerHandler(triangle, Color.RED, Color.MAGENTA);
registerHandler(circle, Color.BLACK, Color.MAGENTA);
group.getChildren().add(circle);
group.getChildren().add(triangle);
group.getChildren().add(square);
Scene scene = new Scene(group, 700, 500);
primaryStage.setScene(scene);
primaryStage.show();
}
private void registerHandler(Shape s, Color defaultColor, Color hoverColor) {
s.setOnMouseEntered( e -> s.setFill(hoverColor));
s.setOnMouseExited(e -> s.setFill(defaultColor));
}
}
You can also do this without using event handlers at all. Either use bindings:
square.fillProperty().bind(Bindings
.when(square.hoverProperty())
.then(Color.MAGENTA)
.otherwise(Color.BLUE));
or using CSS:
square.getStyleClass().add("square");
and then in an external CSS file:
.square {
-fx-fill: blue ;
}
.square:hover {
-fx-fill: magenta ;
}
I want a cursor that displays the coordinates of the place the mouse is pointing to, besides the pointer itself (it should look like a crosshair with little numbers is bottom right corner, that change when the mouse is moving). How to achive such effect efficiently? I tried to use the tooltip mechanism, but then the text lags behind the cursor a lot...
You could attach a label, and change the text inside the mouse moved event everytime that the mouse coordinates changes.
You can create a Text object with the current coordinates, snapshot it to an Image, and then create a WritableImage from that and any other cursor decoration you need. Then wrap the WritableImage in an ImageCursor:
import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.scene.Cursor;
import javafx.scene.ImageCursor;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.image.WritableImage;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.text.Text;
import javafx.stage.Stage;
public class MouseCoordinateCursor extends Application {
#Override
public void start(Stage primaryStage) {
Pane root = new Pane();
root.setOnMouseExited(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
root.setCursor(Cursor.DEFAULT);
}
});
root.setOnMouseMoved(new EventHandler<MouseEvent>() {
final int padding = 9 ;
final int offset = 6 ;
#Override
public void handle(MouseEvent e) {
Text text = new Text(String.format("[%.1f,%.1f]", e.getX(), e.getY()));
Image textImage = text.snapshot(null, null);
int width = (int)textImage.getWidth();
int height = (int)textImage.getHeight();
WritableImage cursorImage = new WritableImage(width + offset, height + offset);
cursorImage.getPixelWriter().setPixels(offset, offset, width, height, textImage.getPixelReader(), 0, 0);
for (int i = 0; i < padding; i++) {
cursorImage.getPixelWriter().setColor(i, padding/2, Color.BLACK);
cursorImage.getPixelWriter().setColor(padding/2, i, Color.BLACK);
}
root.setCursor(new ImageCursor(cursorImage));
}
});
primaryStage.setScene(new Scene(root, 600, 600));
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
I am trying to create balls whenever create button is clicked. I am able to create a single ball but for some reason not able to create multiple balls when the 'Create' button is clicked repeatedly.
Any help is appreciated.
package week3;
import javafx.application.Application;
import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.control.Slider;
import javafx.scene.control.Button;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.TilePane;
import javafx.geometry.Orientation;
import javafx.geometry.Insets;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.beans.property.DoubleProperty;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.util.Duration;
public class BounceBallControl1 extends Application {
public final double radius = 10;
private double x = radius, y = radius;
private double dx = 1, dy = 1;
private Circle circle = new Circle(x, y, radius);
private Timeline animation;
#SuppressWarnings("restriction")
#Override // Override the start method in the Application class
public void start(Stage primaryStage) {
//BallPane ballPane = new BallPane();
Button btnCreate = new Button("Create");
Button btnDelete = new Button("Delete");
btnCreate.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);
btnDelete.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);
TilePane tileButtons = new TilePane(Orientation.HORIZONTAL);
tileButtons.setPadding(new Insets(5, 5, 5, 70));
tileButtons.setHgap(20.0);
tileButtons.getChildren().addAll(btnCreate, btnDelete);
Slider slSpeed = new Slider();
slSpeed.setMax(20);
//rateProperty().bind(slSpeed.valueProperty());
BorderPane pane = new BorderPane();
//pane.setCenter(ballPane);
pane.setTop(slSpeed);
pane.setBottom(tileButtons);
btnCreate.setOnAction(new EventHandler<ActionEvent>() {
public void handle(ActionEvent event) {
for(int i=0;i<=100;i++)
circle.setFill(Color.TURQUOISE); // Set ball color
pane.getChildren().add(circle); // Place a ball into this pane
// Create an animation for moving the ball
animation = new Timeline(
new KeyFrame(Duration.millis(50), (e -> {
// Check boundaries
if (x < radius || x > pane.getWidth() - radius) {
dx *= -1; // Change ball move direction
}
if (y < radius || y > pane.getHeight() - radius) {
dy *= -1; // Change ball move direction
}
// Adjust ball position
x += dx;
y += dy;
circle.setCenterX(x);
circle.setCenterY(y);
})));
animation.setCycleCount(Timeline.INDEFINITE);
animation.play(); // Start animation
}
});
// Create a scene and place it in the stage
Scene scene = new Scene(pane, 250, 250);
primaryStage.setTitle("BounceBallSlider"); // Set the stage title
primaryStage.setScene(scene); // Place the scene in the stage
primaryStage.show(); // Display the stage
}
/*public DoubleProperty rateProperty() {
return animation.rateProperty();
}*/
}
You use the same instance of circle each time you click add button. So every time the same circle is added to the scene. Try by creating new circle each time, it will be ok.
I'm trying to create a JavaFx gui that displays 2 maps side by side. One map is of the city while the other is the same city's transportation system. The goal of this application is to do the following:
When the user clicks on a certain location in the the first map (Normal City Map) the second map (Trans Map) should be highlighted in the same exact location or corresponding location.
i have already created the interface where it displays both the same maps and was able to make the mouseEvent in order to extract the coordinates.
The question is, How can i make the 2nd map highlight the same location when clicked?
ِAlso, is it possible to add a Zoom Feature on the interface?
Here is the Code:
package hcimetromap;
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ListView;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ScrollEvent;
/**
*
* #author tharwat
*/
public class HciMetroMap extends Application {
final int xDiff = 675;
double x = 0;
double y = 0;
#Override public void start(Stage stage) throws Exception{
stage.setTitle("Baseline");
StackPane layout = new StackPane();
layout.getStylesheets().add("mainClass.css");
layout.getStyleClass().add("main-class");
Scene scene = new Scene(layout, 1200, 1200);
scene.getStylesheets().add(
getClass().getResource("mainClass.css").toExternalForm()
);
stage.setScene(scene);
stage.setResizable(false);
stage.centerOnScreen();
scene.setOnMouseClicked(new EventHandler<MouseEvent>() {
public void handle(MouseEvent me) {
//log mouse move to console, method listed below
x = me.getX();
y = me.getY();
System.out.println("Mouse moved, x: " +x +", y: " + y);
}
});
stage.show();
}
public static void main(String[] args) { launch(args); }
}
Associated Css:
.main-class {
-fx-background-image: url('TransMap.jpg'), url('map1.jpg');
-fx-background-repeat: stretch;
-fx-background-size: 50% 100%, 50% 100%;
-fx-background-position: right , left ;
-fx-effect: dropshadow(three-pass-box, black, 30, 0.5, 0, 0);
-fx-background-insets: 30;
}