Customize the stroke of a JavaFX polyline - javafx

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

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

Why is my javafx code centering all my shapes?

I'm trying to teach myself JavaFX and tried to create a simple smiley face image. But for some reason all my shapes wind up centered instead of at the x&y coordinates that I constructed them with. I can't figure out why. Can anyone help me figure it out?
Here's my code:
import javafx.*;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.*;
import javafx.stage.Stage;
public class smiley extends Application {
public static void main(String[] args) {
launch(args);
}
public void start(Stage primaryStage) throws Exception {
Circle head = new Circle(250, 250, 150);
head.setFill(Color.YELLOW);
Circle eyeL = new Circle(200, 175, 25);
eyeL.setFill(Color.BLACK);
Circle eyeR = new Circle(300, 175, 25);
eyeR.setFill(Color.BLACK);
double[] points = { 250.0, 200.0, 250.0, 275.0, 290.0, 275.0 };
Polygon nose = new Polygon(points);
Arc smile = new Arc(275.0, 300.0, 75.0, 50.0, 180.0, 180.0);
smile.setStroke(Color.RED);
smile.setFill(Color.YELLOW);
StackPane smiley = new StackPane();
smiley.getChildren().add(head);
smiley.getChildren().add(eyeL);
smiley.getChildren().add(eyeR);
smiley.getChildren().add(nose);
smiley.getChildren().add(smile);
Scene scene = new Scene(smiley, 500, 500);
primaryStage.setTitle("Smiley");
primaryStage.setScene(scene);
primaryStage.show();
}
}
And this is what I wind up with
StackPane lays out its children on top of each other. Just think of a stack of pancakes. The last one goes on top and the first one is at the bottom. If you set a border/padding, then the children will by place in the center within those insets. Additionally, StackPane will resize all the children to fit the content area, but if that's not possible, it will then place it in the center. I don't think that this is your intent here, so try using the Pane class, which StackPane inherits from.
Pane pane = new Pane();

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?

JavaFX: How can I best place a Label centered in a Shape?

Let's say I already have a Shape on the screen. For example:
Circle circle = new Circle(x, y, radius);
circle.setFill(Color.YELLOW);
root.getChildren().add(circle);
I would like to create a Label "over" that Circle such that the Label is centered in the Circle, the font size is maximized to fit inside the Circle, etc.
I can see how this could be accomplished via binding, but that seems needlessly complicated if the position/size of these things will never change during runtime.
Thank you in advance for your help! I'm very new to JavaFX and not all that experienced at programming in the first place, so I apologize if I should've been able to find this out via my research.
Use a StackPane to automatically center the text on top of the shape.
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.*;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.text.*;
import javafx.stage.Stage;
// java 8 code.
public class Circular extends Application {
public static void main(String[] args) throws Exception {
launch(args);
}
#Override
public void start(final Stage stage) throws Exception {
Text text = createText("Xyzzy");
Circle circle = encircle(text);
StackPane layout = new StackPane();
layout.getChildren().addAll(
circle,
text
);
layout.setPadding(new Insets(20));
stage.setScene(new Scene(layout));
stage.show();
}
private Text createText(String string) {
Text text = new Text(string);
text.setBoundsType(TextBoundsType.VISUAL);
text.setStyle(
"-fx-font-family: \"Times New Roman\";" +
"-fx-font-style: italic;" +
"-fx-font-size: 48px;"
);
return text;
}
private Circle encircle(Text text) {
Circle circle = new Circle();
circle.setFill(Color.ORCHID);
final double PADDING = 10;
circle.setRadius(getWidth(text) / 2 + PADDING);
return circle;
}
private double getWidth(Text text) {
new Scene(new Group(text));
text.applyCss();
return text.getLayoutBounds().getWidth();
}
}
Related
how to put a text into a circle object to display it from circle's center?
The answer to the related question discusses different bounds types for text (such as Visual bounds), in case you need that.
StackPane stackPane = new StackPane();
Circle circle = new Circle();
Label label = new Label("Hi");
circle.setFill(Color.GOLD);
circle.setStroke(Color.GRAY);
circle.radiusProperty().bind(label.widthProperty());
stackPane.getChildren().addAll(circle, label);

JavaFX material's bump and spec maps

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

Resources