Drawing paint over a buffered image? - java

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.

Related

How to draw a triangle with border with Java Graphics

I'm trying to draw a triangle with a border using the Graphics.drawPolygon() method
The triangle is properly drawn, but how can I calculate the 3 points of the border?
I already did it with a circle, but I can't seem to find a solution for triangle.
A requirement of the instructor as that it cannot use Graphics2D.
My code:
if (xPoints != null && yPoints != null) {
int[] nXPoints = new int[] { xPoints[0] - borderThickness, xPoints[1] - borderThickness,
xPoints[2] - borderThickness };
int[] nYPoints = new int[] { yPoints[0] - borderThickness, yPoints[1] - borderThickness,
yPoints[2] - borderThickness };
g.setColor(borderColor);
g.fillPolygon(nXPoints, nYPoints, 3);
g.setColor(fillColor);
g.fillPolygon(xPoints, yPoints, 3);
}
Edit:
Expected result
Use the Graphics methods drawPolygon() to render the outline and fillPolygon() to fill its interior; both have the required signature, as shown here.
Because "operations that draw the outline of a figure operate by traversing an infinitely thin path between pixels with a pixel-sized pen," cast the graphics context to Graphics2D so that you can use draw() and fill() on the corresponding Shape. This will allow you to specify the outline using setStroke(), illustrated here.
I need it to have a custom thickness…I also don't want to use Graphics2D.
Custom thickness is not supported in the Graphics API. As suggested here, the actual graphics context received by paintComponent() is an instance of Graphics2D, which does support custom stroke geometry.
The things is teacher haven't taught me Graphics2D, so I'm not supposed to use it.
Then simply paint the larger triangle and then the smaller. If this isn't working, then you have an error in you calculation of the larger triangle, and you should edit your question to include a complete example.
I'm looking for a way to do it without Graphics2D…a guy has interpreted the question properly in this comment.
As #Marco13 observes, you need a triangle that is larger than the original one by the borderThickness. You can use AffineTransform to do the scaling. While Graphics can't fill() an arbitrary Shape, such as one created by AffineTransform, it can clip the rendering as required. In the example below, a unit triangle is translated and scaled to the center of an N x N panel; a copy is enlarged by delta. Note how rendering is clipped first to the larger background figure and then to the smaller foreground figure.
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Polygon;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import javax.swing.JFrame;
import javax.swing.JPanel;
/**
* #see https://stackoverflow.com/a/39812618/230513
*/
public class GraphicsBorder {
private static class GraphicsPanel extends JPanel {
private static final int N = 256;
private static final Color FILL = new Color(0x990099);
private static final Color BORDER = new Color(0x66FFB2);
private final Shape fore;
private final Shape back;
public GraphicsPanel(Polygon polygon, int delta) {
AffineTransform a1 = new AffineTransform();
a1.translate(N / 2, N / 2);
a1.scale(N / 3, N / 3);
fore = a1.createTransformedShape(polygon);
AffineTransform a2 = new AffineTransform();
a2.translate(N / 2, N / 2 - delta / 3);
a2.scale(N / 3 + delta, N / 3 + delta);
back = a2.createTransformedShape(polygon);
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
g.setColor(BORDER);
g.setClip(back);
g.fillRect(0, 0, N, N);
g.setColor(FILL);
g.setClip(fore);
g.fillRect(0, 0, N, N);
}
#Override
public Dimension getPreferredSize() {
return new Dimension(N, N);
}
}
private void display() {
JFrame f = new JFrame("GraphicsBorder");
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
Polygon p = new Polygon();
p.addPoint(0, -1);
p.addPoint(1, 1);
p.addPoint(-1, 1);
f.add(new GraphicsPanel(p, 16));
f.pack();
f.setLocationRelativeTo(null);
f.setVisible(true);
}
public static void main(String[] args) {
EventQueue.invokeLater(new GraphicsBorder()::display);
}
}

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:

Java Challenge on A Gradient Color Canvas

This is probably an elementary question. However, I have completed reading the 9th Chapter of Java Programming for the Absolute Beginner and have approached the Challenges section. I cannot quite get the progam to show a gradient from dark to light.
The question asks:
"Create a Canvas that paints a gradient that's dark on one side and slowly gets lighter
as it moves to the other side."
I have looked at the Java Challenge on Creating a Canvas page (which I asked about before) and got a better understanding of creating a color gradient canvas, however, I am still having great difficulty.
I think that my main problem is how to get the gray colors in between as the program shows a completely black square or when run with just the first for loop, a completely white square. I THINK THAT MY FOR LOOPS ARE PROBLEMATIC AND DO NOT CORRECTLY IDENTIFY THE VARIABLES.
An answer to this question can potentially aid many new Java programmers in understanding Graphics and Canvas.
I do not know JFrame, Swing, Points, JPanels, BufferedImage, or GradientPaint.
Thank you very much for your time and cooperation reagrding this matter.
HERE IS THE CODE:
import java.awt.*;
public class RandomColorSquare extends GUIFrame {
Canvas slight;
public final static int MIN = 0,
MAX = 225;
public RandomColorSquare(int r, int g, int b) {
super("Random Color Square");
r = r >= MIN && r <= MAX ? r : MIN;
g = g >= MIN && g <= MAX ? g : MIN;
b = r >= MIN && b <= MAX ? b : MIN;
slight = new Canvas();
slight.setBackground(new Color(r,g, b));
slight.setSize(200, 150);
add(slight, BorderLayout.CENTER);
for(r=0; r<225; r++) {
Color c = slight.getBackground().brighter();
slight.setBackground(c);
}
for (g=0; g<225; g++) {
Color d = slight.getBackground().darker();
slight.setBackground(d);
}
for (b=0; b<225; b++) {
Color e = slight.getBackground().darker();
slight.setBackground(e);
}
pack();
setVisible(true);
}
public static void main(String args[]) {
if (args.length != 3) {
new RandomColorSquare(0, 0, 0);
}
else {
new RandomColorSquare(Integer.parseInt(args[0]), Integer.parseInt(args[1]),
Integer.parseInt(args[2]));
}
}
}
First of all, as I mentioned in the comment, when you setBackground you are changing the background of the Canvas, not adding to it. So whatever you set it to last is what you see. In your case, that is the darkest form of blue, which is just black. If you comment out the last two loops, you get a white background (the brightest red).
However. Using only what you know this is the best I can come up with. It involves creating your own custom Canvas, which I called CustomCanvas. In there, you can override the paint method and keep drawing progressively lighter filled rectangles across the screen. Here's a small sample to help you understand what I mean:
class CustomeCanvas extends Canvas{
public void paint(Graphics g){
Color background = new Color(30,30,120); //Set this accordingly
for(int i=0;i<getWidth();i++){
g.setColor(background);
background = getBrighter(background);
g.fillRect(i,0, 1, getHeight());
}
}
private Color getBrighter(Color c) {
int r = c.getRed();
int g = c.getGreen();
int b = c.getBlue();
if(r< MAX) r+=1;
if(g< MAX) g+=1;
if(b< MAX) b+=1;
return new Color(r,g,b);
}
}
Which produces this background:
I still recommend reading about GradientPaint which makes this process a lot easier and nicer.
You can override paint() in your Canvas in a manner similar to how this example does for Panel. In your implementation, use drawImage() to render a BufferedImage in which you've used setRGB() to construct your gradient.
Addendum: The example below illustrates the approach by creating a random image. You can create a gradient similarly.
import java.awt.Canvas;
import java.awt.Dimension;
import java.awt.Frame;
import java.awt.Graphics;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.Random;
/** #see https://stackoverflow.com/a/14096121/230513 */
public class AWTImage {
public static void main(String[] args) throws IOException {
Frame frame = new Frame();
frame.add(new ImageCanvas());
frame.addWindowListener(new WindowAdapter() {
#Override
public void windowClosing(WindowEvent e) {
System.exit(0);
}
});
frame.pack();
frame.setVisible(true);
}
private static class ImageCanvas extends Canvas {
private static final Random r = new Random();
private BufferedImage image;
public ImageCanvas() {
image = new BufferedImage(256, 256, BufferedImage.TYPE_INT_ARGB);
for (int row = 0; row < image.getHeight(); row++) {
for (int col = 0; col < image.getWidth(); col++) {
image.setRGB(col, row, r.nextInt());
}
}
}
#Override
public Dimension getPreferredSize() {
return new Dimension(image.getWidth(), image.getHeight());
}
#Override
public void paint(Graphics g) {
g.drawImage(image, 0, 0, this);
}
}
}

AffineTransform.rotate() - how do I xlate, rotate, and scale at the same time?

I have the following code which does (the first part of) what I want drawing a chessboard with some pieces on it.
Image pieceImage = getImage(currentPiece);
int pieceHeight = pieceImage.getHeight(null);
double scale = (double)side/(double)pieceHeight;
AffineTransform transform = new AffineTransform();
transform.setToTranslation(xPos, yPos);
transform.scale(scale, scale);
realGraphics.drawImage(pieceImage, transform, this);
that is, it gets a chess piece's image and the image's height, it translates the drawing of that image to the square the piece is on and scales the image to the size of the square.
Llet's say I want to rotate the black pieces 180 degrees. Somewhere I expect to have something like:
transform.rotate(Math.toRadians(180) /* ?, ? */);
But I can't figure out what to put in as X and Y. If I put nothing, the image is nicely rotated around the 0,0 point of its chessboard square, putting the piece upside down in the square to the northeast of where it is supposed to be. I've guessed at various other combinations of x,y, with no luck yet.
I am already using translation to put the piece in the right square, the rotation transform wants another x,y around which to rotate things, but I don't know how to tell the transform to rotate the piece around one x,y and write the image to a different x,y. Can someone help me with the rotation parameters, or point me to something that explains how these things work? I've found examples of things that don't explain how they work, and so far I haven't figured out how to alter them to my situation...
Major edit: addition of working code. Sorry, I don't know how to post images, please substitute your own.
When I run the following I get a 2x2 chess board with a rook at the top left and a knight at the bottom right.
If I go into SmallChessboardComponent and take the comment delims off the first rotation transform statement, I get the rook in its original place upside down and the knight does not appear. If I instead take the comment delims off the second transform statement, neither piece appears at all.
I am looking for a way to turn the pieces upside down on the square on which they would appear anyway. I want to draw each piece onto the board; I don't want code that flips the board.
main program:
package main;
import java.awt.BorderLayout;
import javax.swing.JFrame;
import directredraw.SmallChessboardComponent;
public class SmallChessboardMain
{
private static void dbg (String message) { System.out.println(message); }
public static void main(String[] args)
{
//Create the top-level container and add contents to it.
final JFrame frame = new JFrame("Small Chessboard");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
// create the chessboard itself and set it in the component
SmallChessboard chessboard = new SmallChessboard();
// create the GUI component that will contain the chessboard
SmallChessboardComponent chessboardComponent = new SmallChessboardComponent();
chessboardComponent.setBoard (chessboard);
frame.getContentPane().add(chessboardComponent, BorderLayout.CENTER);
// pack and display all this
frame.pack();
frame.setVisible(true);
}
}
chessboard class:
package main;
public class SmallChessboard
{
Piece [][] squares = new Piece[2][2];
public SmallChessboard()
{
squares[0][0] = new Piece(Piece.WHITECOLOR, Piece.ROOK);
squares[1][1] = new Piece(Piece.WHITECOLOR, Piece.KNIGHT);
}
/**
* get the piece at the given rank and file; null if
* no piece exists there.
*/
public Piece getPiece(int rank, int file)
{
if (0 > rank || rank > 2 || 0 > file || file > 2) { return null; }
else { return squares[rank][file]; }
}
}
chessboard component class:
package directredraw;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Rectangle;
import java.awt.geom.AffineTransform;
import javax.swing.JPanel;
import main.Piece;
import main.PieceImages;
import main.SmallChessboard;
public class SmallChessboardComponent extends JPanel
{
private static final long serialVersionUID = 1L;
Color whiteSquareColor = Color.yellow;
Color blackSquareColor = Color.blue;
private static void dbg (String msg) { System.out.println(msg); }
private SmallChessboard chessboard = null;
// currently playing with rotating images; this affine transform
// should help
AffineTransform rotationTransform = null;
private final int DEFAULT_PREFERRED_SIDE = 400;
int wholeSide = DEFAULT_PREFERRED_SIDE;
int side = DEFAULT_PREFERRED_SIDE / 8;
public void setBoard (SmallChessboard givenBoard)
{ chessboard = givenBoard;
}
/**
* set either or both colors for this chessboard; if either of
* the arguments are null, they do not change the existing color
* setting.
*/
public void setColors (Color darkSquare, Color lightSquare)
{
if (darkSquare != null) { blackSquareColor = darkSquare; }
if (lightSquare != null) { whiteSquareColor = lightSquare; }
}
/**
* return the preferred size for this component.s
*/
public Dimension getPreferredSize()
{ return new Dimension(wholeSide, wholeSide);
}
/*
* return the image object for the given piece
*/
private Image getImage(Piece piece)
{ return PieceImages.getPieceImage(this, piece);
}
public void paintComponent (Graphics graphics)
{
Graphics2D realGraphics = (Graphics2D) graphics;
// the image container might have been stretched.
// calculate the largest square held by the current container,
// and then 1/2 of that size for an individual square.
int wholeWidth = this.getWidth();
int wholeHeight = this.getHeight();
wholeSide = (wholeWidth / 2) * 2;
if (wholeHeight < wholeWidth) { wholeSide = (wholeHeight / 2) * 2; }
side = wholeSide / 2;
Rectangle clip = realGraphics.getClipBounds();
boolean firstColumnWhite = false;
// for each file on the board:
// set whether top square is white
// set background color according to white/black square
//
for (int fileIndex=0; fileIndex<8; fileIndex++)
{ boolean currentColorWhite = firstColumnWhite;
firstColumnWhite = !firstColumnWhite;
// draw the board and all the pieces
int rankIndex = 2;
for (rankIndex=2; rankIndex>=0; rankIndex--)
{
currentColorWhite = !currentColorWhite;
// x and y position of the top left corner of the square we're drawing,
// and rect becomes the dimensions and position of the square itself.
int xPos = fileIndex * side;
int yPos = rankIndex * side;
Rectangle rect = new Rectangle(xPos, yPos, side, side);
// if this square intersects the clipping rectangle we're drawing,
// then we'll draw the square and the piece on the square.
if (rect.intersects(clip))
{
// this puts down the correct color of square
if (currentColorWhite) { realGraphics.setColor(whiteSquareColor); }
else { realGraphics.setColor(blackSquareColor); }
realGraphics.fillRect(xPos, yPos, side, side);
// if there is a piece on this square and it isn't selected at the
// moment, then draw it.
Piece currentPiece = chessboard.getPiece(rankIndex, fileIndex);
if (currentPiece != null)
{
Image pieceImage = getImage(currentPiece);
int pieceHeight = pieceImage.getHeight(null);
double scalePiece = (double)side/(double)pieceHeight;
AffineTransform transform = new AffineTransform();
// transform.setToRotation(Math.toRadians(180));
transform.setToRotation(Math.toRadians(180), side/2, side/2);
transform.scale(scalePiece, scalePiece);
transform.translate(xPos/scalePiece, yPos/scalePiece);
// if (currentPiece.isBlack())
// {
// transform.translate(xPos + (side+2), yPos + (side+2));
// transform.rotate(Math.toRadians(180) /*, ,*/ );
// }
// else
// {
// transform.translate(xPos, yPos);
// }
realGraphics.drawImage(pieceImage, transform, this);
}
}
}
}
}
}
Piece.java
package main;
public class Piece
{
// piece types; the sum of the piece type and the
// color gives a number unique to both type and color,
// which is used for things like image indices.
public static final int PAWN = 0;
public static final int KNIGHT = 1;
public static final int BISHOP = 2;
public static final int ROOK = 3;
public static final int QUEEN = 4;
public static final int KING = 5;
// one of these is the color of the current piece
public static final int NOCOLOR = -1;
// the sum of the piece type and the
// color gives a number unique to both type and color,
// which is used for things like image indices.
public static final int BLACKCOLOR = 0;
public static final int WHITECOLOR = 6;
int color = NOCOLOR;
int imageIndex;
public Piece(int color, int pieceType)
{
// dbg -- all pieces are white rooks for now...
this.color = color;
imageIndex = color + pieceType;
}
/**
* return the integer associated with this piece's color;
*/
int getPieceColor()
{ return color;
}
/**
* return true if the piece is black
*/
public boolean isBlack()
{
return (color == BLACKCOLOR);
}
/**
* set the color associated with this piece; constants
* found in this class.
*/
public void setPieceColor(int givenColor)
{ color = givenColor;
}
/**
* return the integer designated for the image used for this piece.
*/
int getImageIndex()
{ return imageIndex;
}
}
and PieceImages.java
package main;
import java.awt.Component;
import java.awt.Image;
import java.awt.MediaTracker;
import java.awt.Toolkit;
import java.net.URL;
public class PieceImages
{ static Image images[] = null;
private static void dbg (String msg) { System.out.println(msg); }
public static Image getPieceImage (Component target, Piece piece)
{
if (images == null)
try
{
MediaTracker tracker = new MediaTracker(target);
images = new Image[12];
images[Piece.BLACKCOLOR + Piece.PAWN] = getImage(tracker, "bPawn.gif");
images[Piece.BLACKCOLOR + Piece.KNIGHT] = getImage(tracker, "bKnight.gif");
images[Piece.BLACKCOLOR + Piece.BISHOP] = getImage(tracker, "bBishop.gif");
images[Piece.BLACKCOLOR + Piece.ROOK] = getImage(tracker, "bRook.gif");
images[Piece.BLACKCOLOR + Piece.QUEEN] = getImage(tracker, "bQueen.gif");
images[Piece.BLACKCOLOR + Piece.KING] = getImage(tracker, "bKing.gif");
images[Piece.WHITECOLOR + Piece.PAWN] = getImage(tracker, "wPawn.gif");
images[Piece.WHITECOLOR + Piece.KNIGHT] = getImage(tracker, "wKnight.gif");
images[Piece.WHITECOLOR + Piece.BISHOP] = getImage(tracker, "wBishop.gif");
images[Piece.WHITECOLOR + Piece.ROOK] = getImage(tracker, "wRook.gif");
images[Piece.WHITECOLOR + Piece.QUEEN] = getImage(tracker, "wQueen.gif");
images[Piece.WHITECOLOR + Piece.KING] = getImage(tracker, "wKing.gif");
if (!tracker.waitForAll(10000))
{ System.out.println("ERROR: not all piece main.images loaded");
}
dbg("piece images loaded");
}
catch (Exception xcp)
{ System.out.println("Error loading images");
xcp.printStackTrace();
}
return images[piece.getImageIndex()];
}
private static Image getImage(MediaTracker tracker, String file)
{
URL url = PieceImages.class.getResource("images/" + file);
Image image = Toolkit.getDefaultToolkit().getImage(url);
tracker.addImage(image, 1);
return image;
}
}
Okay, this is a little slight of hand. The example code will only work for 90 degree increments (it was only designed this way), to do smaller increments you to use some trig to calculate the image width and height (there's a answer somewhere for that to ;))
public class ImagePane extends JPanel {
private BufferedImage masterImage;
private BufferedImage renderedImage;
public ImagePane(BufferedImage image) {
masterImage = image;
applyRotation(0);
}
#Override
public Dimension getPreferredSize() {
return new Dimension(renderedImage.getWidth(), renderedImage.getHeight());
}
#Override
public Dimension getMinimumSize() {
return getPreferredSize();
}
protected int getVirtualAngle(int angle) {
float fRotations = (float) angle / 360f;
int rotations = (int) (fRotations - (fRotations / 1000));
int virtual = angle - (rotations * 360);
if (virtual < 0) {
virtual = 360 + virtual;
}
return virtual;
}
public void applyRotation(int angle) {
// This will only work for angles of 90 degrees...
// Normalize the angle to make sure it's only between 0-360 degrees
int virtualAngle = getVirtualAngle(angle);
Dimension size = new Dimension(masterImage.getWidth(), masterImage.getHeight());
int masterWidth = masterImage.getWidth();
int masterHeight = masterImage.getHeight();
double x = 0; //masterWidth / 2.0;
double y = 0; //masterHeight / 2.0;
switch (virtualAngle) {
case 0:
break;
case 180:
break;
case 90:
case 270:
size = new Dimension(masterImage.getHeight(), masterImage.getWidth());
x = (masterHeight - masterWidth) / 2.0;
y = (masterWidth - masterHeight) / 2.0;
break;
}
renderedImage = new BufferedImage(size.width, size.height, masterImage.getTransparency());
Graphics2D g2d = renderedImage.createGraphics();
AffineTransform at = AffineTransform.getTranslateInstance(x, y);
at.rotate(Math.toRadians(virtualAngle), masterWidth / 2.0, masterHeight / 2.0);
g2d.drawImage(masterImage, at, null);
g2d.dispose();
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g;
int width = getWidth() - 1;
int height = getHeight() - 1;
int x = (width - renderedImage.getWidth()) / 2;
int y = (height - renderedImage.getHeight()) / 2;
g2d.drawImage(renderedImage, x, y, this);
}
}
Now, you could simply "flip" the image vertically, if that works better for you
public class FlipPane extends JPanel {
private BufferedImage masterImage;
private BufferedImage renderedImage;
public FlipPane(BufferedImage image) {
masterImage = image;
flipMaster();
}
#Override
public Dimension getPreferredSize() {
return new Dimension(renderedImage.getWidth(), renderedImage.getHeight());
}
#Override
public Dimension getMinimumSize() {
return getPreferredSize();
}
protected void flipMaster() {
renderedImage = new BufferedImage(masterImage.getWidth(), masterImage.getHeight(), masterImage.getTransparency());
Graphics2D g2d = renderedImage.createGraphics();
g2d.setTransform(AffineTransform.getScaleInstance(1, -1));
g2d.drawImage(masterImage, 0, -masterImage.getHeight(), this);
g2d.dispose();
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g;
int width = getWidth() - 1;
int height = getHeight() - 1;
int x = (width - renderedImage.getWidth()) / 2;
int y = (height - renderedImage.getHeight()) / 2;
g2d.drawImage(renderedImage, x, y, this);
}
}
This basically results in:
Original | 180 degree rotation | Vertical inversion...
Now, if you change the flipMaster method to read:
g2d.setTransform(AffineTransform.getScaleInstance(-1, -1));
g2d.drawImage(masterImage, -masterImage.getWidth(), -masterImage.getHeight(), this);
You'll get the same effect as the 180 rotation ;)
Try performing the rotation before translating it into the correct position. Simply reorder the transformations so that first you scale, then you rotate (around the center point of the image), and then you translate:
transform.scale(scale, scale);
transform.rotate(Math.PI, pieceWidth / 2, pieceHeight /2);
transform.translation(xPos, yPos);
By the way, the black pieces on a chess board usually aren't rotated. :)
Update
In what way does it not work? The solution I provided also also differs from your code in that scaling is performed before translating. You can try the rotating, translating, and then scaling.
I strongly suggest that you modify your code so that you can perform the translation last. If you do this, everything will become a lot less complicated. Once you have done so, you only have to scale once to automatically take care of the rotation.
transform.scale(scale, scale); // or transform.scale(scale, -scale); to rotate
transform.translate(xPos, yPos);

Rounding Inaccuracies When Combining Areas in 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.

Categories

Resources