JavaFX - avoid blur when drawing with rect on Canvas with highdpi

Viewed 728

I'd like to draw on a JavaFX Canvas. Coordinates for the canvas are provided by double values, but I understand that in order to "snap" to the pixel grid, I need integer numbers to avoid drawing "inbetween" pixels, which will result in a blur/smoothing on edges.

Consider this minimal example:

public class App extends Application {

    @Override
    public void start(Stage stage) {

        var vbox = new VBox();
        var canvas = new Canvas(600, 400);
        var gc = canvas.getGraphicsContext2D();

        // draw a background
        gc.beginPath();
        gc.setFill(Color.rgb(200,200,200));
        gc.rect(0, 0, 600, 400);
        gc.fill();

        // draw a smaller square
        gc.beginPath();
        gc.setFill(Color.rgb(100,100,100));
        gc.rect(9.0, 9.0, 50, 50);  // snap to grid
        gc.fill();

        vbox.getChildren().addAll(canvas);

        var scene = new Scene(vbox, 640, 480);
        stage.setScene(scene);
        stage.show();
    }

    public static void main(String[] args) {
        launch();
    }

}

This works and will result in a clear edge, consider this image:

enter image description here

Now suppose HighDPI is enabled, i.e. consider if we scale graphics in Windows 10 to 125 percent. This will result in a scale factor of 1.25 (we can obtain this via getOutputScaleX/Y).

However then the whole canvas is scaled with that factor, and we get a blur. See the attached images (but you have to zoom, and maybe view them in a graphics program and not in a browser).

enter image description here

Then I thought that we can adjust the original coordinates such that the scaled coordinates will result in integers. For example to make sure we hit an integer with offset 9 * 1.25 = 11.25 on the scaled image, let's try to target 11.0 instead, i.e. change this line

gc.rect(9.0, 9.0, 50, 50); 

to

gc.rect(11.0/1.25, 11.0/1.25, 50, 50); 

But this still results in a blur at the edge.

How can we work around this? Ideally I would like to turn off dpi scaling completely for the canvas, and do my own (pixel-perfect) calculations. Is this possible or is there any other solution?

For drawImage there is a parameter setSmoothing, but there is nothing when drawing shapes directly (like rect).

1 Answers

You can solve this by eliminating the scaling used by scaling the Canvas to a smaller size. Here is a sample (note that lines are drawn "in between" pixels in JavaFX, so to get them sharp you need to add 0.5).

This example also places a Group around the scaled Canvas. This is required to take the scale into account when doing layout, but also has some effect on rendering. This example was tested at 100%, 125% and 150%.

import javafx.application.Application;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.stage.Stage;

public class CanvasTest extends Application {

  public static void main(String[] args) {
    Application.launch(args);
  }

  @Override
  public void start(Stage stage) throws Exception {
    HBox hbox = new HBox();

    Canvas canvas = new Canvas(100,100);

    GraphicsContext g2d = canvas.getGraphicsContext2D();

    g2d.fillRect(15, 15, 60, 10);
    g2d.fillRect(15.5, 30, 60, 10);  // supposed to be blurry
    g2d.fillRect(16, 45, 60, 10);
    g2d.fillRect(16.5, 60, 60, 10);  // supposed to be blurry

    g2d.setStroke(Color.RED);
    g2d.strokeLine(13.5, 13.5, 13.5, 93.5);
    g2d.strokeLine(13.5, 13.5, 93.5, 93.5);
    hbox.getChildren().add(new Group(canvas));

    Scene scene = new Scene(hbox);

    stage.setScene(scene);
    stage.show();

    canvas.scaleXProperty().bind(new SimpleDoubleProperty(1.0).divide(scene.getWindow().outputScaleXProperty()));
    canvas.scaleYProperty().bind(new SimpleDoubleProperty(1.0).divide(scene.getWindow().outputScaleYProperty()));
  }
}

Here is how it looks on a 150% monitor (left side is scaled up 5 times so you can see there is no blur):

enter image description here

And here is how it looks at 125% with the updated example:

enter image description here

Related