JavaFX material's bump and spec maps - javafx

When JavaFX8 code loads the color, bump and spec maps, the color and spec work as expected, but bump map is causing strange effects. All three are Mercator maps of Earth. Generally, there is no 3d effect added by the bump map. Bump map only causes Himalaya and Andes appear on the lit side of the globe as black areas with shiny border and on the shaded side as they appear on the color map. What am I doing wrong?
Image diffMap = null;
Image bumpMap = null;
Image specMap = null;
diffMap = new Image(MoleculeSampleApp.class.getResource("Color Map1.jpg").toExternalForm());
bumpMap = new Image(MoleculeSampleApp.class.getResource("Bump1.jpg").toExternalForm());
specMap = new Image(MoleculeSampleApp.class.getResource("Spec Mask1.png").toExternalForm());
final PhongMaterial earthMaterial = new PhongMaterial(Color.WHITE, diffMap, specMap, bumpMap, null);
earthMaterial.setDiffuseColor(Color.WHITE);
earthMaterial.setSpecularColor(Color.WHITE);
Being new to 3d my first thought is that there should be some kind of scaling pixel color values of the bump map into elevation, which I am missing.

Bump map for JavaFX is a normal map, not a height map, for more info see: normal map and height map info.
Here is a sample you can try.
The images for the maps are pretty large, so it might take a little while to download them before your scene shows.
Source I used for images was => Bored? Then Create a Planet (now a dead link).
import javafx.animation.*;
import javafx.application.Application;
import javafx.scene.*;
import javafx.scene.image.Image;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.*;
import javafx.scene.shape.Sphere;
import javafx.scene.transform.Rotate;
import javafx.stage.Stage;
import javafx.util.Duration;
public class EarthViewer extends Application {
private static final double EARTH_RADIUS = 400;
private static final double VIEWPORT_SIZE = 800;
private static final double ROTATE_SECS = 30;
private static final double MAP_WIDTH = 4096;
private static final double MAP_HEIGHT = 2048;
private static final String DIFFUSE_MAP =
"https://imgur.com/vrNnXIs.jpeg";
private static final String NORMAL_MAP =
"https://imgur.com/5T2oAuk.jpeg";
private static final String SPECULAR_MAP =
"https://imgur.com/GV11WNV.jpeg";
private Group buildScene() {
Sphere earth = new Sphere(EARTH_RADIUS);
earth.setTranslateX(VIEWPORT_SIZE / 2d);
earth.setTranslateY(VIEWPORT_SIZE / 2d);
PhongMaterial earthMaterial = new PhongMaterial();
earthMaterial.setDiffuseMap(
new Image(
DIFFUSE_MAP,
MAP_WIDTH,
MAP_HEIGHT,
true,
true
)
);
earthMaterial.setBumpMap(
new Image(
NORMAL_MAP,
MAP_WIDTH,
MAP_HEIGHT,
true,
true
)
);
earthMaterial.setSpecularMap(
new Image(
SPECULAR_MAP,
MAP_WIDTH,
MAP_HEIGHT,
true,
true
)
);
earth.setMaterial(
earthMaterial
);
return new Group(earth);
}
#Override
public void start(Stage stage) {
Group group = buildScene();
Scene scene = new Scene(
new StackPane(group),
VIEWPORT_SIZE, VIEWPORT_SIZE,
true,
SceneAntialiasing.BALANCED
);
scene.setFill(Color.rgb(10, 10, 40));
scene.setCamera(new PerspectiveCamera());
stage.setScene(scene);
stage.show();
stage.setFullScreen(true);
rotateAroundYAxis(group).play();
}
private RotateTransition rotateAroundYAxis(Node node) {
RotateTransition rotate = new RotateTransition(
Duration.seconds(ROTATE_SECS),
node
);
rotate.setAxis(Rotate.Y_AXIS);
rotate.setFromAngle(360);
rotate.setToAngle(0);
rotate.setInterpolator(Interpolator.LINEAR);
rotate.setCycleCount(RotateTransition.INDEFINITE);
return rotate;
}
public static void main(String[] args) {
launch(args);
}
}
Normal? Why????
The JavaDoc states for the PhongMaterial bumpMapProperty states:
The bump map of this PhongMaterial, which is a normal map stored as a RGB Image.
A normal map is used rather than a height map because:
[normal maps] are much more accurate, as rather than only simulating the pixel
being away from the face along a line, they can simulate that pixel
being moved at any direction, in an arbitrary way.
A brief description of both normal mapping and height mapping is provided in the wikipedia bump mapping article.
Sample Images
Update, July 2021
Unfortunately the image source from "Bored? Then Create a Planet" is no longer available, so I updated the answer to link to different images (hopefully those will remain online). Because it is linked to different images, the resultant rendering of earth looks a bit different than the example image above, though it is similar. The code to render is basically no different, though the images changed.
Diffuse map
Normal map
Specular map

Related

How do I set fxyz3d shape materials so they work?

How do I specify a material for an fxyz3d shape? When I add to a JavaFX Group of 3D objects the fxyz3d node
Cone cone = new Cone(coneFacets, coneRadius, coneHeight);
cone.setMaterial(Materials.redMaterial());
it turns every shape in that group solid black, not just the cone, regardless of what any of the specified materials are. If I comment out the above two lines and the one that adds the cone to the group, all the displays of the other shapes return to their specified appearances.
I am using javafx-sdk-17.0.1, fxyz3d-0.5.4.jar, JavaSE-16, Windows 10. Is Javadoc available for fxyz3d anywhere? Or is it necessary to download source and build it locally?
The redMaterial is defined as
final PhongMaterial material = new PhongMaterial();
material.setDiffuseColor(Color.INDIANRED);
material.setSpecularColor(Color.RED);
The following code will reproduce this. As is, both cone and cylinder display black. Comment out the four lines that create and add the Cone, and the cylinder will display red as specified by the material. (Don't otherwise use this as a starting-point example, as there are also issues with automatic scaling as the user adjusts the stage window size yet to be addressed.)
package org.javafxtests;
import org.fxyz3d.shapes.Cone;
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.PerspectiveCamera;
import javafx.scene.Scene;
import javafx.scene.SubScene;
import javafx.scene.control.Label;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.paint.PhongMaterial;
import javafx.scene.shape.Cylinder;
import javafx.stage.Stage;
public class JxyzConeMaterials extends Application {
// https://www.tutorialspoint.com/javafx/index.htm
// https://www.javatpoint.com/javafx-tutorial
// https://openjfx.io/javadoc/11/
/**
* The application initialization method.
*/
#Override
public void init() throws Exception {
super.init();
}
/**
* Main entry point for all JavaFX applications. The start method is called
* after the init method has returned and the JavaFX framework and hosting
* system are ready to start the application.
*/
#Override
public void start(Stage primaryStage) throws Exception {
primaryStage.setTitle("Test JXzy Cone materials");
double sceneWidth = 750.0d;
double sceneHeight = 500.0d;
// The scene structure is constructed from the inside-out (bottom-up).
// A tool bar goes along the top
final FlowPane toolbar = new FlowPane();
toolbar.setPrefWidth(Double.MAX_VALUE);
toolbar.getChildren().addAll(new Label("Files"));
// A TreeView goes down the left side
final TreeView<String> treeView = new TreeView<String>();
treeView.setPrefHeight(Double.MAX_VALUE);
TreeItem<String> treeRoot = new TreeItem<String>("<empty>");
treeView.setRoot(treeRoot);
// A SubScene for viewing 3D objects goes to the right of the TreeView
final SubScene canvasScene = new SubScene(new AnchorPane(), 0, 0);
final AnchorPane canvasRootPane = (AnchorPane) canvasScene.getRoot();
canvasRootPane.setPrefWidth(Double.MAX_VALUE);
canvasRootPane.setPrefHeight(Double.MAX_VALUE);
canvasScene.setWidth(0.75 * sceneWidth); // No setPref methods
canvasScene.setHeight(sceneHeight);
// Create a controllable camera for the 3D SubScene
final PerspectiveCamera canvasCamera = new PerspectiveCamera(true);
final Group cameraTruck = new Group();
final Group cameraGimbal = new Group();
canvasCamera.setFarClip(6000);
canvasCamera.setNearClip(0.01);
cameraGimbal.getChildren().add(canvasCamera);
cameraTruck.getChildren().add(cameraGimbal);
cameraTruck.setTranslateZ(-500.0d);
canvasScene.setCamera(canvasCamera);
canvasRootPane.getChildren().add(cameraTruck);
// Create an HBox at the bottom of the scene,
// TreeView on the left and 3D canvas on the right.
HBox treeAnd3dViews = new HBox(treeView, canvasScene);
treeAnd3dViews.setFillHeight(true);
HBox.setHgrow(canvasScene, Priority.ALWAYS);
treeAnd3dViews.setMaxHeight(Double.MAX_VALUE);
treeAnd3dViews.setMaxWidth(Double.MAX_VALUE);
// Create a VBox to stack the tool bar over the above.
VBox toolbarOverViews = new VBox(toolbar, treeAnd3dViews);
toolbarOverViews.setMaxWidth(Double.MAX_VALUE);
toolbarOverViews.setMaxHeight(Double.MAX_VALUE);
VBox.setVgrow(treeAnd3dViews, Priority.ALWAYS);
AnchorPane.setTopAnchor(toolbarOverViews, 0.0);
AnchorPane.setBottomAnchor(toolbarOverViews, 0.0);
AnchorPane.setLeftAnchor(toolbarOverViews, 0.0);
AnchorPane.setRightAnchor(toolbarOverViews, 0.0);
final Scene scene = new Scene(new AnchorPane(), sceneWidth, sceneHeight);
final AnchorPane sceneRootPane = (AnchorPane) scene.getRoot();
sceneRootPane.getChildren().add(toolbarOverViews);
// Draw an arrow consisting of a cylinder with a cone on top.
double lineRadius = 1.0d;
double lineLength = 25.0d;
int coneFacets = 6;
double coneRadius = 3.0d;
double coneHeight = 6.0d;
final PhongMaterial material = new PhongMaterial();
material.setDiffuseColor(Color.INDIANRED);
material.setSpecularColor(Color.RED);
Cylinder cylinder = new Cylinder(lineRadius, lineLength);
cylinder.setMaterial(material);
Cone cone = new Cone(coneFacets, coneRadius, coneHeight);
cone.setMaterial(material);
// The cone points in the negative Y direction
cone.setTranslateY(-(lineLength / 2.0d) - coneHeight );
canvasRootPane.getChildren().add(cylinder);
canvasRootPane.getChildren().add(cone);
// Show
primaryStage.setScene(scene);
primaryStage.show();
}
#Override
public void stop() throws Exception {
super.stop();
}
/**
* Main method to launch the application with parameters if needed.
* This may or may not be called, depending on how this application
* is launched.
*
* #param args specifies arguments to {#linkplain Application#launch)}.
*/
public static void main(String[] args) {
launch(args);
}
}

java.lang.IllegalArgumentException: Problem and Hitbox

I have two problems one this that, if i want to show score with the circle object:
layoutV.getChildren().addAll(virus, score);
I get the following error:
Exception in thread "JavaFX Application Thread" java.lang.IllegalArgumentException: Children: duplicate children added: parent = Pane#6661fc86[styleClass=root].
As far as I understand it is because the Task wants to show multiple scores. So should I use another scene or layout to show score?
My other problem is the hitbox of the object, right know everytime i click the score goes up. I looked up the mouse event getTarget but it does not seem like I can make it so that my object is the only target to use the mouse event on.
public class Main extends Application {
private Stage window;
private Pane layoutV;
private Scene scene;
private Circle virus;
private int score;
private Label scores;
#Override
public void start(Stage primaryStage) {
window = primaryStage;
window.setTitle("Enemy TEST");
this.score = 0;
scores = new Label("Score "+ score);
layoutV = new Pane();
scene = new Scene(layoutV, 600, 600);
window.setScene(scene);
window.show();
Thread th = new Thread(task);
th.setDaemon(true);
th.start();
}
Task task = new Task<Void>() {
#Override
protected Void call() throws Exception {
while (true) {
Platform.runLater(new Runnable() {
#Override
public void run() {
drawCircles();
}
});
Thread.sleep(1000);
}
}
};
public void drawCircles() {
double x = (double)(Math.random() * ((550 - 50) + 1)) + 50;
double y = (double)(Math.random() * ((550 - 50) + 1)) + 50;
double r = (double)(Math.random() * ((30 - 10) + 1)) + 10;
virus = new Circle(x, y, r, Color.VIOLET);
layoutV.setOnMouseClicked(e -> {
if (e.getButton() == MouseButton.PRIMARY) {
layoutV.getChildren().remove(e.getTarget());
this.score++;
System.out.println("score: "+ this.score);
}
});
layoutV.getChildren().addAll(virus);
scene.setRoot(layoutV);
window.setScene(scene);
}
public static void main(String[] args) {
launch(args);
}
}
You have lots of issues, not just the ones from your question:
Although it will work as you coded
it, I don't advise spawning a thread to draw your circles, instead
see:
JavaFX periodic background task
You don't need to set the root in the scene and the scene in the
window every time you draw a new circle.
Nor do you need to set the
mouse handler on the layout every time you draw a circle.
Rather than setting a mouse handler on the layout, you are better off setting a mouse handler on the circles themselves (which you can do before you add them to the scene).
score is an int, not a node you can only add nodes to the scene
graph.
See the documentation for the scene package:
A node may occur at most once anywhere in the scene graph. Specifically, a node must appear no more than once in the children list of a Parent or as the clip of a Node. See the Node class for more details on these restrictions.
How you are adding the node more than once is not clear to me, because you are probably doing it in code different than the Main class you provided.
To add a circle with a score on top, use a StackPane with the score in a label, but make the label mouse transparent, so that it does not register any clicks:
Label scoreLabel = new Label(score + "");
scoreLabel.setMouseTransparent(true);
StackPane balloon = new StackPane(circle, scoreLabel);
layoutV.getChildren.add(balloon);
Add the click handler on the balloon.
And additional issues I don't detail here but are solved in the demo code provided.
To fix all your errors, I would write some code like below. Perhaps you can review it and compare it with your code to help understand one way to create this game.
The example code might not be exactly the functionality you are looking for (that is not really its purpose), but it should be enough to keep you on the right track for implementing your application.
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.text.*;
import javafx.stage.Stage;
import javafx.util.Duration;
import java.util.concurrent.ThreadLocalRandom;
public class Inoculation extends Application {
public static final int W = 600;
public static final int H = 600;
private final IntegerProperty score = new SimpleIntegerProperty(0);
private final Pane playingField = new Pane();
#Override
public void start(Stage stage) {
StackPane overlay = createOverlay();
Pane layout = new StackPane(playingField, overlay);
stage.setScene(new Scene(layout, W, H));
stage.show();
Infection infection = new Infection(playingField, score);
infection.begin();
}
private StackPane createOverlay() {
Label totalScoreLabel = new Label();
totalScoreLabel.textProperty().bind(
Bindings.concat(
"Score ", score.asString()
)
);
StackPane overlay = new StackPane(totalScoreLabel);
StackPane.setAlignment(totalScoreLabel, Pos.TOP_LEFT);
overlay.setMouseTransparent(true);
return overlay;
}
public static void main(String[] args) {
launch(args);
}
}
class Infection {
private static final Duration SPAWN_PERIOD = Duration.seconds(1);
private static final int NUM_SPAWNS = 10;
private final Timeline virusGenerator;
public Infection(Pane playingField, IntegerProperty score) {
virusGenerator = new Timeline(
new KeyFrame(
SPAWN_PERIOD,
event -> spawnVirus(
playingField,
score
)
)
);
virusGenerator.setCycleCount(NUM_SPAWNS);
}
public void begin() {
virusGenerator.play();
}
private void spawnVirus(Pane playingField, IntegerProperty score) {
Virus virus = new Virus();
virus.setOnMouseClicked(
event -> {
score.set(score.get() + virus.getVirusScore());
playingField.getChildren().remove(virus);
}
);
playingField.getChildren().add(virus);
}
}
class Virus extends StackPane {
private static final int MAX_SCORE = 3;
private static final int RADIUS_INCREMENT = 10;
private final int virusScore = nextRandInt(MAX_SCORE) + 1;
public Virus() {
double r = (MAX_SCORE + 1 - virusScore) * RADIUS_INCREMENT;
Circle circle = new Circle(
r,
Color.VIOLET
);
Text virusScoreText = new Text("" + virusScore);
virusScoreText.setBoundsType(TextBoundsType.VISUAL);
virusScoreText.setMouseTransparent(true);
getChildren().setAll(
circle,
virusScoreText
);
setLayoutX(nextRandInt((int) (Inoculation.W - circle.getRadius() * 2)));
setLayoutY(nextRandInt((int) (Inoculation.H - circle.getRadius() * 2)));
setPickOnBounds(false);
}
public int getVirusScore() {
return virusScore;
}
// next random int between 0 (inclusive) and bound (exclusive)
private int nextRandInt(int bound) {
return ThreadLocalRandom.current().nextInt(bound);
}
}
Some additional notes on this implementation that might be useful to know:
The total score is placed in an overlayPane so that it is not obscured by elements added to the playingField (which contains the virus spawns).
The overlayPane is made mouseTransparent, so that it won't intercept any mouse events, and the clicks will fall through to the items in the playing field.
The app currently generates viruses within a fixed field size, regardless of whether you resize the window. That is just the way it is designed and coded, you could code it otherwise if wished. It would be more work to do so.
The Bindings class is used to create a string expression binding which concatenates the static string "Score " with an integer property representing the score. This allows the string representing the score to be bound to the score label text in the overlay so that it automatically updates whenever the score is changed.
The virus generation uses a timeline and is based on the concepts from:
JavaFX periodic background task
The application class is kept deliberately simple to handle mostly just the core application lifecycle, and the actual functionality of the application is abstracted to an Infection class which handles the spawning of the virus and a Virus class that generates a new virus.
This technique is used to center a score for each individual virus on the virus:
how to put a text into a circle object to display it from circle's center?
The virus itself is laid out in a StackPane. The pane has pick on bounds set to false. To remove the virus infection, you must click on the circle which represents the virus and not just anywhere in the square for the stack pane.
Because circle coordinates are in local co-ordinates and the circle is in a parent stack pane representing the virus, the circle itself does not need x and y values set, instead layout x and y values are set on the enclosing pane to allow positioning of the pane representing the entire virus.
The following technique is used to generate random integers in acceptable ranges using ThreadLocalRandom:
How do I generate random integers within a specific range in Java?

javafx: How do I set the bounds of a Group so it shows a specific window of the many shapes inside, which may or may not exceed my intended bounds?

I have a JavaFX Group that contains a lot of ImageViews and a few overlays. I want it to render a specific mathematical rectangle which usually does not match the bounding box of the combined children. For a simpler concrete example I have created the following app:
import javafx.application.*;
import javafx.scene.*;
import javafx.scene.layout.*;
import javafx.scene.paint.*;
import javafx.scene.shape.*;
import javafx.stage.*;
public class GroupWindow
extends Application
{
#Override
public void start(Stage stage)
throws Exception
{
Group gr = new Group();
Shape circle1 = new Circle(-300, 0, 50);
circle1.setFill(Color.GREEN);
gr.getChildren().add(circle1);
Shape circle2 = new Circle(0, 300, 50);
circle2.setFill(Color.GREEN);
gr.getChildren().add(circle2);
Shape LLrect = new Rectangle(-200, 0, 200,200);
LLrect.setFill(Color.BLUE);
gr.getChildren().add(LLrect);
VBox vbox = new VBox(gr);
stage.setScene(new Scene(vbox));
stage.show();
}
public static void main(String[] args)
{
Application.launch(args);
}
}
When I run the application I get a window that looks like
What I need is for it to look like
I could accomplish this if I knew the method call that would tell the Group to restrict its viewport to x in [-200..200] and y in [-200..200].
One comment suggests setting a clip. Adding a gr.setClip(new Rectangle(-200,-200, 400,400)); causes it to not draw the green circles, but the rendered window encompasses the same space as the first (undesirable) screenshot. So the clip does not affect how the Group decides its rendering window.
What technique should I use to specify this intent to javafx's layout engine?

Customize the stroke of a JavaFX polyline

I have a shape that I would like to render as a JavaFX Polyline, but with multiple colours in the stroke. Essentially, it would be an 8-pixel wide blue line with a 2-pixel wide black border on either side of it. I can achieve the same effect by creating a Group, and then adding two Polylines into the Group:
Group group = new Group();
double[] coords = ...
Polyline bg = new Polyline(coords);
bg.setStroke(Color.BLACK);
bg.setStrokeWidth(12);
Polyline fg = new Polyline(coords);
fg.setStroke(Color.BLUE);
fg.setStrokeWidth(8);
group.getChildren().add(bg);
group.getChildren().add(fg);
So, while that renders the way I want, I now have a Group rather than a Polyline, so I can't treat it as a Shape. I can't see any way to specify a custom drawing mechanism, so is there a way to do this?
Overall, I like the group based approach you outlined in your question. It is unfortunate that it does not fit your particular situation, but for others who come across this question, it may be the preferred solution for their application.
You could apply a DropShadow effect to your PolyLine to generate the border color. This will end up with slightly rounded edges for corners, which may or may not be what you wish.
import javafx.application.Application;
import javafx.scene.*;
import javafx.scene.effect.*;
import javafx.scene.paint.Color;
import javafx.scene.shape.Polyline;
import javafx.stage.Stage;
public class Polyanna extends Application {
#Override
public void start(Stage stage) {
Polyline polyline = new Polyline();
polyline.getPoints().addAll(50.0, 50.0,
200.0, 100.0,
100.0, 200.0
);
polyline.setStrokeWidth(8);
DropShadow borderEffect = new DropShadow(
BlurType.THREE_PASS_BOX, Color.BLUE, 2, 1, 0, 0
);
polyline.setEffect(borderEffect);
stage.setScene(
new Scene(
new Group(polyline),
250, 250
)
);
stage.show();
}
public static void main(String\[\] args) {
launch(args);
}
}
An alternate option is to draw a Polygon with a fill and stroke rather than a PolyLine. You could write a routine which takes an array of points for a Polyline and generates a matching array of points for a Polygon from that input array (with a bit of work ;-)
You can use shape intersection capabilities to create an arbitrary shape that you can fill and stroke to possibly end up with the easiest way to get something closest to what you wish. With this approach you have programmatic control over things like line caps, mitering and line join settings for the stroke of the border color.
import javafx.application.Application;
import javafx.scene.*;
import javafx.scene.paint.Color;
import javafx.scene.shape.*;
import javafx.stage.Stage;
public class PolyIntersect extends Application {
private static final double W = 250;
private static final double H = 250;
#Override
public void start(Stage stage) {
Polyline polyline = new Polyline();
polyline.getPoints().addAll(50.0, 50.0,
200.0, 100.0,
100.0, 200.0
);
polyline.setStrokeWidth(8);
Rectangle bg = new Rectangle(0, 0, W, H);
Shape shape = Shape.intersect(bg, polyline);
shape.setFill(Color.BLACK);
shape.setStroke(Color.BLUE);
shape.setStrokeType(StrokeType.OUTSIDE);
shape.setStrokeWidth(2);
stage.setScene(
new Scene(
new Group(shape),
W, H
)
);
stage.setResizable(false);
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}

adding path transition to Group in JavaFX

I'm writing some javaFX code and I have added some Paths to a Group, this Group has been added to another Group which has been added to the root and is displaying on the screen. Now at a certain point I want to use a PathTransition to animate this lowest level group to a new location. I'm having trouble working out the correct coordinates for the transition. I have read that the PathTransition will animate the node from its center and not the upper left hand corner so I have tried adding
Group.getLayoutX()/2 to the starting x and Group.getLayoutY()/2 to the starting y coord but it still seems to make the group jump to a new starting location before the animation begins.
The final destination seems a bit off as well.
Is there a better way to animate a Group that contains several Paths?
Adjust the layoutX and layoutY of the nodes following paths based upon half the layout bounds of the nodes being animated.
rect.setLayoutX(rect.getLayoutX() + rect.getLayoutBounds().getWidth() / 2);
rect.setLayoutY(rect.getLayoutY() + rect.getLayoutBounds().getHeight() / 2);
Try the modification of Uluk's circle path transition code from JavaFX 2 circle path for animation.
The code will have the upper left corner of the nodes original layout follow the path.
import javafx.animation.PathTransition.OrientationType;
import javafx.animation.*;
import javafx.application.Application;
import javafx.scene.*;
import javafx.scene.paint.Color;
import javafx.scene.shape.*;
import javafx.stage.Stage;
import javafx.util.Duration;
public class ArcToDemo extends Application {
private PathTransition pathTransitionEllipse;
private PathTransition pathTransitionCircle;
private void init(Stage primaryStage) {
Group root = new Group();
primaryStage.setResizable(false);
primaryStage.setScene(new Scene(root, 600, 460));
// Ellipse path example
Rectangle rect = new Rectangle(0, 0, 40, 40);
rect.setArcHeight(10);
rect.setArcWidth(10);
rect.setFill(Color.ORANGE);
root.getChildren().add(rect);
Path path = createEllipsePath(200, 200, 50, 100, 45);
root.getChildren().add(path);
pathTransitionEllipse = PathTransitionBuilder.create()
.duration(Duration.seconds(4))
.path(path)
.node(rect)
.orientation(OrientationType.NONE)
.cycleCount(Timeline.INDEFINITE)
.autoReverse(false)
.build();
rect.setLayoutX(rect.getLayoutX() + rect.getLayoutBounds().getWidth() / 2);
rect.setLayoutY(rect.getLayoutY() + rect.getLayoutBounds().getHeight() / 2);
// Circle path example
Rectangle rect2 = new Rectangle(0, 0, 20, 20);
rect2.setArcHeight(10);
rect2.setArcWidth(10);
rect2.setFill(Color.GREEN);
root.getChildren().add(rect2);
Path path2 = createEllipsePath(400, 200, 150, 150, 0);
root.getChildren().add(path2);
pathTransitionCircle = PathTransitionBuilder.create()
.duration(Duration.seconds(2))
.path(path2)
.node(rect2)
.orientation(OrientationType.NONE)
.cycleCount(Timeline.INDEFINITE)
.autoReverse(false)
.build();
rect2.setLayoutX(rect2.getLayoutX() + rect2.getLayoutBounds().getWidth() / 2);
rect2.setLayoutY(rect2.getLayoutY() + rect2.getLayoutBounds().getHeight() / 2);
}
private Path createEllipsePath(double centerX, double centerY, double radiusX, double radiusY, double rotate) {
ArcTo arcTo = new ArcTo();
arcTo.setX(centerX - radiusX + 1); // to simulate a full 360 degree celcius circle.
arcTo.setY(centerY - radiusY);
arcTo.setSweepFlag(false);
arcTo.setLargeArcFlag(true);
arcTo.setRadiusX(radiusX);
arcTo.setRadiusY(radiusY);
arcTo.setXAxisRotation(rotate);
Path path = PathBuilder.create()
.elements(
new MoveTo(centerX - radiusX, centerY - radiusY),
arcTo,
new ClosePath()) // close 1 px gap.
.build();
path.setStroke(Color.DODGERBLUE);
path.getStrokeDashArray().setAll(5d, 5d);
return path;
}
#Override
public void start(Stage primaryStage) throws Exception {
init(primaryStage);
primaryStage.show();
pathTransitionEllipse.play();
pathTransitionCircle.play();
}
public static void main(String[] args) {
launch(args);
}
}
Implementation Notes
The code assumes that the original layout position of the nodes being animated is 0,0 (if it is not then you will need to adjust accordingly).
For non-square nodes, there is a slight gap between the node and the path (because the calculations are based on the layout bounds rectangle and not the visible shape of the node).
Also the path transition is using an orientation of NONE rather than ORTHOGONAL_TO_TANGENT (otherwise the layout calculations give a strange effect as the node starts to rotate and it seems as though the node is traveling divergent to the path).
Instead of the above approach, you could define a custom subclass of Transition which modifies the TranslateX and TranslateY properties of the node so that the corner of the node follows the path rather than the center and no modifications to the node's layout are required, however that would be significantly more work.

Resources