I am trying to draw a texture on a sphere with JavaFX (16). I add the material with the texture but the texture is stretched to the whole surface. It is possible to set the texture on only a portion of the surface? Like the image below (not mine, taken from SO):
My code so far (very trivial):
public void start(Stage stage) throws Exception {
Sphere sphere = new Sphere(200);
PhongMaterial material = new PhongMaterial();
material.setDiffuseMap(new Image(new File("picture.png").toURI().toURL().toExternalForm()));
sphere.setMaterial(material);
Group group = new Group(sphere);
Scene scene = new Scene( new StackPane(group), 640, 480, true, SceneAntialiasing.BALANCED);
scene.setCamera(new PerspectiveCamera());
stage.setScene(scene);
stage.show();
}
The reason why the texture you apply is stretched to the whole sphere is that the texture coordinates that define the Sphere mesh are mapping the whole sphere surface, and therefore, when you apply a diffuse image, it is translated 1-1 to that surface.
You could create a custom mesh, with custom texture coordinates values, but that can be more complex.
Another option is to create the diffuse image "on demand", based on your needs.
For a sphere, a 2D image that can be wrapped around the 3D sphere can be defined by a 2*r*PI x 2*r rectangular container (a JavaFX Pane for our purposes).
Then, you can draw inside your images, scaling and translating them accordingly.
Finally, you need a way to convert that drawing into an image, and for that you can use Scene::snapshot.
Just to play around with this idea, I'll create a rectangular grid that will be wrapped around the sphere, in order to have some kind of a coordinate system.
private Image getTexture(double r) {
double h = 2 * r;
double w = 2 * r * 3.125; // 3.125 is ~ PI, rounded to get perfect squares.
Pane pane = new Pane();
pane.setPrefSize(w, h);
pane.getStyleClass().add("pane-grid");
Group rootAux = new Group(pane);
Scene sceneAux = new Scene(rootAux, rootAux.getBoundsInLocal().getWidth(), rootAux.getBoundsInLocal().getHeight());
sceneAux.getStylesheets().add(getClass().getResource("/style.css").toExternalForm());
SnapshotParameters sp = new SnapshotParameters();
return rootAux.snapshot(sp, null);
}
where style.css has:
.pane-grid {
-fx-background-color: #D3D3D333,
linear-gradient(from 0.5px 0.0px to 50.5px 0.0px, repeat, black 5%, transparent 5%),
linear-gradient(from 0.0px 0.5px to 0.0px 50.5px, repeat, black 5%, transparent 5%);
}
.pane-solid {
-fx-background-color: black;
}
(based on this answer)
With a radius of 400, you get this image:
each square is 50x50, and there are 50x16 squares.
If you apply this diffuse map to an Sphere:
#Override
public void start(Stage stage) {
PhongMaterial earthMaterial = new PhongMaterial();
earthMaterial.setDiffuseMap(getTexture(400));
final Sphere earth = new Sphere(400);
earth.setMaterial(earthMaterial);
Scene scene = new Scene(root, 800, 600, true);
scene.setFill(Color.WHITESMOKE);
stage.setScene(scene);
stage.show();
}
you get:
In theory, now you could fill any of the grid squares, like:
private Image getTexture(double r) throws IOException {
double h = 2 * r;
double w = 2 * r * 3.125;
Pane pane = new Pane();
pane.setPrefSize(w, h);
pane.getStyleClass().add("pane-grid");
Rectangle rectangle = new Rectangle(50, 50, Color.RED);
rectangle.setStroke(Color.WHITE);
rectangle.setStrokeWidth(2);
// fill rectangle at 20 x 10
rectangle.setTranslateX(20 * 50 + 1);
rectangle.setTranslateY(10 * 50 + 1);
Group rootAux = new Group(pane, rectangle);
...
with the result:
Now that you have a well positioned image (for now just a red rectangle), you can get rid of the grid, and simply use a black color for the texture image:
private Image getTexture(double r) throws IOException {
double h = 2 * r;
double w = 2 * r * 3.125;
Pane pane = new Pane();
pane.setPrefSize(w, h);
// pane.getStyleClass().add("pane-grid");
pane.getStyleClass().add("pane-solid");
resulting in:
Now it is up to you to apply this idea to your needs. Note that you can use an ImageView with size 50x50, or 100x100, ... instead of the red rectangle, so you can use a more complex image.
Related
I have a method that rotates an image and uses the drawImage method to display it onto a canvas. However, when rotating the image, the image shrinks and grows because the width and height change (say you rotate a square, the width and height of the image changes). Here is the method:
public void rotateImage(GraphicsContext gc, double speed) {
erase(gc); // erases the previous image
imgView.setRotate(imgView.getRotate() + speed);
SnapshotParameters params = new SnapshotParameters();
params.setFill(Color.TRANSPARENT);
image = imgView.snapshot(params, null);
gc.drawImage(image, pos.x, pos.y, width, height);
}
Any help would be appreciated, and I can post the rest of the code if needed.
A snapshot with the provided parameters uses the dimensions of the node in the parent to determine the size of the image. Rotating an image yields dimensions different to those of the original image in most cases. In those cases the snapshot is bigger than the original image. (Consider a square image rotated by 45°; The width and height of the rotated image is the size of the diagonal of the original image, i.e. larger by a factor of sqrt(2) = 1.41...).
Since drawImage scales the drawn image to fit into a rectangle of size width x height, the snapshot that is larger than this size is scaled down.
Use the transforms of the GraphicsContext instead to avoid creating a new Image instance with each call of the method and avoid scaling the image.
Example
#Override
public void start(Stage primaryStage) {
Image image = new Image("https://upload.wikimedia.org/wikipedia/commons/thumb/8/85/Smiley.svg/240px-Smiley.svg.png");
Canvas canvas = new Canvas(500, 500);
GraphicsContext context = canvas.getGraphicsContext2D();
Slider slider = new Slider(0, 360, 0);
Button btn = new Button("draw");
VBox root = new VBox(canvas, slider, btn);
btn.setOnAction(evt -> {
context.setFill(Color.TRANSPARENT);
context.fillRect(0, 0, canvas.getWidth(), canvas.getHeight());
double posX = 200;
double posY = 150;
context.save();
// apply transformation that puts makes (posX, posY) the point
// where (0,0) is drawn and rotate
context.translate(posX, posY);
context.rotate(slider.getValue());
// draw with center at (0, 0)
context.drawImage(image, -image.getWidth()/2, -image.getHeight()/2);
// undo transformations
context.restore();
});
primaryStage.setScene(new Scene(root));
primaryStage.show();
}
I am creating a board game (first ported to JavaFX) in which the player must kill the opponent's piece going through a loop.
The above is provided on the Wikipedia page for Surakurta (another name for Permanin). But, I have been only able to build a grid of this sort:
How can I create those roundabouts in the corners?
Implementation details for already built grid: GridPane filled with 36 BoardInput extends javafx.scene.control.Button objects. These objects are special because they automatically create a Background with three BackgroundFill objects - horizontal line, vertical line, and the pebble circular fill.
Use a Path. ArcTo elements allow you to create the circular parts. HLineTo, VLineTo and ClosePath can be used for the straight sections:
Furthermore I don't recommend using BackgroundFills. I'd prefer overlaying invisible buttons on top of the board visuals or handling MouseEvents for the GridPane itself.
Example
private static ArcTo createArc(double radius, double dx, double dy) {
ArcTo result = new ArcTo(radius, radius, 0, dx, dy, true, true);
result.setAbsolute(false);
return result;
}
private static HLineTo createHLine(double length) {
HLineTo result = new HLineTo(length);
result.setAbsolute(false);
return result;
}
private static VLineTo createVLine(double length) {
VLineTo result = new VLineTo(length);
result.setAbsolute(false);
return result;
}
private static Path createPath(double radius, double midSize, Color storke) {
final double lineLength = 2 * radius + midSize;
Path result = new Path(
new MoveTo(radius, 2 * radius), // start at left end of top horizontal line
createArc(radius, radius, -radius), // top left loop
createVLine(lineLength), // down
createArc(radius, -radius, -radius), // bottom left loop
createHLine(lineLength), // right
createArc(radius, -radius, radius), // bottom right loop
createVLine(-lineLength), // up
createArc(radius, radius, radius),
new ClosePath() // left
);
result.setStroke(storke);
result.setStrokeWidth(10);
return result;
}
#Override
public void start(Stage primaryStage) throws Exception {
Scene scene = new Scene(new StackPane(
createPath(100, 50, Color.GREEN),
createPath(50, 150, Color.AQUA)
));
primaryStage.setScene(scene);
primaryStage.show();
}
Output
I am making a simple simulation, and have had a good amount of trouble finding the X and Y coordinates of a rotated, weirdly sized, imageView node. (The blue bit is the front)
The goal is to find out an XY coordinate relative to the direction that the imageView is pointing, after it has been rotated. I can find the angle that the imageView is at relative to its starting position but I cannot figure out how to get an XY coordinate of the imageView relative to this angle. Since the .setRotate(angle) method does not change the X and Y location of the imageView, how should I go about finding a point that the imageView is facing?
Minimal example of the rotation and imageView I am using:
Main.java
public class Main extends Application {
#Override
public void start(Stage primaryStage) {
Group root = new Group();
primaryStage.setScene(new Scene(root, 500, 500));
Image robotImage = null;
try {
robotImage = new Image(new FileInputStream("res\\robot.png"));
} catch (FileNotFoundException e) {
e.printStackTrace();
}
ImageView robot = new ImageView(robotImage);
robot.setLayoutX(125);
robot.setLayoutY(125);
System.out.println("PreRotate X: " + robot.getLayoutX() + "PreRotate Y: " + robot.getLayoutY());
robot.setRotate(45);
System.out.println("PostRotate X: " + robot.getLayoutX() + "PostRotate Y: " + robot.getLayoutY());
root.getChildren().add(robot);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
I have already tried using the bounds of the imageView along with lines that lay on top of the imageView, but that requires me to find the new max/min x/y every time that the imageView changes its max/min x/y.
For example:
if (turnAngle < 35) {
directionLine.setStartX(robotLeftRightAngle.getBoundsInParent().getMaxX());
directionLine.setStartY(robotLeftRightAngle.getBoundsInParent().getMinY());
directionLine.setEndX(robotRightLeftAngle.getBoundsInParent().getMinX() + ((robotLeftRightAngle.getBoundsInParent().getMaxX() - robotRightLeftAngle.getBoundsInParent().getMinX()) / 2));
directionLine.setEndY(robotRightLeftAngle.getBoundsInParent().getMinY() + ((robotLeftRightAngle.getBoundsInParent().getMinY() - robotRightLeftAngle.getBoundsInParent().getMinY()) / 2));
}
else if (turnAngle < 55) {
directionLine.setStartX(robotLeftRightAngle.getBoundsInParent().getMaxX());
directionLine.setStartY(robotLeftRightAngle.getBoundsInParent().getMaxY());
directionLine.setEndX(robotRightLeftAngle.getBoundsInParent().getMinX() + ((robotLeftRightAngle.getBoundsInParent().getMaxX() - robotRightLeftAngle.getBoundsInParent().getMinX()) / 2));
directionLine.setEndY(robotRightLeftAngle.getBoundsInParent().getMinY() + ((robotLeftRightAngle.getBoundsInParent().getMaxY() - robotRightLeftAngle.getBoundsInParent().getMinY()) / 2));
}
And so on all the way to 360. DRY yikes.
How should I approach this? Am I using the wrong transformation? Did I not see a method that can be used for this? I know that there must be a better approach. Thanks for reading.
I'm not sure I 100% understand the question. The transformations between coordinate systems but it's hard to tell the coordinate systems you need to convert between from your description, so I assume you want to convert between the coordinate system of robot to the coordinate system of group.
It's possible to use localToParent to convert from the coordinate system of a node to that of the parent which accomodates for all transforms. (parentToLocal would achieve the inverse transformation, but this does not seem to be the required transformation in this case.)
The following example modifies the start and end points of a line to the coordinates of the top left and a point 100 px above of the Rectangle in the Rectangle's coordinate system:
#Override
public void start(Stage primaryStage) {
Group root = new Group();
primaryStage.setScene(new Scene(root, 500, 500));
Rectangle robot = new Rectangle(100, 20, Color.RED);
robot.setLayoutX(125);
robot.setLayoutY(125);
Line line = new Line(125, 125, 125, 25);
robot.rotateProperty().addListener(o -> {
Point2D start = robot.localToParent(0, 0);
Point2D end = robot.localToParent(0, -100);
line.setStartX(start.getX());
line.setStartY(start.getY());
line.setEndX(end.getX());
line.setEndY(end.getY());
});
RotateTransition rotateTransition = new RotateTransition(Duration.seconds(5), robot);
rotateTransition.setCycleCount(Animation.INDEFINITE);
rotateTransition.setFromAngle(0);
rotateTransition.setToAngle(360);
rotateTransition.setInterpolator(Interpolator.LINEAR);
rotateTransition.play();
root.getChildren().addAll(robot, line);
primaryStage.show();
}
So I want to put a dot on a circle. I don't want it centered I want it for example in the right corner of the circle but I want it to be a part of the circle not just another circle placed on top of this one. The reason I want it this way is to show the Rotation Transition of the circle more clearly. How do make this happen?
You could put the Circles in a Group and rotate that group instead of the Circle:
public void start(Stage primaryStage) {
Circle circle = new Circle(100);
Circle dot = new Circle(20, 30, 10, Color.RED);
Group group = new Group(circle, dot);
group.setLayoutX(100);
group.setLayoutY(200);
Pane root = new Pane(group);
Scene scene = new Scene(root, 500, 500);
RotateTransition transition = new RotateTransition(Duration.seconds(1), group);
transition.setByAngle(360);
transition.setInterpolator(Interpolator.LINEAR);
transition.setCycleCount(Animation.INDEFINITE);
transition.play();
primaryStage.setScene(scene);
primaryStage.show();
}
I'm working on a interactive sort application. I must represent numbers as rectangles, and for example, when the sorting algorithm is running, when two numbers are swapped, the rectangles must be swapped. I want to do this with animations. How can I swap the rectangles? I currently testing this using transition but I have some problems. I have two rectangles in a group. When I try to swap the rectangles, both will meet in the middle and stop. Here's the code:
Rectangle r1 = rectangles.get(numbers[0]);
Rectangle r2 = rectangles.get(numbers[1]);
TranslateTransition translateTransition = new TranslateTransition();
translateTransition.setNode(r1);
translateTransition.setDuration(Duration.millis(1000));
translateTransition.setFromX(r1.getX());
translateTransition.setToX(r2.getX());
TranslateTransition translateTransition2 = new TranslateTransition();
translateTransition2.setNode(r2);
translateTransition2.setDuration(Duration.millis(1000));
translateTransition2.setFromX(r2.getX());
translateTransition2.setToX(r1.getX());
translateTransition2.play();
translateTransition.play();
I need a pane similar with canvas. I need to be able to set the rectangles coordinates.
TranslateTransition works with translateX property of the Node. Thus, if you positioned rectangles using setLayout, relocate or just constructor parameter TranslateTransition wouldn't work for you.
You either need to start using translateX coordinates or use Timeline instead of TranslateTransition.
You can read more about layout and translate in JavaDoc for layout
Here is translateX based swap example:
public void start(Stage primaryStage) {
final Rectangle r1 = new Rectangle(50, 50, Color.RED);
final Rectangle r2 = new Rectangle(50, 50, Color.BLUE);
// note I use translate to position rectangles
r1.setTranslateX(50);
r2.setTranslateX(250);
Button btn = new Button();
btn.setText("Move it");
btn.relocate(100, 100);
btn.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
double x1 = r1.getTranslateX();
double x2 = r2.getTranslateX();
TranslateTransition translateTransition = new TranslateTransition();
translateTransition.setNode(r1);
translateTransition.setDuration(Duration.millis(1000));
translateTransition.setToX(x2);
TranslateTransition translateTransition2 = new TranslateTransition();
translateTransition2.setNode(r2);
translateTransition2.setDuration(Duration.millis(1000));
translateTransition2.setToX(x1);
translateTransition2.play();
translateTransition.play();
}
});
Pane root = new Pane();
root.getChildren().addAll(btn, r1, r2);
Scene scene = new Scene(root, 400, 350);
primaryStage.setTitle("Hello World!");
primaryStage.setScene(scene);
primaryStage.show();
}