JavaFx canvas filltext rendering quality - java

I am working on a JavaFx application which use canvas to represent a diagram. The canvas is painting text using graphicsContext.fillText(). In the image below the canvas is on the right, on the left is a Label using the same font. My question is which renering parameter should I use to make the right text look the same as on the left ?
public class SampleRenderingIssue extends Application {
private final StackPane root = new StackPane();
private final Scene scene = new Scene(root, 300, 250);
private final BorderPane pane = new BorderPane();
private final Canvas canvas = new Canvas();
#Override
public void start(Stage stage) {
stage.setTitle("Sample Canvas");
VBox vbox = new VBox();
VBox.setVgrow(pane, Priority.ALWAYS);
vbox.getChildren().addAll( pane );
pane.getChildren().add(canvas);
root.getChildren().add(vbox);
stage.setScene(scene);
stage.sizeToScene();
setupCanvasPane();
stage.show();
Platform.runLater(()-> paint());
}
private void setupCanvasPane(){
canvas.widthProperty().bind(pane.widthProperty());
canvas.heightProperty().bind(pane.heightProperty());
pane.widthProperty().addListener((o,p,c)-> paint());
paint();
}
public void paint(){
GraphicsContext gr = canvas.getGraphicsContext2D();
gr.clearRect( 0,0, canvas.getWidth(), canvas.getHeight() );
gr.setFill( Color.web("#222222") );
gr.fillRect( 0,0,canvas.getWidth(), canvas.getHeight());
gr.setStroke( Color.WHITE );
gr.setFill( Color.WHITE );
gr.setLineWidth( 1d );
gr.strokeLine( 0,0, canvas.getWidth(), canvas.getHeight() );
gr.setFont( Font.font( "Candara"));
gr.fillText("This is a text", 100, 100 );
gr.setFont( Font.font( "Monospaced"));
gr.fillText("This is a text", 100, 120 );
}
public static void main(String[] args) {
launch(args);
}
}
The issue reproduces on a FullHd display where the Windows scale in Control Panel is set to 125% ( default value for a notebook ).

I also had poor text rendering quality in a Canvas. I display dense text in a small font size and it looked quite ugly before.
This may or may not be the same problem you have been running up against, since my Windows scaling is at 100%, but I've discovered in my case it was caused by the Canvas not using the same ClearType implementation as the rest of the UI widgets do. It seems to be set to GREY mode instead of LCD. It's a simple one line fix:
GraphicsContext gc = canvas.getGraphicsContext2D();
gc.setFontSmoothingType(FontSmoothingType.LCD);
gc.setFont(Font.font("Consolas", 10));
gc.fillText("Example ABC 123", 10, 10);
Here is the result, in each case the top line is the rendered version, and the bottom line is a Label in the scene. I've included a zoomed in version, where you can clearly see Canvas is not doing the LCD subpixel rendering correctly
In the right handle example they appear to be identical, and my visual quality is restored.

Related

How to set up and transform a Camera in a SubScene?

I am trying to get an isometric view of a grid. The grid is only part of the main scene so I created a subscene and I want add a camera to it. I will want to be able to zoom the camera in and out and pan it around preserving the viewing angle throughout all these operation.
Here is what I have without the camera:
public class MyApp extends Application {
#Override
public void start(Stage stage) throws Exception {
Button actionButton = new Button("Placeholder\t\t\t\t\t\t\t\t\t\t\t\t");
HBox hbox = new HBox(actionButton);
BorderPane mainPane = new BorderPane(new MyView(), null, null, hbox, null);
Scene scene = new Scene(mainPane);
stage.setScene(scene);
stage.show();
}
private class MyView extends Group {
MyView() {
super();
GridPane grid = new GridPane();
for (int i = 0; i < 64; i++) {
Rectangle tile = new Rectangle(30, 30, Color.GREEN);
BorderPane pane = new BorderPane(tile);
pane.setBorder(new Border(new BorderStroke(null, BorderStrokeStyle.SOLID,
null, null, null)));
grid.add(pane, i / 8, i % 8);
}
Group root = new Group();
root.getChildren().add(grid);
SubScene scene = new SubScene(root, 300, 300, true, SceneAntialiasing.BALANCED);
scene.setFill(Color.DARKCYAN); // just to see the area
getChildren().add(scene);
}
}
public static void main(String[] args) throws Exception {
launch(args);
}
}
Now I add the camera like this in the MyView constructor:
Camera camera = new PerspectiveCamera(true);
scene.setCamera(camera);
and the grid disappears.
I didn't even do any transformation yet (i would do camera.getTransforms().addAll(new Rotate(-15, Rotate.Y_AXIS));). What am i doing wrong?
also, how can I tell the subscene to take whatever space is available? I don't want to need to specify specific size because the program can run on all sorts of screens.
Your camera is positioned at the same z coordinates as the Group. However you have to make sure it's at a distance between farClip and nearClip:
PerspectiveCamera camera = new PerspectiveCamera(true);
camera.setTranslateZ(-100);
camera.setFieldOfView(120);
Furthermore for isometric views PerspectiveCamera is the wrong Camera to use. Use ParallelCamera instead:
Camera camera = new ParallelCamera();
//camera.setRotationAxis(new Point3D(1, 1, 0));
//camera.setRotate(30);
scene.setCamera(camera);
also, how can I tell the subscene to take whatever space is available?
Change the type extended by MyView to something that is resizable and bind the size of the SubScene to the size of MyView:
private class MyView extends Pane {
MyView() {
...
setPrefSize(300, 300);
scene.widthProperty().bind(widthProperty());
scene.heightProperty().bind(heightProperty());
}

JavaFX: Trouble binding properties to panes

I am a beginner who just recently started learning JavaFX, and I seem to be making the same reoccurring mistake within my programs. For example, in the following code I am trying to draw the X-Axis and Y-Axis and have it binded to half the width and height of the pane. When executed, the axes are very small and located at the topleft corner, but as you resize the window of the application, the axes slowly increase in size until the window not being resized anymore.
public class Debug extends Application {
#Override
public void start(Stage primaryStage) {
Pane pane = new Pane();
GraphFunc test = new GraphFunc();
pane.getChildren().add(test);
Scene scene = new Scene(pane, 500, 500);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
class GraphFunc extends Pane {
private double xAxisSpan, yAxisSpan;
public GraphFunc() {
xAxisSpan = 100;
yAxisSpan = 100;
drawAxes();
}
private void drawAxes() {
Line xAxis = new Line(0, 50, 100, 50);
Line yAxis = new Line(50, 0, 50, 100);
xAxis.setStartX(0);
xAxis.startYProperty().bind(heightProperty().divide(2));
xAxis.endXProperty().bind(widthProperty());
xAxis.endYProperty().bind(heightProperty().divide(2));
yAxis.startXProperty().bind(widthProperty().divide(2));
yAxis.setStartY(0);
yAxis.endXProperty().bind(widthProperty().divide(2));
yAxis.endYProperty().bind(heightProperty());
getChildren().addAll(xAxis, yAxis);
}
}
I am confused because when pane is change to a StackPane, this does not happen. Also if I moved the code in drawAxes() to start() and added the lines to pane it would also not do this. Please explain, I cannot seem to understand what is happening after researching and playing around with it.
This is happening because of Pane. Pane is meant to be used when absolute positioning of children is required.
This is from Oracle documentation:
This class may be used directly in cases where absolute positioning of children is required since it does not perform layout beyond resizing resizable children to their preferred sizes. It is the application's responsibility to position the children since the pane leaves the positions alone during layout.
Pane pane = new Pane();
GraphFunc test = new GraphFunc();
pane.getChildren().add(test);
Scene scene = new Scene(pane, 500, 500);
If you remove the width and height from the scene and instead set the width and height directly on to test like this:
public void start(Stage primaryStage) {
GridPane pane = new GridPane();
GraphFunc test = new GraphFunc();
test.setPrefSize(500, 500);
pane.getChildren().add(test);
Scene scene = new Scene(pane);
primaryStage.setScene(scene);
primaryStage.show();
}
You will get this:
drawn image
However you will not be able to span.
From Oracle documentation:
Note: if an application needs children to be kept aligned within a
parent (centered, positioned at top-left, etc), it should use a
StackPane instead.
Also from Oracle documentation:
Pane does not clip its content by default, so it is possible that
childrens' bounds may extend outside its own bounds, either if
children are positioned at negative coordinates or the pane is resized
smaller than its preferred size.

JavaFX - label size not initialized

I am working on an application that enables the user to draw graphs, i.e. edges and nodes. As nodes I am currently using plain JavaFX label elements. When drawing the edges, I need to consider the bounds of the label, however, the width and the height of the labels seem to be initialized only after "drawing" it. E.g., when starting the application the label bounds have a width/height of 0, but if a label is repositioned by the user, the width/height is correct.
Is there a possibility to force JavaFX to draw the current elements? The code is rather complex, but the following gives an idea of what I want to do:
stackpane = new StackPane();
text = new Label("Test");
text.setStyle("-fx-border-color:black; -fx-padding:3px;");
stackpane.getChildren().addAll(text);
...
// is it possible to force JavaFX to draw the text here?
...
// some calculations with the bounds of the label
Node node = getLabel();
Bounds bounds = node.getBoundsInParent();
double height = bounds.getHeight();
double width = bounds.getWidth();
I also tried to wrap the text in a rectangle and then manually set the width/height of the rectangle. This works, but the nodes have labels of different length and thus manually setting it is not always suitable.
You may try to bind your logic to node.boundsInParentProperty() changes
public class SmartBorder extends Application {
#Override
public void start(Stage primaryStage) {
final Label txt = new Label("Example");
txt.relocate(100, 100);
Pane root = new Pane();
final Rectangle border = new Rectangle();
border.setFill(Color.TRANSPARENT);
border.setStroke(Color.RED);
// here we autoupdate border
txt.boundsInParentProperty().addListener(new ChangeListener<Bounds>() {
#Override
public void changed(ObservableValue<? extends Bounds> ov, Bounds old, Bounds b) {
border.setX(b.getMinX() - 1);
border.setY(b.getMinY() - 1);
border.setWidth(b.getWidth()+2);
border.setHeight(b.getHeight()+2);
}
});
root.getChildren().addAll(txt, border);
// click to see automatic border reaction
root.setOnMouseClicked(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent t) {
txt.relocate(Math.random()*200, Math.random()*200);
txt.setText(Math.random() + "");
}
});
Scene scene = new Scene(root, 300, 250);
primaryStage.setTitle("Hello World!");
primaryStage.setScene(scene);
primaryStage.show();
}
}

JavaFX : Draw on Center Pane based on user input in Left Pane

Here is my summary code
public class KlArgon extends Application {
BorderPane border ;
Scene scene ;
Stage stage;
#Override
public void start(Stage stage) {
:
border = new BorderPane();
:
HBox infoBox = addInfoHBox();
border.setTop(infoBox);
:
VBox menuBox = addMenuVBox();
border.setLeft(menuBox);
:
border.setCenter(addAnchorPane(addGridPane()));
// setRight and setBottom is not used
:
scene = new Scene (border);
stage.setScene(scene);
stage.show();
}
private Node addAnchorPane(GridPane grid) {
AnchorPane anchorpane = new AnchorPane();
anchorpane.getChildren().add(grid);
AnchorPane.setTopAnchor(grid, 10.0);
return anchorpane;
}
private GridPane addGridPane() {
GridPane grid = new GridPane();
grid.setHgap(10);
grid.setVgap(10);
grid.setPadding(new Insets(0, 10, 0, 10));
grid.add(addWhiteboard(), 1, 0);
return grid;
}
private Node addWhiteboard() {
Canvas canvas = new Canvas (wboardWd, wdboardHt);
GraphicsContext gc = canvas.getGraphicsContext2D();
drawShapes(gc);
drawfromClipboard(gc);
return canvas;
}
}
I refer to the Center pane as the "Whiteboard". Among other things, I have two buttons in menuBox - btnCopyFromClipboard and btnClearWhiteboard.
When user presses btnCopyFromClipboard - the user should be able to draw an rectangle in the "Whiteboard" only (i.e. Center pane only) and then the clipboard image will be copied (scaled) into that rectangle.
So I made border,scene, stage as global and I am trying to get this to work - not only it is buggy/ugly- to me it looks like a hack. Is there a cleaner way to do this i.e. manage Center Pane when button in left pane is pressed?
Basically I want the Center Pane to be the Canvas and the GraphicsContext operations are performed whe the Buttons in Left Pane is pressed.
What I have working is pressing the btnCopyFromClipboard lets me draw the rectangle anywhere/everywhere (instead of limiting it to the Center Pane / the whiteboard). I want to restrict the rectangle to be drawn inside the Center Pane / the whiteboard only.
Some inputs/pointers from someone who has been through this will be very much appreciated.
http://docs.oracle.com/javafx/2/layout/builtin_layouts.htm was helpful to get me started.
I was in a fix as well and here is a question which I asked in Oracle Forums, to which, James gave me a vivid reply. Please go through this, it has your answer
https://community.oracle.com/thread/2598756

Fade smoothly between gradients

The following code should fade a rectangle smoothly between two gradients:
public class FXTest extends Application {
#Override
public void start(Stage primaryStage) {
StackPane root = new StackPane();
Rectangle rect = new Rectangle(200, 100, new LinearGradient(0, 0, 1, 1, true, CycleMethod.REPEAT, new Stop(0, Color.BLACK), new Stop(1, Color.RED)));
root.getChildren().add(rect);
Scene scene = new Scene(root, 300, 250);
primaryStage.setScene(scene);
primaryStage.show();
getFillTimeline(rect, new LinearGradient(0, 0, 1, 1, true, CycleMethod.REPEAT, new Stop(0, Color.RED), new Stop(1, Color.BLACK))).play();
}
private Timeline getFillTimeline(Shape node, Paint newPaint) {
Timeline t = new Timeline();
t.getKeyFrames().add(new KeyFrame(Duration.ZERO, new KeyValue(node.fillProperty(), node.getFill())));
t.getKeyFrames().add(new KeyFrame(Duration.seconds(3), new KeyValue(node.fillProperty(), newPaint)));
return t;
}
}
If I specify plain colours in the above example, then the fade occurs no problem, nice and smoothly. However, if I use gradients (as in the code above) then no fade occurs, the gradient just switches suddenly after 3 seconds (or whatever the duration of the timeline is.)
How can I achieve a gradual fade when utilising a gradient rather than a simple colour? Is there some logic I'm missing behind why this isn't meant to work the way I'm thinking, or is it a bug?
In either case, what would be the best workaround to achieve this effect?
I don't suppose it would know how to make the smooth transition between to fundamentally different paints. Try setting up the solid color as a gradient, where both start- and end- color are of same color.

Categories