How to draw a crisp, opaque hairline in JavaFX 2.2?

Solution 1:

You can use a Region subclass, such as a Pane for your Parent, with snapToPixel set to true.

Additionally, refer to the Node documentation on the co-ordinate system.

At the device pixel level, integer coordinates map onto the corners and cracks between the pixels and the centers of the pixels appear at the midpoints between integer pixel locations. Because all coordinate values are specified with floating point numbers, coordinates can precisely point to these corners (when the floating point values have exact integer values) or to any location on the pixel. For example, a coordinate of (0.5, 0.5) would point to the center of the upper left pixel on the Stage. Similarly, a rectangle at (0, 0) with dimensions of 10 by 10 would span from the upper left corner of the upper left pixel on the Stage to the lower right corner of the 10th pixel on the 10th scanline. The pixel center of the last pixel inside that rectangle would be at the coordinates (9.5, 9.5).

Also see the Shape documentation:

Most nodes tend to have only integer translations applied to them and quite often they are defined using integer coordinates as well. For this common case, fills of shapes with straight line edges tend to be crisp since they line up with the cracks between pixels that fall on integer device coordinates and thus tend to naturally cover entire pixels. On the other hand, stroking those same shapes can often lead to fuzzy outlines because the default stroking attributes specify both that the default stroke width is 1.0 coordinates which often maps to exactly 1 device pixel and also that the stroke should straddle the border of the shape, falling half on either side of the border. Since the borders in many common shapes tend to fall directly on integer coordinates and those integer coordinates often map precisely to integer device locations, the borders tend to result in 50% coverage over the pixel rows and columns on either side of the border of the shape rather than 100% coverage on one or the other. Thus, fills may typically be crisp, but strokes are often fuzzy.

Two common solutions to avoid these fuzzy outlines are to use wider strokes that cover more pixels completely - typically a stroke width of 2.0 will achieve this if there are no scale transforms in effect - or to specify either the StrokeType.INSIDE or StrokeType.OUTSIDE stroke styles - which will bias the default single unit stroke onto one of the full pixel rows or columns just inside or outside the border of the shape.

So, if you leave your Nodes in a Group or Region which does not snapToPixel, you can follow the above instructions from the Shape documentation.

Here is some sample code:

import javafx.application.Application;
import javafx.scene.*;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Line;
import javafx.scene.shape.LineBuilder;
import javafx.scene.shape.StrokeType;
import javafx.scene.text.Text;
import javafx.stage.Stage;

/** http://stackoverflow.com/questions/11886230/how-to-draw-a-crisp-opaque-hairline-in-javafx-2-2 */
public class LineWidths extends Application {
  public static void main(String[] args) { launch(args); }

  @Override public void start(Stage stage) {
    Line fuzzyline = LineBuilder.create()
        .startX(5).startY(50)
        .endX(90).endY(50)
        .stroke(Color.BLACK).strokeWidth(1)
      .build();
    Line hairline = LineBuilder.create()
        .startX(4.5).startY(99.5)
        .endX(89.5).endY(99.5)
        .stroke(Color.BLACK).strokeWidth(1)
      .build();
    Line fatline = LineBuilder.create()
        .startX(5).startY(150)
        .endX(90).endY(150)
        .stroke(Color.BLACK).strokeWidth(1).strokeType(StrokeType.OUTSIDE)
      .build();
    Pane snappedPane = new Pane();
    Line insideline = LineBuilder.create()
        .startX(5).startY(25)
        .endX(90).endY(25)
        .stroke(Color.BLACK).strokeWidth(1)
      .build();
    snappedPane.setSnapToPixel(true);
    snappedPane.getChildren().add(insideline);
    snappedPane.setPrefSize(100, 50);
    snappedPane.relocate(-0.5, 174.5);

    stage.setScene(
      new Scene(
        new Group(
          fuzzyline, hairline, fatline, snappedPane,
          new Text(10, 40, "fuzzyline"),  
          new Text(10, 90, "hairline"),  
          new Text(10, 140, "fatline"),  
          new Text(10, 190, "snappedPane")
        ), 100, 250
      )
    );
    stage.show();
  }
}

Line Type Sample