问题
I have been trying to make a program in Java using Graphics2D and PaintComponent. However, Java is not rounding some values correctly resulting in a few points being rendered a pixel different from where it supposed to be which gives an unclean image. You can see what I mean in the picture below.
Here is my code. What can I change to fix this? Thank you!
public void paintComponent( Graphics g )
{
super.paintComponent( g );
g.setColor(Color.red);
int orginX = getWidth()/2;
int orginY = getHeight()/2;
for(int i=0; i<=360; i+= 10)
{
double angle = Math.toRadians(i);
double centerX = radius * Math.cos(angle) + orginX;
double centerY = radius * Math.sin(angle) + orginY;
int[] anglePointsX = {(int) (radius * Math.cos(angle+Math.toRadians(60)) + centerX), (int) (radius * Math.cos(angle-Math.toRadians(60)) + centerX), orginX};
int[] anglePointsY = {(int) (radius * Math.sin(angle+Math.toRadians(60)) + centerY), (int) (radius * Math.sin(angle-Math.toRadians(60)) + centerY), orginY};
g.drawPolygon(anglePointsX, anglePointsY, 3);
}
回答1:
I was going to suggest you could draw two separate lines:
g.drawPolygon(anglePointsX, anglePointsY, 2); // the outter line.
g.drawLine(orginX, orginY, anglePointsX[0], anglePointsY[0]); // from origin to one outer point
Almost worked, only one line was not as expected.
So then my next suggestion is again to draw two separate lines, but this time just rotate the line that is drawn from the origin.
super.paintComponent( g );
g.setColor(Color.red);
Graphics2D g2 = (Graphics2D)g.create();
int radius = 100;
int orginX = getWidth()/2;
int orginY = getHeight()/2;
double radians = Math.toRadians(60);
for(int i=0; i < 360; i+= 10)
{
double angle = Math.toRadians(i);
double centerX = radius * Math.cos(angle) + orginX;
double centerY = radius * Math.sin(angle) + orginY;
int[] anglePointsX = {(int) (radius * Math.cos(angle + radians) + centerX), (int) (radius * Math.cos(angle - radians) + centerX), orginX};
int[] anglePointsY = {(int) (radius * Math.sin(angle + radians) + centerY), (int) (radius * Math.sin(angle - radians) + centerY), orginY};
g.drawPolygon(anglePointsX, anglePointsY, 2);
// g2.drawLine(orginX, orginY, anglePointsX[0], anglePointsY[0]);
AffineTransform af = new AffineTransform();
af.translate(orginX, orginY);
af.rotate( angle );
g2.setTransform( af );
g2.drawLine(0, 0, 175, 0);
}
The problem with this approach is that I don't know how to calculate the length of the line from the origin and just hardcoded 175. Maybe your math is better than mine and you know how to calculate the fixed length for the line.
The brute force method to determine the maximum line length can be to create two loops. The first loop paints the outer lines and determines the maximum x value used. The second loop then paints 36 rotated lines using this value as the length:
protected void paintComponent(Graphics g)
{
super.paintComponent( g );
g.setColor(Color.red);
Graphics2D g2 = (Graphics2D)g.create();
int radius = 100;
int orginX = getWidth()/2;
int orginY = getHeight()/2;
double radians60 = Math.toRadians(60);
int maxX = 0;
for(int i=0; i < 360; i+= 10)
{
double angle = Math.toRadians(i);
double centerX = radius * Math.cos(angle) + orginX;
double centerY = radius * Math.sin(angle) + orginY;
int[] anglePointsX = {(int) (radius * Math.cos(angle + radians60) + centerX), (int) (radius * Math.cos(angle - radians60) + centerX), orginX};
int[] anglePointsY = {(int) (radius * Math.sin(angle + radians60) + centerY), (int) (radius * Math.sin(angle - radians60) + centerY), orginY};
g.drawPolygon(anglePointsX, anglePointsY, 2);
maxX = Math.max(maxX, anglePointsX[0]);
}
for(int i=0; i < 360; i+= 10)
{
double angle = Math.toRadians(i);
AffineTransform af = new AffineTransform();
af.translate(orginX, orginY);
af.rotate( angle );
g2.setTransform( af );
g2.drawLine(0, 0, maxX - orginX, 0);
}
g2.dispose();
}
回答2:
In this case, the reason for the "thick" lines was indeed a rounding error. So what you observed was not really an issue of the drawing, but of the computation. Note that for various reasons (mainly the limited precision of double
values in general), you can not assume that all results of the trigonometric functions like cos
and sin
are what you might expect them to be. For example,
System.out.println(Math.sin(Math.toRadians(180)));
will print 1.2246467991473532E-16
, although mathematically, it should be 0.0
. Similarly, some of the values that you computed might end up being something like 199.999999999941234234
when the result should be 200.0
. Then, when you are casting these values to int
, you are truncating the fractional part, and the result will be 199
- causing the odd visual effect that you observed.
In this case, this could, pragmatically, be solved by rounding the results using Math.round
, as it is done in the following example in the PixelsOffByOnePanel
.
For completeness, the PixelsOffByOnePanelNicer
class shows how you can do this with a Path2D
and double
values.
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GridLayout;
import java.awt.geom.Path2D;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
public class PixelsOffByOne
{
public static void main(String[] args)
{
SwingUtilities.invokeLater(new Runnable()
{
@Override
public void run()
{
JFrame frame = new JFrame("");
frame.getContentPane().setLayout(new GridLayout(1,2));
frame.getContentPane().add(new PixelsOffByOnePanel());
frame.getContentPane().add(new PixelsOffByOnePanelNicer());
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
}
class PixelsOffByOnePanel extends JPanel
{
int radius = 100;
@Override
public Dimension getPreferredSize()
{
return new Dimension(400,400);
}
@Override
public void paintComponent( Graphics g )
{
super.paintComponent( g );
g.setColor(Color.RED);
int orginX = getWidth()/2;
int orginY = getHeight()/2;
for(int i=0; i<=360; i+=10)
{
double angle = Math.toRadians(i);
double centerX = radius * Math.cos(angle) + orginX;
double centerY = radius * Math.sin(angle) + orginY;
int x0 = (int) Math.round(radius * Math.cos(angle+Math.toRadians(60)) + centerX);
int x1 = (int) Math.round(radius * Math.cos(angle-Math.toRadians(60)) + centerX);
int[] anglePointsX = {x0, x1, orginX};
int y0 = (int) Math.round(radius * Math.sin(angle+Math.toRadians(60)) + centerY);
int y1 = (int) Math.round(radius * Math.sin(angle-Math.toRadians(60)) + centerY);
int[] anglePointsY = {y0, y1, orginY};
g.drawPolygon(anglePointsX, anglePointsY, 3);
}
}
}
class PixelsOffByOnePanelNicer extends JPanel
{
int radius = 100;
@Override
public Dimension getPreferredSize()
{
return new Dimension(400,400);
}
@Override
protected void paintComponent(Graphics gr)
{
super.paintComponent(gr);
Graphics2D g = (Graphics2D)gr;
g.setColor(Color.RED);
int originX = getWidth()/2;
int originY = getHeight()/2;
double d = Math.toRadians(60);
Path2D path = new Path2D.Double();
for(int i=0; i<=360; i+=10)
{
double angle = Math.toRadians(i);
double centerX = radius * Math.cos(angle) + originX;
double centerY = radius * Math.sin(angle) + originY;
double x0 = (int) Math.round(radius * Math.cos(angle+d) + centerX);
double x1 = (int) Math.round(radius * Math.cos(angle-d) + centerX);
double x2 = originX;
double y0 = (int) Math.round(radius * Math.sin(angle+d) + centerY);
double y1 = (int) Math.round(radius * Math.sin(angle-d) + centerY);
double y2 = originY;
path.moveTo(x0, y0);
path.lineTo(x1, y1);
path.lineTo(x2, y2);
path.closePath();
}
g.draw(path);
}
}
A side note: Usually, the drawing may look even nicer when you enable anti-aliasing, by calling
g.setRenderingHint(
RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
before starting to draw. But as you are drawing several lines multiple times, the result may not look as pleasing as it could if you dedicatedly only drew each line only once.
回答3:
Try using i < 360
instead i<=360
, as you've already drawen a line at this angle (0 == 360)
Try using a Path2D
or GeneralPath
rather then drawPolygon
as this will give you float
and double
precision
Have a look at Drawing Arbitrary Shapes for more details
Updated...
So I changed i <= 360
to i < 360
and didn't see any immediate changes, however, then applied some rendering hints and it seemed to improve the problem.
I started by reducing the number of elements to 6 (this is where I started seeing an issue) with
int dif = (int) (360 / 6d);
for (int i = 0; i < 360; i += dif) {
Then I applied some rendering hints...
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
g2d.setColor(Color.red);
g2d.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);
g2d.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_ENABLE);
g2d.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);
int orginX = getWidth() / 2;
int orginY = getHeight() / 2;
int dif = (int) (360 / 6d);
for (int i = 0; i < 360; i += dif) {
double angle = Math.toRadians(i);
double centerX = radius * Math.cos(angle) + orginX;
double centerY = radius * Math.sin(angle) + orginY;
int[] anglePointsX = {(int) (radius * Math.cos(angle + Math.toRadians(60)) + centerX), (int) (radius * Math.cos(angle - Math.toRadians(60)) + centerX), orginX};
int[] anglePointsY = {(int) (radius * Math.sin(angle + Math.toRadians(60)) + centerY), (int) (radius * Math.sin(angle - Math.toRadians(60)) + centerY), orginY};
g2d.drawPolygon(anglePointsX, anglePointsY, 3);
}
g2d.dispose();
}
Before and after comparison...
I then changed the drawing from using drawPolygon
to use the Shape
API, in particular a Path2D
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
g2d.setColor(Color.red);
g2d.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);
g2d.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_ENABLE);
g2d.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);
int orginX = getWidth() / 2;
int orginY = getHeight() / 2;
int dif = (int) (360 / 6d);
for (int i = 0; i < 360; i += dif) {
double angle = Math.toRadians(i);
double centerX = radius * Math.cos(angle) + orginX;
double centerY = radius * Math.sin(angle) + orginY;
Path2D path = new Path2D.Double();
path.moveTo(radius * Math.cos(angle + Math.toRadians(60)) + centerX, radius * Math.sin(angle + Math.toRadians(60)) + centerY);
path.lineTo((radius * Math.cos(angle - Math.toRadians(60)) + centerX), (radius * Math.sin(angle - Math.toRadians(60)) + centerY));
path.lineTo(orginX, orginY);
g2d.draw(path);
}
g2d.dispose();
}
With and without rendering hints
As you can, even without rendering hints, the issue is gone
回答4:
Two different problems, perhaps.
Problem 1, aliasing. You need to turn it on, using something like the following:
https://docs.oracle.com/javase/tutorial/2d/advanced/quality.html
As for the lines that appear to be thicker than they should, you are drawing them twice, which makes them appear thicker. So the best solution is to avoid drawing them twice. Try to refactor your code to draw lines instead of polygons so each line is drawn only once.
来源:https://stackoverflow.com/questions/32532038/java-graphics2d-is-drawing-one-pixel-off-rounding-error