Java - Does subpixel line accuracy require an AffineTransform?
Asked Answered
H

1

9

I've never worked with Java drawing methods before, so I decided to dive in and create an analog clock as a PoC. In addition to the hands, I draw a clock face that includes tick marks for minutes/hours. I use simple sin/cos calculations to determine the position of the lines around the circle.

However, I've noticed that since the minute tick-marks are very short, the angle of the lines looks wrong. I'm certain this is because both Graphics2D.drawLine() and Line2D.double() methods cannot draw with subpixel accuracy.

I know I can draw lines originating from the center and masking it out with a circle (to create longer, more accurate lines), but that seems like such an inelegant and costly solution. I've done some research on how to do this, but the best answer I've come across is to use an AffineTransform. I assume I could use an AffineTransform with rotation only, as opposed to having to perform a supersampling.

Is this the only/best method of drawing with sub-pixel accuracy? Or is there a potentially faster solution?

Edit: I am already setting a RenderingHint to the Graphics2D object.

As requested, here is a little bit of the code (not fully optimized as this was just a PoC):

diameter = Math.max(Math.min(pnlOuter.getSize().getWidth(),
                             pnlOuter.getSize().getHeight()) - 2, MIN_DIAMETER);

for (double radTick = 0d; radTick < 360d; radTick += 6d) {
   g2d.draw(new Line2D.Double(
      (diameter / 2) + (Math.cos(Math.toRadians(radTick))) * diameter / 2.1d,
      (diameter / 2) + (Math.sin(Math.toRadians(radTick))) * diameter / 2.1d,
      (diameter / 2) + (Math.cos(Math.toRadians(radTick))) * diameter / 2.05d,
      (diameter / 2) + (Math.sin(Math.toRadians(radTick))) * diameter / 2.05d));
} // End for(radTick)

Here's a screenshot of the drawing. It may be somewhat difficult to see, but look at the tick mark for 59 minutes. It is perfectly vertical.

Sample image

Hasp answered 16/2, 2011 at 15:53 Comment(1)
screenshot of the clock and code for calculating/drawing the clock would help.Numbat
D
8

Line2D.double() methods cannot draw with subpixel accuracy.

Wrong, using RenderingHints.VALUE_STROKE_PURE the Graphics2D object can draw "subpixel" accuracy with the shape Line2D.


I assume I could use an AffineTransform with rotation only, as opposed to having to perform a supersampling. Is this the only/best method of drawing with sub-pixel accuracy? Or is there a potentially faster solution?

I think you are missing somthing here. The Graphics2D object already holds a AffineTransform and it is using it for all drawing actions and its cheap performance wise.


But to get back to you what is missing from your code - this is missing:

g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,
                     RenderingHints.VALUE_STROKE_PURE);

Below is a self contained example that produces this picture:

screenshot

public static void main(String[] args) throws Exception {

    final JFrame frame = new JFrame("Test");

    frame.add(new JComponent() {
        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);

            Graphics2D g2d = (Graphics2D) g;

            System.out.println(g2d.getTransform());
            g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                                 RenderingHints.VALUE_ANTIALIAS_ON);
            g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,
                                 RenderingHints.VALUE_STROKE_PURE);

            double dia = Math.min(getWidth(), getHeight()) - 2;

            for (int i = 0; i < 60 ; i++) {
                double angle = 2 * Math.PI * i / 60;
                g2d.draw(new Line2D.Double(
                        (dia / 2) + Math.cos(angle) * dia / 2.1d,
                        (dia / 2) + Math.sin(angle) * dia / 2.1d,
                        (dia / 2) + Math.cos(angle) * dia / 2.05d,
                        (dia / 2) + Math.sin(angle) * dia / 2.05d));
            }

            g2d.draw(new Ellipse2D.Double(1, 1, dia - 1, dia - 1));
        }
    });

    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    frame.setSize(400, 400);
    frame.setVisible(true);
}
Dunois answered 16/2, 2011 at 16:38 Comment(7)
Hmm, it appears you are using an AffineTransform that does have to translate (i.e. supersample) as well as rotate. I am hoping to avoid this, as I currently have this updating every 50ms.Hasp
Shouldn't you draw the clock face separately and then merge it in with the changing clock hands?Numbat
@D.N.: I'm only creating the offline image once? Do you need to create it every single update? (@typo.pl: yes)Dunois
Good point, but the hands have the same issue as the ticks, just not as prominent. You can especially tell with the minute hand, which "jumps" from one pixel position to the next instead of a smooth transition. I'd like to have the sub-pixel accuracy on all components.. :(Hasp
Beautiful! Oh so very beautiful! This is exactly what was missing, thank you! Thanks for the clarification on the use of AffineTransform in Graphics2D. I already knew that the implementation was hardly using processing power (1 hour of 50ms updates took 0.3s of processing time), so I didn't want to disrupt the efficiency. It takes considerably more processing time now, but it's still completely negligible (I measured 0.025s in 60s, which is roughly 5x more processing).Hasp
Also, I noticed one slight improvement in your code that I hadn't optimized, and that's storing the conversion to radians and using that to reduce the complexity of calculations. The compiler might help me, but yours is guaranteed to be faster. Nice touch :)Hasp
@D.N.: No problem! This was also good to know so thanks for the question! :)Dunois

© 2022 - 2024 — McMap. All rights reserved.