Rounding Inaccuracies When Combining Areas in Java? - java
I'm working with Areas in Java.
My test program draws three random triangles and combines them to form one or more polygons. After the Areas are .add()ed together, I use PathIterator to trace the edges.
Sometimes, however, the Area objects will not combine as they should... and as you can see in the last image I posted, extra edges will be drawn.
I think the problem is caused by rounding inaccuracies in Java's Area class (when I debug the test program, the Area shows the gaps before the PathIterator is used), but I don't think Java provides any other way to combine shapes.
Any solutions?
Example code and images:
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.geom.Area;
import java.awt.geom.Line2D;
import java.awt.geom.Path2D;
import java.awt.geom.PathIterator;
import java.util.ArrayList;
import java.util.Random;
import javax.swing.JFrame;
public class AreaTest extends JFrame{
private static final long serialVersionUID = -2221432546854106311L;
Area area = new Area();
ArrayList<Line2D.Double> areaSegments = new ArrayList<Line2D.Double>();
AreaTest() {
Path2D.Double triangle = new Path2D.Double();
Random random = new Random();
// Draw three random triangles
for (int i = 0; i < 3; i++) {
triangle.moveTo(random.nextInt(400) + 50, random.nextInt(400) + 50);
triangle.lineTo(random.nextInt(400) + 50, random.nextInt(400) + 50);
triangle.lineTo(random.nextInt(400) + 50, random.nextInt(400) + 50);
triangle.closePath();
area.add(new Area(triangle));
}
// Note: we're storing double[] and not Point2D.Double
ArrayList<double[]> areaPoints = new ArrayList<double[]>();
double[] coords = new double[6];
for (PathIterator pi = area.getPathIterator(null); !pi.isDone(); pi.next()) {
// Because the Area is composed of straight lines
int type = pi.currentSegment(coords);
// We record a double array of {segment type, x coord, y coord}
double[] pathIteratorCoords = {type, coords[0], coords[1]};
areaPoints.add(pathIteratorCoords);
}
double[] start = new double[3]; // To record where each polygon starts
for (int i = 0; i < areaPoints.size(); i++) {
// If we're not on the last point, return a line from this point to the next
double[] currentElement = areaPoints.get(i);
// We need a default value in case we've reached the end of the ArrayList
double[] nextElement = {-1, -1, -1};
if (i < areaPoints.size() - 1) {
nextElement = areaPoints.get(i + 1);
}
// Make the lines
if (currentElement[0] == PathIterator.SEG_MOVETO) {
start = currentElement; // Record where the polygon started to close it later
}
if (nextElement[0] == PathIterator.SEG_LINETO) {
areaSegments.add(
new Line2D.Double(
currentElement[1], currentElement[2],
nextElement[1], nextElement[2]
)
);
} else if (nextElement[0] == PathIterator.SEG_CLOSE) {
areaSegments.add(
new Line2D.Double(
currentElement[1], currentElement[2],
start[1], start[2]
)
);
}
}
setSize(new Dimension(500, 500));
setLocationRelativeTo(null); // To center the JFrame on screen
setDefaultCloseOperation(EXIT_ON_CLOSE);
setResizable(false);
setVisible(true);
}
public void paint(Graphics g) {
// Fill the area
Graphics2D g2d = (Graphics2D) g;
g.setColor(Color.lightGray);
g2d.fill(area);
// Draw the border line by line
g.setColor(Color.black);
for (Line2D.Double line : areaSegments) {
g2d.draw(line);
}
}
public static void main(String[] args) {
new AreaTest();
}
}
A successful case:
A failing case:
Here:
for (int i = 0; i < 3; i++) {
triangle.moveTo(random.nextInt(400) + 50, random.nextInt(400) + 50);
triangle.lineTo(random.nextInt(400) + 50, random.nextInt(400) + 50);
triangle.lineTo(random.nextInt(400) + 50, random.nextInt(400) + 50);
triangle.closePath();
area.add(new Area(triangle));
}
you are adding in fact
1 triangle in the first loop
2 triangles in the second loop
3 triangles in the third loop
This is where your inaccuracies come from. Try this and see if your problem still persists.
for (int i = 0; i < 3; i++) {
triangle.moveTo(random.nextInt(400) + 50, random.nextInt(400) + 50);
triangle.lineTo(random.nextInt(400) + 50, random.nextInt(400) + 50);
triangle.lineTo(random.nextInt(400) + 50, random.nextInt(400) + 50);
triangle.closePath();
area.add(new Area(triangle));
triangle.reset();
}
Note the path reset after each loop.
EDIT: to explain more where the inaccuracies come from here the three paths you try to combine. Which makes it obvious where errors might arise.
I've re-factored your example to make testing easier, adding features of both answers. Restoring triangle.reset() seemed to eliminate the artifatcts for me. In addition,
Build the GUI on the event dispatch thread.
For rendering, extend a JComponent, e.g. JPanel, and override paintComponent().
Absent subcomponents having a preferred size, override getPreferredSize().
Use RenderingHints.
SSCCE:
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.event.ActionEvent;
import java.awt.geom.AffineTransform;
import java.awt.geom.Area;
import java.awt.geom.Line2D;
import java.awt.geom.Path2D;
import java.awt.geom.PathIterator;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import javax.swing.AbstractAction;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JSpinner;
import javax.swing.SpinnerNumberModel;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
/** #see http://stackoverflow.com/q/9526835/230513 */
public class AreaTest extends JPanel {
private static final int SIZE = 500;
private static final int INSET = SIZE / 10;
private static final int BOUND = SIZE - 2 * INSET;
private static final int N = 5;
private static final AffineTransform I = new AffineTransform();
private static final double FLATNESS = 1;
private static final Random random = new Random();
private Area area = new Area();
private List<Line2D.Double> areaSegments = new ArrayList<Line2D.Double>();
private int count = N;
AreaTest() {
setLayout(new BorderLayout());
create();
add(new JPanel() {
#Override
public void paintComponent(Graphics g) {
Graphics2D g2d = (Graphics2D) g;
g2d.setRenderingHint(
RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
g.setColor(Color.lightGray);
g2d.fill(area);
g.setColor(Color.black);
for (Line2D.Double line : areaSegments) {
g2d.draw(line);
}
}
#Override
public Dimension getPreferredSize() {
return new Dimension(SIZE, SIZE);
}
});
JPanel control = new JPanel();
control.add(new JButton(new AbstractAction("Update") {
#Override
public void actionPerformed(ActionEvent e) {
create();
repaint();
}
}));
JSpinner countSpinner = new JSpinner();
countSpinner.setModel(new SpinnerNumberModel(N, 3, 42, 1));
countSpinner.addChangeListener(new ChangeListener() {
#Override
public void stateChanged(ChangeEvent e) {
JSpinner s = (JSpinner) e.getSource();
count = ((Integer) s.getValue()).intValue();
}
});
control.add(countSpinner);
add(control, BorderLayout.SOUTH);
}
private int randomPoint() {
return random.nextInt(BOUND) + INSET;
}
private void create() {
area.reset();
areaSegments.clear();
Path2D.Double triangle = new Path2D.Double();
// Draw three random triangles
for (int i = 0; i < count; i++) {
triangle.moveTo(randomPoint(), randomPoint());
triangle.lineTo(randomPoint(), randomPoint());
triangle.lineTo(randomPoint(), randomPoint());
triangle.closePath();
area.add(new Area(triangle));
triangle.reset();
}
// Note: we're storing double[] and not Point2D.Double
List<double[]> areaPoints = new ArrayList<double[]>();
double[] coords = new double[6];
for (PathIterator pi = area.getPathIterator(I, FLATNESS);
!pi.isDone(); pi.next()) {
// Because the Area is composed of straight lines
int type = pi.currentSegment(coords);
// We record a double array of {segment type, x coord, y coord}
double[] pathIteratorCoords = {type, coords[0], coords[1]};
areaPoints.add(pathIteratorCoords);
}
// To record where each polygon starts
double[] start = new double[3];
for (int i = 0; i < areaPoints.size(); i++) {
// If we're not on the last point, return a line from this point to the next
double[] currentElement = areaPoints.get(i);
// We need a default value in case we've reached the end of the List
double[] nextElement = {-1, -1, -1};
if (i < areaPoints.size() - 1) {
nextElement = areaPoints.get(i + 1);
}
// Make the lines
if (currentElement[0] == PathIterator.SEG_MOVETO) {
// Record where the polygon started to close it later
start = currentElement;
}
if (nextElement[0] == PathIterator.SEG_LINETO) {
areaSegments.add(
new Line2D.Double(
currentElement[1], currentElement[2],
nextElement[1], nextElement[2]));
} else if (nextElement[0] == PathIterator.SEG_CLOSE) {
areaSegments.add(
new Line2D.Double(
currentElement[1], currentElement[2],
start[1], start[2]));
}
}
}
public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
JFrame f = new JFrame();
f.add(new AreaTest());
f.pack();
f.setLocationRelativeTo(null);
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.setResizable(false);
f.setVisible(true);
}
});
}
}
I played around with this, and found a hacky way of getting rid of these. I'm not 100% sure that this will work in all cases, but it might.
After reading that the Area.transform's JavaDoc mentions
Transforms the geometry of this Area using the specified
AffineTransform. The geometry is transformed in place, which
permanently changes the enclosed area defined by this object.
I had a hunch and added possibility of rotating the Area by holding down a key. As the Area was rotating, the "inward" edges started to slowly disappear, until only the outline was left. I suspect that the "inward" edges are actually two edges very close to each other (so they look like a single edge), and that rotating the Area causes very small rounding inaccuracies, so the rotating sort of "melts" them together.
I then added a code to rotate the Area in very small steps for a full circle on keypress, and it looks like the artifacts disappear:
The image on the left is the Area built from 10 different random triangles (I upped the amount of triangles to get "failing" Areas more often), and the one on the right is the same Area, after being rotated full 360 degrees in very small increments (10000 steps).
Here's the piece of code for rotating the area in small steps (smaller amounts than 10000 steps would probably work just fine for most cases):
final int STEPS = 10000; //Number of steps in a full 360 degree rotation
double theta = (2*Math.PI) / STEPS; //Single step "size" in radians
Rectangle bounds = area.getBounds(); //Getting the bounds to find the center of the Area
AffineTransform trans = AffineTransform.getRotateInstance(theta, bounds.getCenterX(), bounds.getCenterY()); //Transformation matrix for theta radians around the center
//Rotate a full 360 degrees in small steps
for(int i = 0; i < STEPS; i++)
{
area.transform(trans);
}
As I said before, I'm not sure if this works in all cases, and the amount of steps needed might be much smaller or larger depending on the scenario. YMMV.
Related
Image moving to the target
A and C images are far away. At this time, B image is created from the coordinate values of A For each iteration, x, y must be moved to arrive at the C image coordinate value. For example, A (100,100), C (300,300) Starting at B (100,100), Each time it is repeated, x, y must be moved to reach B (300,300). This is the method for entering the current moving source. Public void Attack () { int x1 = a.getX (); int y1 = a.getY (); int x2 = c.getX (); int y2 = c.getY (); if(b.getX ()==c.getX&&b.getY () == c.getY())' { system.out.println ("ok");' }else { b.setbounds (help1,help2,100,50) } } Here, I want to know the code to enter help1 help2. Pythagorean formula Between A and C images I want to know how to make the B image move along a virtual straight line. Like a tower defense game. A image is a tower B image is bullet The C image is the enemy. I want the bullets fired from the tower to move to enemy locations. I am Korean. I used a translator
The following code is an mre of using a straight line equation y = mx + c to draw a moving object along such line. To test the code copy the entire code into MoveAlongStraightLine.java and run, or run it online: import java.awt.BasicStroke; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.geom.Ellipse2D; import java.awt.geom.Point2D; import javax.swing.JButton; import javax.swing.JFrame; import javax.swing.JPanel; import javax.swing.SwingUtilities; import javax.swing.Timer; public class MoveAlongStraightLine extends JFrame { public MoveAlongStraightLine(){ setDefaultCloseOperation (JFrame.EXIT_ON_CLOSE); ShootinBoard shootingBoard = new ShootinBoard(); JButton fire = new JButton("Fire"); fire.addActionListener(e->shootingBoard.fire()); add(shootingBoard); add(fire, BorderLayout.SOUTH); pack(); setVisible(true); } public static void main(String[]args){ SwingUtilities.invokeLater(()->new MoveAlongStraightLine()); } } class ShootinBoard extends JPanel{ //use constants for better readability private static final double W = 600, H = 400; private static final int DOT_SIZE = 10, SPEED = 2; private final int dX = 1; //x increment private final Timer timer; private final Point2D.Double shooter, target; private Point2D.Double bullet; public ShootinBoard() { setPreferredSize(new Dimension((int)W, (int)H)); shooter = new Point2D.Double(50,350); bullet = shooter; //place bullet at start point target = new Point2D.Double(550,50); timer = new Timer(SPEED, e->moveBullet()); } #Override public void paintComponent(Graphics g){ super.paintComponent(g); Graphics2D g2 = (Graphics2D)g; g2.setStroke(new BasicStroke(3)); //draw source g2.setColor(Color.blue); g2.draw(new Ellipse2D.Double(shooter.getX(), shooter.getY(),DOT_SIZE , DOT_SIZE)); //draw bullet g2.setColor(Color.black); g2.draw(new Ellipse2D.Double(bullet.getX(), bullet.getY(),DOT_SIZE , DOT_SIZE)); //draw target g2.setColor(Color.red); g2.draw(new Ellipse2D.Double(target.getX(), target.getY(),DOT_SIZE , DOT_SIZE)); } void fire(){ timer.stop(); bullet = shooter; //place bullet at start point timer.start(); } void moveBullet() { if(target.x == bullet.x && target.y == bullet.y) { timer.stop(); } //y = mx + c for more details see https://www.usingmaths.com/senior_secondary/java/straightline.php double m = (target.y - bullet.y)/ (target.x - bullet.x);//slope double c = (target.x * bullet.y - bullet.x * target.y)/(target.x - bullet.x); double newBulletX = bullet.x+dX; //increment x double newBulletY = m * newBulletX + c; //calculate new y bullet = new Point2D.Double(newBulletX,newBulletY); repaint(); } }
Java - Calculating and placing the angle of a geometric shape
I'm currently working on a program which enables user to draw various geometric shapes. However, I got some issues on calculating and placing the angle objects onto my Canvas panel accurately. The angle object is basically an extension of the Arc2D object, which provides a additional method called computeStartAndExtent(). Inside my Angle class, this method computes and finds the necessary starting and extension angle values: private void computeStartAndExtent() { double ang1 = Math.toDegrees(Math.atan2(b1.getY2() - b1.getY1(), b1.getX2() - b1.getX1())); double ang2 = Math.toDegrees(Math.atan2(b2.getY2() - b2.getY1(), b2.getX2() - b2.getX1())); if(ang2 < ang1) { start = Math.abs(180 - ang2); extent = ang1 - ang2; } else { start = Math.abs(180 - ang1); extent = ang2 - ang1; } start -= extent; } It is a bit buggy code that only works when I connect two lines to each other, however, when I connect a third one to make a triangle, the result is like the following, As you see the ADB angle is the only one that is placed correctly. I couldn't figure how to overcome this. If you need some additional info/code please let me know. EDIT: b1 and b2 are Line2D objects in computeStartAndExtent() method. Thank you.
There are some of things that can be made to simplify the calculation: Keep the vertices ordered, so that it is always clear how to calculate the vertex angles pointing away from the corner Furthermore, always draw the polygon to the same direction; then you can always draw the angles to the same direction. The example below assumes the polygon is drawn clockwise. The same angle calculation would result in the arcs drawn outside given a polygon drawn counterclockwise. Example code; is not quite the same as yours as I don't have your code, but has similar functionality: import java.awt.Dimension; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Shape; import java.awt.geom.Arc2D; import javax.swing.JFrame; import javax.swing.JPanel; import javax.swing.SwingUtilities; public class Polygon extends JPanel { private static final int RADIUS = 20; private final int[] xpoints = { 10, 150, 80, 60 }; private final int[] ypoints = { 10, 10, 150, 60 }; final Arc2D[] arcs; Polygon() { arcs = new Arc2D[xpoints.length]; for (int i = 0; i < arcs.length; i++) { // Indices of previous and next corners int prev = (i + arcs.length - 1) % arcs.length; int next = (i + arcs.length + 1) % arcs.length; // angles of sides, pointing outwards from the corner double ang1 = Math.toDegrees(Math.atan2(-(ypoints[prev] - ypoints[i]), xpoints[prev] - xpoints[i])); double ang2 = Math.toDegrees(Math.atan2(-(ypoints[next] - ypoints[i]), xpoints[next] - xpoints[i])); int start = (int) ang1; int extent = (int) (ang2 - ang1); // always draw to positive direction, limit the angle <= 360 extent = (extent + 360) % 360; arcs[i] = new Arc2D.Float(xpoints[i] - RADIUS, ypoints[i] - RADIUS, 2 * RADIUS, 2 * RADIUS, start, extent, Arc2D.OPEN); } } #Override public Dimension getPreferredSize() { return new Dimension(160, 160); } #Override protected void paintComponent(Graphics g) { super.paintComponent(g); g.drawPolygon(xpoints, ypoints, xpoints.length); Graphics2D g2d = (Graphics2D) g; for (Shape s : arcs) { g2d.draw(s); } } public static void main(String args[]){ SwingUtilities.invokeLater(new Runnable() { #Override public void run() { JFrame frame = new JFrame("Polygon"); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.add(new Polygon()); frame.pack(); frame.setVisible(true); } }); } } Results in:
Drawing paint over a buffered image?
Hi guys I know this is a common one, but i've searched around quite a bit and can't seem to get my paint method to draw over the components in my JPanel. My paint method is linked to a button press. It prints out about 1500 data points and their assigned cluster (kmeans) package eye_pathscanner; import java.awt.Color; import java.awt.Graphics; import java.awt.Point; import java.util.ArrayList; import java.util.Random; import javax.swing.JFrame; import javax.swing.JOptionPane; import javax.swing.JPanel; public class ReplayData extends JPanel { // replay type can be parsed as argument into draw() to change paints behaviour private int ReplayType = 0; public ArrayList<DataPoint> points; //Initialise records public ReplayData() { points = new ArrayList<DataPoint>(); } public void ReplaceData() { points = new ArrayList<DataPoint>(); } public void PrintPoints() { } public void addPoint(DataPoint point) { points.add(point); } #Override public void paintComponent(Graphics g) { Color black = new Color(0, 0, 0); Random random = new Random(); final float luminance = 0.9f; if (ReplayType == 1) { super.paintComponent(g); for (int x = 0; x < kMeans.NUM_CLUSTERS; x++) { // Saturation ideal between 0.1 and 0.3 float saturation = (random.nextInt(2000) + 1000) / 10000f; float hue = random.nextFloat(); Color cluster_colour = Color.getHSBColor(hue, saturation, luminance); // Randomise the border colour saturation = (random.nextInt(2000) + 1000) / 10000f; hue = random.nextFloat(); Color cluster_colour_border = Color.getHSBColor(hue, saturation, luminance); double centroidx = kMeans.centroids.get(x).getmX(); double centroidy = kMeans.centroids.get(x).getmY(); for (int i = 0; i < kMeans.TOTAL_DATA; i++) if(kMeans.dataSet.get(i).cluster() == x){ // Set each child data point to a colour so you can see which cluster it belongs too g.setColor(cluster_colour); g.fillRect((int)TrackerData.getRecordNumber(i).getEyeX(),(int)TrackerData.getRecordNumber(i).getEyeY(), 3, 3); g.drawLine((int)kMeans.dataSet.get(i).X(),(int)kMeans.dataSet.get(i).Y(), (int)centroidx, (int)centroidy); //g.setColor(Color.black); g.setColor(cluster_colour_border); g.drawRect((int)TrackerData.getRecordNumber(i).getEyeX(),(int)TrackerData.getRecordNumber(i).getEyeY(), 3, 3); } g.setColor(black); g.fillOval((int)centroidx,(int)centroidy, 15, 15); } } } // 1 for K-means with different colour cluster groups // 2 for slow replay public void draw(int i) { ReplayType = i; repaint(); } } This code all works great for me, however I lose the image that was drawn beneath the paint after using it. I can maximize the page and the image shows up again but over the paint? (can anyone explain this behavior). JLabel picture_panel = new JLabel(); picture_panel.setBounds(70, 130, 640, 797); picture_panel.addMouseListener(this); BufferedImage img = null; try { img = ImageIO.read(new File("C:/Eyetracker_Images/Random.jpg")); // eventually C:\\ImageTest\\pic2.jpg ImageIcon icon = new ImageIcon(img); picture_panel.setIcon(icon); } catch (IOException e) { e.printStackTrace(); } Heres where my image is created, and it's called as shown below on one my buttons replayData.setBounds(0, 0, 802, 977); frame.getContentPane().add(replayData); replayData.draw(1); Any help would be much appreciated, Thanks.
This maybe an artifact of using setBounds(). Moreover, you appear to be using the default layout manager without invoking pack() on the enclosing container. As you already have a BufferedImage containing the rendered image, simply invoke drawImage() in your implementation of paintComponent(). Examples are seen here, here and here. Also consider overriding the getPreferredSize() method of ReplayData, as suggested here and here, to establish its dimensions.
Space ship simulator guidance computer targeting with concentric indicator squares
I'm working on a 3D space trading game with some people, and one of the things I've been assigned to do is to make a guidance computer 'tunnel' that the ship travels through, with the tunnel made of squares that the user flies through to their destination, increasing in number as the user gets closer to the destination. It's only necessary to render the squares for the points ahead of the ship, since that's all that's visible to the user. On their way to a destination, the ship's computer is supposed to put up squares on the HUD that represent fixed points in space between you and the destination, which are small in the distance and get larger as the points approach the craft. I've had a go at implementing this and can't seem to figure it out, mainly using logarithms (Math.log10(x) and such). I tried to get to get the ship position in 'logarithmic space' to help find out what index to start from when drawing the squares, but then the fact that I only have distance to the destination to work with confuses the matter, especially when you consider that the number of squares has to vary dynamically to make sure they stay fixed at the right locations in space (i.e., the squares are positioned at intervals of 200 or so before being transformed logarithmically). With regard to this, I had a working implementation with the ship between a start of 0.0d and end of 1.0d, although the implementation wasn't so nice. Anyway, the problem essentially boils down to a 1d nature. Any advice would be appreciated with this issue, including possible workarounds to achieve the same effect or solutions. (Also, there's a Youtube video showing this effect: http://www.youtube.com/watch?v=79F9Nj7GgfM&t=3m5s) Cheers, Chris Edit: rephrased the entire question. Edit: new testbed code: package st; import java.awt.BorderLayout; import java.awt.Canvas; import java.awt.Color; import java.awt.Dimension; import java.awt.Font; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.GraphicsDevice; import java.awt.GraphicsEnvironment; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.image.BufferStrategy; import java.text.DecimalFormat; import javax.swing.JFrame; import javax.swing.JPanel; import javax.swing.SwingUtilities; import javax.swing.Timer; public class StUI2 extends JFrame { public static final double DEG_TO_RAD = Math.PI / 180.0d; public static final DecimalFormat decimalFormat = new DecimalFormat("0.0000"); public static final Font MONO = new Font("Monospaced", Font.PLAIN, 10); public class StPanel extends Canvas { protected final Object imgLock = new Object(); protected int lastWidth = 1, lastHeight = 1; protected boolean first = true; protected Color bgColour = Color.DARK_GRAY, gridColour = Color.GRAY; double shipWrap = 700; double shipFrame = 100; double shipPos = 0; long lastUpdateTimeMS = -1; long currUpdateTimeMS = -1; public StPanel() { setFocusable(true); setMinimumSize(new Dimension(1, 1)); setAlwaysOnTop(true); } public void internalPaint(Graphics2D g) { synchronized (imgLock) { if (lastUpdateTimeMS < 0) { lastUpdateTimeMS = System.currentTimeMillis(); } currUpdateTimeMS = System.currentTimeMillis(); long diffMS = currUpdateTimeMS - lastUpdateTimeMS; g.setFont(MONO); shipPos += (60d * ((double)diffMS / 1000)); if (shipPos > shipWrap) { shipPos = 0d; } double shipPosPerc = shipPos / shipWrap; double distToDest = shipWrap - shipPos; double compression = 1000d / distToDest; g.setColor(bgColour); Dimension d = getSize(); g.fillRect(0, 0, (int)d.getWidth(), (int)d.getHeight()); //int amnt2 = (int)unlog10((1000d / distToDest)); g.setColor(Color.WHITE); g.drawString("shipPos: " + decimalFormat.format(shipPos), 10, 10); g.drawString("distToDest: " + decimalFormat.format(distToDest), 10, 20); g.drawString("shipWrap: " + decimalFormat.format(shipWrap), 150, 10); int offset = 40; g.setFont(MONO); double scalingFactor = 10d; double dist = 0; int curri = 0; int i = 0; do { curri = i; g.setColor(Color.GREEN); dist = distToDest - getSquareDistance(distToDest, scalingFactor, i); double sqh = getSquareHeight(dist, 100d * DEG_TO_RAD); g.drawLine(30 + (int)dist, (offset + 50) - (int)(sqh / 2d), 30 + (int)dist, (offset + 50) + (int)(sqh / 2d)); g.setColor(Color.LIGHT_GRAY); g.drawString("i: " + i + ", dist: " + decimalFormat.format(dist), 10, 120 + (i * 10)); i++; } while (dist < distToDest); g.drawLine(10, 122, 200, 122); g.drawString("last / i: " + curri + ", dist: " + decimalFormat.format(dist), 10, 122 + (i * 10)); g.setColor(Color.MAGENTA); g.fillOval(30 + (int)shipPos, offset + 50, 4, 4); lastUpdateTimeMS = currUpdateTimeMS; } } public double getSquareDistance(double initialDist, double scalingFactor, int num) { return Math.pow(scalingFactor, num) * num * initialDist; } public double getSquareHeight(double distance, double angle) { return distance / Math.tan(angle); } /* (non-Javadoc) * #see java.awt.Canvas#paint(java.awt.Graphics) */ #Override public void paint(Graphics g) { internalPaint((Graphics2D)g); } public void redraw() { synchronized (imgLock) { Dimension d = getSize(); if (d.width == 0) d.width = 1; if (d.height == 0) d.height = 1; if (first || d.getWidth() != lastWidth || d.getHeight() != lastHeight) { first = false; // remake buf GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); //create an object that represents the device that outputs to screen (video card). GraphicsDevice gd = ge.getDefaultScreenDevice(); gd.getDefaultConfiguration(); createBufferStrategy(2); lastWidth = (int)d.getWidth(); lastHeight = (int)d.getHeight(); } BufferStrategy strategy = getBufferStrategy(); Graphics2D g = (Graphics2D)strategy.getDrawGraphics(); internalPaint(g); g.dispose(); if (!strategy.contentsLost()) strategy.show(); } } } protected final StPanel canvas; protected Timer viewTimer = new Timer(1000 / 60, new ActionListener() { #Override public void actionPerformed(ActionEvent e) { canvas.redraw(); } }); { viewTimer.setRepeats(true); viewTimer.setCoalesce(true); } /** * Create the applet. */ public StUI2() { JPanel panel = new JPanel(new BorderLayout()); setContentPane(panel); panel.add(canvas = new StPanel(), BorderLayout.CENTER); setVisible(true); setDefaultCloseOperation(EXIT_ON_CLOSE); setSize(800, 300); setTitle("Targetting indicator test #2"); viewTimer.start(); } public static double unlog10(double x) { return Math.pow(10d, x); } public static void main(String[] args) { SwingUtilities.invokeLater(new Runnable() { #Override public void run() { StUI2 ui = new StUI2(); } }); } }
Assuming you want the squares to be equal height (when you reach them), you can calculate a scaling factor based on the distance to the destination (d) and the required height of the squares upon reaching them (h). From these two pieces of information you can calculate the inverse tangent (atan) of the angle (alpha) between the line connecting the ship to the destination (horizontal line in your image) and the line connecting the top of the squares with the destination (angled line in your image). EDIT: corrected formula Using the angle, you can calculate the height of the square (h') at any given distance from the destination: you know the distance to the destination (d') and the angle (alpha); The height of the square at distance d' is h'=r'*sin(alpha) -- sin(alpha)=cos(alpha)*tan(alpha) and r'=d'/cos(alpha) (the distance between the destination and the top of the square -- the "radius"). Or more easily: h'=d'*tan(alpha). Note: adopting the algorithm to varying height (when you reach them) squares is relatively simple: when calculating the angle, just assume a (phantom) square of fixed height and scale the squares relatively to that. If the height of the square at distance d' is calculated for you by your graphic library, all the better, you only need to figure out the distances to place the squares. What distances to place the squares from the destination? 1) If you want a varying number of squares shown (in front of the ship), but potentially infinite number of squares to consider (based on d), you can chose the distance of the closest square to the destination (d1) and calculate the distances of other squares by the formula s^k*k*d1, where s (scaling factor) is a number > 1 for the k'th square (counting from the destination). You can stop the algorithm when the result is larger than d. Note that if d is sufficiently large, the squares closest to the distance will block the destination (there are many of them and their heights are small due to the low angle). In this case you can introduce a minimal distance (possibly based on d), below which you do not display the squares -- you will have to experiment with the exact values to see what looks right/acceptable. 2) If you want a fixed amount of squares (sn) showing always, regardless of d, you can calculate the distances of the squares from the destination by the formula d*s^k, where s is a number < 1, k is the index of the square (counting from the ship). The consideration about small squares probably don't apply here unless sn is high. To fix the updated code, change the relavant part to: double dist = 0; double d1 = 10; int curri = 0; int i = 1; int maxSquareHeight = 40; double angle = Math.atan(maxSquareHeight/distToDest); while (true) { curri = i; g.setColor(Color.GREEN); dist = getSquareDistance(d1, scalingFactor, i); if (dist > distToDest) { break; } double sqh = getSquareHeight(dist, angle); g.drawLine(30 + (int)(shipWrap - dist), offset+50-(int)(sqh / 2d), 30 + (int)(shipWrap - dist), offset+50+(int)(sqh / 2d)); g.setColor(Color.LIGHT_GRAY); i++; } public double getSquareHeight(double distance, double angle) { return distance * Math.tan(angle); } You should also reduce scalingFactor to the magnitude of ~1.5. EDIT: If you replace the formula s^k*k*d1 with s^(k-1)*k*d1, then the first square will be exactly at distance d1. EDIT: fixed square height calculating formula EDIT: updated code
Java Graphics.fillPolygon: How to also render right and bottom edges?
When drawing polygons, Java2D leaves off the right and bottom edges. I understand why this is done. However, I would like to draw something that includes those edges. One thing that occurred to me was to follow fillPolygon with drawPolygon with the same coordinates, but this appears to leave a gap. (See the little triangular image at the bottom.) There are two possibilities, but I can't tell which. To enable antialiasing, I'm doing this: renderHints = new RenderingHints(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); renderHints.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); g2d.setRenderingHints(renderHints); One possibility is that the antialiasing is not being done on the alpha channel, so the gap is caused by overdraw. In that case, if the alpha channel were what was being antialiased, the edges would abut properly. The other possibility is that there is just a gap here. How can I fix this? Also, I'm not sure, but it appears that the polygon outline may actually be TOO BIG. That is, it may be going further out than the right and bottom edges that I want to include. Thanks. -- UPDATE -- Based on a very nice suggestion by Hovercraft Full of Eels, I have made a compilable example: import java.awt.Color; import java.awt.Graphics2D; import java.awt.RenderingHints; import java.awt.image.BufferedImage; import javax.swing.ImageIcon; import javax.swing.JLabel; import javax.swing.JOptionPane; public class polygon { private static final int WIDTH = 20; public static void main(String[] args) { BufferedImage img = new BufferedImage(WIDTH, WIDTH, BufferedImage.TYPE_INT_ARGB); Graphics2D g2 = img.createGraphics(); int[] xPoints = {WIDTH / 3, (2*WIDTH) / 3, WIDTH / 3}; int[] yPoints = {0, WIDTH / 2, WIDTH}; g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g2.setColor(Color.green); g2.drawLine(0, WIDTH-1, WIDTH, WIDTH-1); g2.drawLine(0, 0, WIDTH, 0); g2.drawLine(WIDTH/3, 0, WIDTH/3, WIDTH); g2.drawLine((2*WIDTH/3), 0, (2*WIDTH/3), WIDTH); g2.setColor(Color.black); g2.drawPolygon(xPoints, yPoints, xPoints.length); g2.setColor(Color.black); g2.fillPolygon(xPoints, yPoints, xPoints.length); g2.dispose(); ImageIcon icon = new ImageIcon(img); JLabel label = new JLabel(icon); JOptionPane.showMessageDialog(null, label); } } If you leave the filled polygon red, you get the image below (zoomed by 500%), which shows that the polygon does not extend all the way to the right edge. That is, the vertical green line is corresponds to x=(2*WIDTH)/2, and although the red polygon includes that coordinate, it does not paint any pixels there. To see the gap problem, I changed red in the program to black. In this image, you can see a subtle gap on the lower right side, where the outline drawn by drawPolygon does not quite meet up with what was drawn with fillPolygon.
Show us your code for your drawing in a simple compilable runnable program. For instance when I try to imitate your image and used RenderingHints, it seemed to produce an appropriate sized image with complete right/bottom edges: import java.awt.Color; import java.awt.Graphics2D; import java.awt.RenderingHints; import java.awt.image.BufferedImage; import javax.swing.BorderFactory; import javax.swing.ImageIcon; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPanel; public class Foo002 { private static final int WIDTH = 20; public static void main(String[] args) { BufferedImage img = new BufferedImage(WIDTH, WIDTH, BufferedImage.TYPE_INT_ARGB); Graphics2D g2 = img.createGraphics(); int[] xPoints = { WIDTH / 3, (2 * WIDTH) / 3, WIDTH / 3 }; int[] yPoints = { 0, WIDTH / 2, WIDTH }; g2.setColor(Color.black); g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); g2.fillPolygon(xPoints, yPoints, xPoints.length); g2.dispose(); ImageIcon icon = new ImageIcon(img); JLabel label = new JLabel(icon); label.setBorder(BorderFactory.createLineBorder(Color.black)); JPanel panel = new JPanel(); panel.add(label); JOptionPane.showMessageDialog(null, panel); } } If you can show us a similar program that reproduces your problem, then we can give you better help.
I like the convenience of ImageIcon, shown by #HFOE, but this variation may make it a little easier to see what's happening. From the Graphics API, Operations that draw the outline of a figure operate by traversing an infinitely thin path between pixels with a pixel-sized pen that hangs down and to the right of the anchor point on the path. Operations that fill a figure operate by filling the interior of that infinitely thin path. In contrast, Graphics2D must follow more complex rules for antialiasing, which allow it to "draw outside the lines." import java.awt.Color; import java.awt.Dimension; import java.awt.EventQueue; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.GridLayout; import java.awt.RenderingHints; import java.awt.image.BufferedImage; import javax.swing.JFrame; import javax.swing.JPanel; /** #see http://stackoverflow.com/questions/7701097 */ public class PixelView extends JPanel { private static final int SIZE = 20; private static final int SCALE = 16; private BufferedImage img; public PixelView(Color fill) { this.setBackground(Color.white); this.setPreferredSize(new Dimension(SCALE * SIZE, SCALE * SIZE)); img = new BufferedImage(SIZE, SIZE, BufferedImage.TYPE_INT_ARGB); Graphics2D g2 = img.createGraphics(); int[] xPoints = {SIZE / 3, (2 * SIZE) / 3, SIZE / 3}; int[] yPoints = {0, SIZE / 2, SIZE}; g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g2.setColor(Color.green); g2.drawLine(0, SIZE - 1, SIZE, SIZE - 1); g2.drawLine(0, 0, SIZE, 0); g2.drawLine(SIZE / 3, 0, SIZE / 3, SIZE); g2.drawLine((2 * SIZE / 3), 0, (2 * SIZE / 3), SIZE); g2.setColor(Color.black); g2.drawPolygon(xPoints, yPoints, xPoints.length); g2.setColor(fill); g2.fillPolygon(xPoints, yPoints, xPoints.length); g2.dispose(); } #Override protected void paintComponent(Graphics g) { super.paintComponent(g); g.drawImage(img, 0, 0, getWidth(), getHeight(), null); } private static void display() { JFrame f = new JFrame("PixelView"); f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); f.setLayout(new GridLayout(1, 0)); f.add(new PixelView(Color.black)); f.add(new PixelView(Color.red)); f.pack(); f.setLocationRelativeTo(null); f.setVisible(true); } public static void main(String[] args) { EventQueue.invokeLater(new Runnable() { #Override public void run() { display(); } }); } }
Sometimes "the graphics pen hangs down and to the right from the path it traverses", and sometimes it doesn't. I don't have any clear idea how to predict when it will or won't, but I have observed that RenderingHints.VALUE_STROKE_PURE can sometimes be used to alter the behavior, by trial and error. In particular, I found that if you turn on STROKE_PURE during your drawPolygon() calls in your program, it will make them match up with your fillPolygon() calls, as you desire. I did a little study showing the effect of the STROKE_CONTROL hint, which is one of: STROKE_NORMALIZE (the default, on my system) STROKE_PURE on the following calls: drawLine() drawPolygon() fillPolygon() in both antialiasing modes: ANTIALIASING_OFF ANTIALIASING_ON And there is one more annoying dimension that apparently matters as well: rendered directly to a JComponent on the screen rendered to a BufferedImage. Here are the results when rendering directly to a visible JComponent: And here are the results when rendering into a BufferedImage: (Notice the two cases in which the two pictures differ, i.e. in which direct rendering differs from BufferedImage rendering: ANTIALIAS_OFF/STROKE_NORMALIZE/fillPolygon and ANTIALIAS_OFF/STROKE_PURE/drawPolygon.) Overall, there doesn't seem to be much rhyme or reason to the whole thing. But we can make the following specific observations based on the above pictures: Observation #1: If you want your antialiased drawPolygon()s and antialiased fillPolygon()s to match up well (the original question), then turn on STROKE_PURE during the antialiased drawPolygon() calls. (It doesn't matter whether it's on during the antialiased fillPolygon() calls.) Observation #2: If you want your antialiased fillPolygon()s and non-antialiased fillPolygon()s to match up (because, say, your app allows the user to switch antialiasing on and off, and you don't want the picture to jump northwest and southeast each time they do that), then turn on STROKE_PURE during the non-antialiased fillPolygon() calls. Here is the program I used to generate the pictures above. My results are from compiling and running it it with opensdk11 on linux; I'd be interested to know if anyone gets any different results on different platforms. /** Study the effect of STROKE_PURE. */ import java.awt.*; import java.awt.event.*; import java.awt.image.*; import javax.swing.*; #SuppressWarnings("serial") public final class AntiAliasingStudy { // These can be fiddled with. final static int patchWidth = 24; // keep this a multiple of 4 for sanity final static int patchHeight = 20; // keep this a multiple of 4 for sanity final static int borderThickness = 4; final static int mag = 6; // derived quantities final static int totalWidth = 5*borderThickness + 4*patchWidth; final static int totalHeight = 4*borderThickness + 3*patchHeight; private static void drawLittleStudy(Graphics2D g2d, int x00, int y00, int patchWidth, int patchHeight, int borderThickness, int totalWidth, int totalHeight) { g2d.setColor(new java.awt.Color(240,240,240)); g2d.fillRect(x00,y00,totalWidth, totalHeight); for (int row = 0; row < 3; ++row) { for (int col = 0; col < 4; ++col) { int x0 = x00 + borderThickness + col*(patchWidth+borderThickness); int y0 = y00 + borderThickness + row*(patchHeight+borderThickness); int x1 = x0 + patchWidth; int y1 = y0 + patchHeight; g2d.setColor(java.awt.Color.WHITE); g2d.fillRect(x0, y0, patchWidth, patchHeight); boolean antialias = (col >= 2); boolean pure = (col % 2 == 1); g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, antialias ? RenderingHints.VALUE_ANTIALIAS_ON : RenderingHints.VALUE_ANTIALIAS_OFF); g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, pure ? RenderingHints.VALUE_STROKE_PURE : RenderingHints.VALUE_STROKE_NORMALIZE); g2d.setColor(java.awt.Color.RED); if (row == 0) { // lines (drawLine) // diagonals g2d.drawLine(x0,y1, x1,y0); g2d.drawLine(x0,y0, x1,y1); // orthogonals g2d.drawLine((x0+patchWidth/4),y0, (x0+patchWidth*3/4),y0); g2d.drawLine((x0+patchWidth/4),y1, (x0+patchWidth*3/4),y1); g2d.drawLine(x0,(y0+patchHeight/4), x0,(y0+patchHeight*3/4)); g2d.drawLine(x1,(y0+patchHeight/4), x1,(y0+patchHeight*3/4)); } else if (row == 1) { // outlines (drawPolygon) // A stopsign g2d.drawPolygon(new int[] {x0+patchWidth/2-2, x0, x0, x0+patchWidth/2-2, x0+patchWidth/2+2, x1, x1, x0+patchWidth/2+2}, new int[] {y0, y0+patchHeight/2-2, y0+patchHeight/2+2, y1, y1, y0+patchHeight/2+2, y0+patchHeight/2-2, y0}, 8); } else if (row == 2) { // fill (fillPolygon) // A stopsign g2d.fillPolygon(new int[] {x0+patchWidth/2-2, x0, x0, x0+patchWidth/2-2, x0+patchWidth/2+2, x1, x1, x0+patchWidth/2+2}, new int[] {y0, y0+patchHeight/2-2, y0+patchHeight/2+2, y1, y1, y0+patchHeight/2+2, y0+patchHeight/2-2, y0}, 8); } } } } // drawLittleStudy // Show a study, previously created by drawLittleStudy(), magnified and annotated. private static void showMagnifiedAndAnnotatedStudy(Graphics g, BufferedImage studyImage, int x00, int y00, int patchWidth, int patchHeight, int borderThickness, int totalWidth, int totalHeight, int mag, ImageObserver imageObserver) { // Magnify the image g.drawImage(studyImage, /*dst*/ x00,y00,x00+totalWidth*mag,y00+totalHeight*mag, /*src*/ 0,0,totalWidth,totalHeight, imageObserver); // Draw annotations on each picture in black, // in the highest quality non-biased mode // (now that we know what that is!) g.setColor(java.awt.Color.BLACK); ((Graphics2D)g).setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); ((Graphics2D)g).setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE); for (int row = 0; row < 3; ++row) { for (int col = 0; col < 4; ++col) { int x0 = borderThickness + col*(patchWidth+borderThickness); int y0 = borderThickness + row*(patchHeight+borderThickness); int x1 = x0 + patchWidth; int y1 = y0 + patchHeight; if (false) { g.drawLine(x00+x0*mag,y00+y0*mag, x00+x1*mag,y00+y0*mag); g.drawLine(x00+x1*mag,y00+y0*mag, x00+x1*mag,y00+y1*mag); g.drawLine(x00+x1*mag,y00+y1*mag, x00+x0*mag,y00+y1*mag); g.drawLine(x00+x0*mag,y00+y1*mag, x00+x0*mag,y00+y0*mag); } if (row == 0) { // diagonals g.drawLine(x00+x0*mag,y00+y1*mag, x00+x1*mag,y00+y0*mag); g.drawLine(x00+x0*mag,y00+y0*mag, x00+x1*mag,y00+y1*mag); // orthogonals g.drawLine(x00+(x0+patchWidth/4)*mag,y00+y0*mag, x00+(x0+patchWidth*3/4)*mag,y00+y0*mag); g.drawLine(x00+(x0+patchWidth/4)*mag,y00+y1*mag, x00+(x0+patchWidth*3/4)*mag,y00+y1*mag); g.drawLine(x00+x0*mag,y00+(y0+patchHeight/4)*mag, x00+x0*mag,y00+(y0+patchHeight*3/4)*mag); g.drawLine(x00+x1*mag,y00+(y0+patchHeight/4)*mag, x00+x1*mag,y00+(y0+patchHeight*3/4)*mag); } else { // row 1 or 2 // A stopsign g.drawPolygon(new int[] {x00+(x0+patchWidth/2-2)*mag, x00+x0*mag, x00+x0*mag, x00+(x0+patchWidth/2-2)*mag, x00+(x0+patchWidth/2+2)*mag, x00+x1*mag, x00+x1*mag, x00+(x0+patchWidth/2+2)*mag}, new int[] {y00+y0*mag, y00+(y0+patchHeight/2-2)*mag, y00+(y0+patchHeight/2+2)*mag, y00+y1*mag, y00+y1*mag, y00+(y0+patchHeight/2+2)*mag, y00+(y0+patchHeight/2-2)*mag, y00+y0*mag}, 8); } } } FontMetrics fm = g.getFontMetrics(); { String[][] texts = { {"ANTIALIAS_OFF", "STROKE_NORMALIZE"}, {"ANTIALIAS_OFF", "STROKE_PURE"}, {"ANTIALIAS_ON", "STROKE_NORMALIZE"}, {"ANTIALIAS_ON", "STROKE_PURE"}, }; for (int col = 0; col < 4; ++col) { int xCenter = borderThickness*mag + col*(patchWidth+borderThickness)*mag + patchWidth*mag/2; { int x = x00 + xCenter - fm.stringWidth(texts[col][0])/2; int y = y00 + 3*(patchHeight+borderThickness)*mag + fm.getAscent(); g.drawString(texts[col][0], x,y); x = xCenter - fm.stringWidth(texts[col][1])/2; y += fm.getHeight(); g.drawString(texts[col][1], x,y); } } } { String[] texts = { "drawLine", "drawPolygon", "fillPolygon", }; for (int row = 0; row < 3; ++row) { int yCenter = y00 + borderThickness*mag + row*(patchHeight+borderThickness)*mag + patchHeight*mag/2; int x = x00 + 4*(patchWidth+borderThickness)*mag + 10; g.drawString(texts[row], x,yCenter); } } } // showMagnifiedAndAnnotatedStudy private static Dimension figureOutPreferredSize(FontMetrics fm) { int preferredWidth = (totalWidth-borderThickness)*mag + 10 + fm.stringWidth("drawPolygon") + 9; int preferredHeight = fm.getHeight() + totalHeight + (totalHeight-borderThickness)*mag + 2*fm.getHeight() + 2; return new Dimension(preferredWidth, preferredHeight); } private static class IndirectExaminationView extends JComponent { public IndirectExaminationView() { setFont(new Font("Times", Font.PLAIN, 12)); setPreferredSize(figureOutPreferredSize(getFontMetrics(getFont()))); } #Override public void paintComponent(Graphics g) { FontMetrics fm = g.getFontMetrics(); g.setColor(java.awt.Color.BLACK); g.drawString("through BufferedImage:", 0,fm.getAscent()); // The following seem equivalent java.awt.image.BufferedImage studyImage = new java.awt.image.BufferedImage(totalWidth, totalHeight, java.awt.image.BufferedImage.TYPE_INT_ARGB); //java.awt.image.BufferedImage studyImage = (BufferedImage)this.createImage(totalWidth, totalHeight); drawLittleStudy(studyImage.createGraphics(), 0,0, patchWidth, patchHeight, borderThickness, totalWidth, totalHeight); Graphics2D studyImageGraphics2D = studyImage.createGraphics(); g.drawImage(studyImage, /*dst*/ 0,fm.getHeight(),totalWidth,fm.getHeight()+totalHeight, /*src*/ 0,0,totalWidth,totalHeight, this); showMagnifiedAndAnnotatedStudy(g, studyImage, 0,fm.getHeight()+totalHeight, patchWidth, patchHeight, borderThickness, totalWidth, totalHeight, mag, this); } } // DirectExaminationView private static class DirectExaminationView extends JComponent { public DirectExaminationView() { setFont(new Font("Times", Font.PLAIN, 12)); setPreferredSize(figureOutPreferredSize(getFontMetrics(getFont()))); } private BufferedImage imgFromTheRobot = null; #Override public void paintComponent(Graphics g) { final FontMetrics fm = g.getFontMetrics(); g.setColor(java.awt.Color.BLACK); g.drawString("direct to JComponent:", 0,fm.getAscent()); drawLittleStudy((Graphics2D)g, 0,fm.getHeight(), patchWidth, patchHeight, borderThickness, totalWidth, totalHeight); if (imgFromTheRobot != null) { System.out.println(" drawing image from robot"); showMagnifiedAndAnnotatedStudy(g, imgFromTheRobot, 0, fm.getHeight()+totalHeight, patchWidth, patchHeight, borderThickness, totalWidth, totalHeight, mag, this); imgFromTheRobot = null; } else { System.out.println(" scheduling a robot"); g.drawString("*** SCREEN CAPTURE PENDING ***", 0, fm.getHeight()+totalHeight+fm.getHeight()+fm.getHeight()); // Most reliable way to do it seems to be to put it on a timer after a delay. Timer timer = new Timer(1000/2, new ActionListener() { #Override public void actionPerformed(ActionEvent ae) { System.out.println(" in timer callback"); Robot robot; try { robot = new Robot(); } catch (AWTException e) { System.err.println("caught AWTException: "+e); throw new Error(e); } Point myTopLeftOnScreen = getLocationOnScreen(); Rectangle rect = new Rectangle( myTopLeftOnScreen.x, myTopLeftOnScreen.y + fm.getHeight(), totalWidth,totalHeight); BufferedImage img = robot.createScreenCapture(rect); imgFromTheRobot = img; repaint(); System.out.println(" out timer callback"); } }); timer.setRepeats(false); timer.start(); } } } // DirectExaminationView public static void main(String[] args) { javax.swing.SwingUtilities.invokeLater(new Runnable() { #Override public void run() { final JFrame directFrame = new JFrame("direct to JComponent") {{ getContentPane().add(new DirectExaminationView()); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); pack(); setLocation(0,0); setVisible(true); }}; new JFrame("through BufferedImage") {{ getContentPane().add(new IndirectExaminationView()); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); pack(); setLocation(directFrame.getWidth(),0); setVisible(true); }}; } }); } } // class AntiAliasingStudy