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:
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).
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).
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):
And here is how it looks at 125% with the updated example: