I am, first of all, drawing two arcs randomly using the Graphics drawArc and fillArc methods. One arc, say arc1 is bigger than the other arc, say arc2.
Now i want to see if arc1, contains(wholly or partly) arc2. I have tried various ways but to no avail. Forexample, first of all calculating the distances between them and then taking the dot product of these two and seeing if its greater than the radius of the first arc multiplied by the cosine of its orientation.
Still no success, any help or suggestions offered will be greatly appreciated.
Is there a better/another approach to achieve this?
Is it also possible to estimate how much of arc2 is covered by arc1? thanks,
I will give you an easy solution that counts for any shape - not only arcs:
public Vector measureArea(int[] pix) {
int i;
Vector v=new Vector();
for(i=0; i<pix.length; i++)
if((pix[i]&0x00ffffff)==0x00000000) v.add(i);
return v;
}
This finds the pixels that belong to this area: you could fill the arc as follows then call this function:
BufferedImage bim=new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
Graphics g=bim.getGraphics();
g.setColor(Color.white);
g.fillRect(0, 0, w, h);
g.setColor(Color.black);
g2.fillArc(x, y, 2*w/16, 2*h/16, 270, 250);
int[] pix=bim.getRGB(0, 0, w, h, null, 0, w);
Vector v=measureArea(pix);
Repeat with the second arc then find the common points.
for(i=0; i<v.size(); i++) {
int I=((Integer)v.get(i)).intValue();
for(j=0; j<v2.size(); j++) {
int J=((Integer)v2.get(j)).intValue();
if(I==J) ..... // do something
}
}
If you want more of a mathematical approach you have to define the filled arc in terms of circle (or maybe two wedges) and find the area of the intersecting these shapes.
There is a third approach using Areas in java.
Area a=new Area(new Arc2D.Double(x+3*w/4-w/16, y+h/4-h/16, 2*w/16, 2*h/16, 270, 250, Arc2D.OPEN));
Area a2=new Area(new Arc2D.Double(x+3*w/4, y+h/4, 2*w/16, 2*h/16, 270, 200, Arc2D.OPEN));
Area intrsct=new Area(new Arc2D.Double(x+3*w/4-w/16, y+h/4-h/16, 2*w/16, 2*h/16, 270, 250, Arc2D.OPEN));
intrsct.intersect(a2);
Now intrsct has the intersection.
If we expand this to simple Shapes we have:
Arc2D.Double a=new Arc2D.Double(x+3*w/4-w/16, y+h/4-h/16, 2*w/16, 2*h/16, 270, 250, Arc2D.OPEN);
Arc2D.Double a2=new Arc2D.Double(x+3*w/4, y+h/4, 2*w/16, 2*h/16, 270, 200, Arc2D.OPEN);
Rectangle b=a.getBounds();
int intrsct=0;
for(i=0; i<b.getWidth(); i++)
for(j=0; j<b.getHeight(); j++)
if(a.contains(b.x+i, b.y+j) && a2.contains(b.x+i, b.y+j)) intrsct++;
A fourth approach.
--
If you want an arc with a given color you need to check for that color in the first approach. So we change measure area as follows:
public Vector measureArea(int[] pix, int color) {
int i;
Vector v=new Vector();
int c=color&0x00ffffff;
for(i=0; i<pix.length; i++)
if((pix[i]&0x00ffffff)==c) v.add(i);
return v;
}
and call it measureArea(pix, Color.red.getRGB()) for example.
And make sure you clear the image for each shape to be counted on its own:
public Image init( Graphics g )
{
bim=new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
g=bim.getGraphics();
g.setColor(Color.yellow);
g.fillRect(0, 0, w, h);
g.setColor(Color.red);
g.fillArc(x, y, 300, 300, 270, 75); // 2*w/16, 2*h/16
int[] pix=bim.getRGB(0, 0, w, h, null, 0, w);
Vector v1=measureArea(pix, Color.red.getRGB());
g.setColor(Color.yellow);
g.fillRect(0, 0, w, h);
g.setColor(Color.blue);
g.fillArc(x+100, y+100, 150, 150, 270, 45); //2*w/32, 2*h/32,
pix=bim.getRGB(0, 0, w, h, null, 0, w);
Vector v2=measureArea(pix, Color.blue.getRGB());
System.out.println( intersect(v1, v2) );
return bim;
}
Notes 3: the method with Areas is independent of color - use that if it works.
The method with pixels can be used later if you have complicated shapes:
To draw all the shapes together just do what you do now: keep them in one image. To measure the area use another image bim2 where you draw each shape successively call the measure area function clear the image etc - it doesnt have to be shown any where - you have the other image to show all the shapes together. I hope this works.
The answer by gpash lists several options. As mentioned in a comment, I'd recommend Area-based apprach for the generic case. Although area computations (like computing the intersection, for this example) can be expensive, they are likely a good tradeoff between the image-based and the purely analytical approaches:
The image-based approach raises some questions, e.g. regarding the image size. Additionally, the runtime and memory consumption may be large for "large" shapes (imagine shapes that cover a region of, say, 1000x1000 pixels).
The purely analytical solution may be rather mathematically involved. One could consider breaking it down to simpler tasks, and it's certainly doable, but not trivial. Maybe more importantly: This approach does not generalize for other Shape types.
With the Area based solution, computing the intersection between two arbitrary shapes s0 and s1 (which may be Arc2D, or any other shape) is fairly trivial:
Area a = new Area(s0);
a.intersect(new Area(s1));
(that's it).
A side note: One could consider performing a conservative test: The shapes can not intersect if their bounding volumes do not intersect. So for certain use-cases, one could consider doing something like this:
Shape s0 = ...;
Shape s1 = ...;
if (!s0.getBounds().intersects(s1.getBounds()))
{
// The bounds do not intersect. Then the shapes
// can not intersect.
return ...;
}
else
{
// The bounds DO intesect. Perform the Area-based
// intersection computation here:
...
}
What is left then is the computation of the area of an Area - that is, the size of the intersection area. The Area class has a method that can be used to check whether the area isEmpty. But it does not have a method to compute the size of the area. However, this can be computed by converting the resulting area into a polygon using a (flattening!) PathIterator, and then computing the polygon area as, for example in the answers to this question.
What may be tricky about this is that in general, areas can be signed (that is, they can be positive or negative, depending on whether the vertices of the polygon are given in counterclockwise or or clockwise order, respectively). Additionally, the intersection between two shapes does not necessarily result in a single, connected shape, but may result in different closed regions, as shown in this image:
The image is a screenshot from the following MCVE that allows dragging around the given shapes with the mouse, and prints the area of the shapes and their intersection.
This uses some utility methods for the area computation that are taken from a set of utilites for geometry in general, and shapes in particular, which I started collecting a while ago)
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.geom.AffineTransform;
import java.awt.geom.Arc2D;
import java.awt.geom.Area;
import java.awt.geom.PathIterator;
import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
public class ShapeIntersectionAreaTest
{
public static void main(String[] args)
{
SwingUtilities.invokeLater(() -> createAndShowGUI());
}
private static void createAndShowGUI()
{
JFrame f = new JFrame();
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.getContentPane().add(new ShapeIntersectionAreaTestPanel());
f.setSize(800,800);
f.setLocationRelativeTo(null);
f.setVisible(true);
}
}
class ShapeIntersectionAreaTestPanel extends JPanel
implements MouseListener, MouseMotionListener
{
private Shape shape0;
private Shape shape1;
private Shape draggedShape;
private Point previousMousePosition;
ShapeIntersectionAreaTestPanel()
{
shape0 = new Arc2D.Double(100, 160, 200, 200, 90, 120, Arc2D.PIE);
shape1 = new Arc2D.Double(300, 400, 100, 150, 220, 260, Arc2D.PIE);
addMouseListener(this);
addMouseMotionListener(this);
}
#Override
protected void paintComponent(Graphics gr)
{
super.paintComponent(gr);
Graphics2D g = (Graphics2D)gr;
g.setRenderingHint(
RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
g.setColor(Color.RED);
g.fill(shape0);
g.setColor(Color.BLUE);
g.fill(shape1);
Shape intersection =
ShapeIntersectionAreaUtils.computeIntersection(shape0, shape1);
g.setColor(Color.MAGENTA);
g.fill(intersection);
double area0 = Math.abs(
ShapeIntersectionAreaUtils.computeSignedArea(shape0, 1.0));
double area1 = Math.abs(
ShapeIntersectionAreaUtils.computeSignedArea(shape1, 1.0));
double areaIntersection = Math.abs(
ShapeIntersectionAreaUtils.computeSignedArea(intersection, 1.0));
g.setColor(Color.BLACK);
g.setFont(new Font("Monospaced", Font.PLAIN, 12));
g.drawString(String.format("Red area : %10.3f", area0), 10, 20);
g.drawString(String.format("Blue area : %10.3f", area1), 10, 40);
g.drawString(String.format("Intersection area: %10.3f", areaIntersection), 10, 60);
}
#Override
public void mouseDragged(MouseEvent e)
{
int dx = e.getX() - previousMousePosition.x;
int dy = e.getY() - previousMousePosition.y;
AffineTransform at =
AffineTransform.getTranslateInstance(dx, dy);
if (draggedShape == shape0)
{
shape0 = at.createTransformedShape(draggedShape);
draggedShape = shape0;
}
if (draggedShape == shape1)
{
shape1 = at.createTransformedShape(draggedShape);
draggedShape = shape1;
}
repaint();
previousMousePosition = e.getPoint();
}
#Override
public void mouseMoved(MouseEvent e)
{
}
#Override
public void mouseClicked(MouseEvent e)
{
}
#Override
public void mousePressed(MouseEvent e)
{
draggedShape = null;
if (shape0.contains(e.getPoint()))
{
draggedShape = shape0;
}
if (shape1.contains(e.getPoint()))
{
draggedShape = shape1;
}
previousMousePosition = e.getPoint();
}
#Override
public void mouseReleased(MouseEvent e)
{
draggedShape = null;
}
#Override
public void mouseEntered(MouseEvent e)
{
}
#Override
public void mouseExited(MouseEvent e)
{
}
}
// Utility methods related to shape and shape area computations, mostly taken from
// https://github.com/javagl/Geom/blob/master/src/main/java/de/javagl/geom/Shapes.java
class ShapeIntersectionAreaUtils
{
public static Shape computeIntersection(Shape s0, Shape s1)
{
Area a = new Area(s0);
a.intersect(new Area(s1));
return a;
}
/**
* Compute all closed regions that occur in the given shape, as
* lists of points, each describing one polygon
*
* #param shape The shape
* #param flatness The flatness for the shape path iterator
* #return The regions
*/
static List<List<Point2D>> computeRegions(
Shape shape, double flatness)
{
List<List<Point2D>> regions = new ArrayList<List<Point2D>>();
PathIterator pi = shape.getPathIterator(null, flatness);
double coords[] = new double[6];
List<Point2D> region = Collections.emptyList();
while (!pi.isDone())
{
switch (pi.currentSegment(coords))
{
case PathIterator.SEG_MOVETO:
region = new ArrayList<Point2D>();
region.add(new Point2D.Double(coords[0], coords[1]));
break;
case PathIterator.SEG_LINETO:
region.add(new Point2D.Double(coords[0], coords[1]));
break;
case PathIterator.SEG_CLOSE:
regions.add(region);
break;
case PathIterator.SEG_CUBICTO:
case PathIterator.SEG_QUADTO:
default:
throw new AssertionError(
"Invalid segment in flattened path");
}
pi.next();
}
return regions;
}
/**
* Computes the (signed) area enclosed by the given point list.
* The area will be positive if the points are ordered
* counterclockwise, and and negative if the points are ordered
* clockwise.
*
* #param points The points
* #return The signed area
*/
static double computeSignedArea(List<? extends Point2D> points)
{
double sum0 = 0;
double sum1 = 0;
for (int i=0; i<points.size()-1; i++)
{
int i0 = i;
int i1 = i + 1;
Point2D p0 = points.get(i0);
Point2D p1 = points.get(i1);
double x0 = p0.getX();
double y0 = p0.getY();
double x1 = p1.getX();
double y1 = p1.getY();
sum0 += x0 * y1;
sum1 += x1 * y0;
}
Point2D p0 = points.get(0);
Point2D pn = points.get(points.size()-1);
double x0 = p0.getX();
double y0 = p0.getY();
double xn = pn.getX();
double yn = pn.getY();
sum0 += xn * y0;
sum1 += x0 * yn;
double area = 0.5 * (sum0 - sum1);
return area;
}
/**
* Compute the (signed) area that is covered by the given shape.<br>
* <br>
* The area will be positive for regions where the points are
* ordered counterclockwise, and and negative for regions where
* the points are ordered clockwise.
*
* #param shape The shape
* #param flatness The flatness for the path iterator
* #return The signed area
*/
public static double computeSignedArea(Shape shape, double flatness)
{
double area = 0;
List<List<Point2D>> regions = computeRegions(shape, flatness);
for (List<Point2D> region : regions)
{
double signedArea = computeSignedArea(region);
area += signedArea;
}
return area;
}
}
(Note: The mechanisms for dragging the shapes are not particularly elegant. In a real application, this should be solved differently - this is just for the demonstration of the area computation methods)
Related
here i'm trying to draw a circle using drawOval method and I want to move it on the screen with a specific velocity. but i have a problem with double variables for the velocity.for example when vx=0.25 and vy=0 the circle is just stuck on its place.
sorry for my bad English though.
here is the java code that i'm using
int x=0 , y=0;
#Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
move();
g.drawOval(x, y, 10, 10);
repaint();
}
public void move() {
x+=0.25;
y+=0.25;
}
You should not call move from the paintComponent method! You never know when this method will be called, and thus, you cannot control the movement speed properly.
You should not call repaint from the paintComponent method! Never. This will send the painting system into an endless cycle of repaint operations!
Regarding the question:
There is a method for drawing arbitrary shapes based on double coordinates. This is also covered and explained extensively in the 2D Graphics Tutorial. The key is to use the Shape interface. For your particular example, the relevant part of the code is this:
private double x = 0;
private double y = 0;
#Override
public void paintComponent(Graphics gr)
{
super.paintComponent(gr);
Graphics2D g = (Graphics2D)gr;
double radius = 5;
g.draw(new Ellipse2D.Double(
x - radius, y - radius, radius * 2, radius * 2));
}
That is, you create an Ellipse2D instance, and then just draw it.
Here is an MVCE, showing what you're probably trying to accomplish:
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.geom.Ellipse2D;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
public class PaintWithDouble
{
public static void main(String[] args)
{
SwingUtilities.invokeLater(() -> createAndShowGui());
}
private static void createAndShowGui()
{
JFrame f = new JFrame();
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
PaintWithDoublePanel p = new PaintWithDoublePanel();
f.getContentPane().add(p);
startMoveThread(p);
f.setSize(500, 500);
f.setLocationRelativeTo(null);
f.setVisible(true);
}
private static void startMoveThread(PaintWithDoublePanel p)
{
Thread t = new Thread(() -> {
while (true)
{
p.move();
p.repaint();
try
{
Thread.sleep(20);
}
catch (InterruptedException e)
{
Thread.currentThread().interrupt();
return;
}
}
});
t.setDaemon(true);
t.start();
}
}
class PaintWithDoublePanel extends JPanel
{
private double x = 0;
private double y = 0;
#Override
public void paintComponent(Graphics gr)
{
super.paintComponent(gr);
Graphics2D g = (Graphics2D) gr;
g.setRenderingHint(
RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
double radius = 5;
g.draw(new Ellipse2D.Double(
x - radius, y - radius, radius * 2, radius * 2));
g.drawString("At " + x + ", " + y, 10, 30);
}
public void move()
{
x += 0.05;
y += 0.05;
}
}
Edited in response to the comment (and to clarify some things that have been said in other answers) :
While it is technically correct to say that there are "only whole pixels", and there "is no pixel with coordinates (0.3, 1.8)", this does not mean that fractional coordinates will not affect the final appearance of the rendered output. Every topic becomes a science when you're studying it long enough. Particularly, a lot of research went into the question of how to improve the visual appearance of rendered output, going beyond what you can achieve with a trivial Bresenham or so. An entry point for further research could be the article about subpixel rendering.
In many cases, as usual, there are trade-offs between the appearance and the drawing performance. As for Java and its 2D drawing capabilities, these trade-offs are mostly controlled via the RenderingHints class. For example, there is the RenderingHints#VALUE_STROKE_PURE that enables subpixel rendering. The effect is shown in this screen capture:
The slider is used to change the y-offset of the rightmost point of a horizontal line by -3 to +3 pixels. In the upper left, you see a line, rendered as-it-is. In the middle, you see the line magnified by a factor of 8, to better show the effect: The pixels are filled with different opacities, depending on how much of the pixel is covered by an idealized, 1 pixel wide line.
While it's certainly the case that this is not relevant for most application cases, it might be worth noting here.
The following is an MCVE that was used for the screen capture:
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.geom.Line2D;
import java.awt.image.BufferedImage;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JSlider;
import javax.swing.SwingUtilities;
public class PaintWithDoubleMagnified
{
public static void main(String[] args)
{
SwingUtilities.invokeLater(() -> createAndShowGui());
}
private static void createAndShowGui()
{
JFrame f = new JFrame();
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.getContentPane().setLayout(new BorderLayout());
PaintWithDoubleMagnifiedPanel p = new PaintWithDoubleMagnifiedPanel();
f.getContentPane().add(p, BorderLayout.CENTER);
JSlider slider = new JSlider(0, 100, 50);
slider.addChangeListener(e -> {
int value = slider.getValue();
double relative = -0.5 + value / 100.0;
p.setY(relative * 6);
});
f.getContentPane().add(slider, BorderLayout.SOUTH);
f.setSize(500, 500);
f.setLocationRelativeTo(null);
f.setVisible(true);
}
}
class PaintWithDoubleMagnifiedPanel extends JPanel
{
private double y = 0;
#Override
public void paintComponent(Graphics gr)
{
super.paintComponent(gr);
Graphics2D g = (Graphics2D) gr;
g.drawString("At " + y, 10, 20);
paintLine(g);
BufferedImage image = paintIntoImage();
g.setRenderingHint(
RenderingHints.KEY_INTERPOLATION,
RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR);
g.scale(8.0, 8.0);
g.drawImage(image, 0, 0, null);
}
public void setY(double y)
{
this.y = y;
repaint();
}
private void paintLine(Graphics2D g)
{
g.setColor(Color.BLACK);
g.setRenderingHint(
RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
g.setRenderingHint(
RenderingHints.KEY_STROKE_CONTROL,
RenderingHints.VALUE_STROKE_PURE);
Line2D line = new Line2D.Double(
10, 30, 50, 30 + y);
g.draw(line);
}
private BufferedImage paintIntoImage()
{
BufferedImage image = new BufferedImage(
100, 100, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = image.createGraphics();
paintLine(g);
g.dispose();
return image;
}
}
First notice that rendering system is using int as arguments for each pixel.
So if p1 is near p2 on x axis then
p1(x,y) and p2(x+1,y)
eg:(0,0) and (1,0)
You do not have something in the middle like (0.5,1) since no pixels.
That why Graphics api is using int for (x,y) coordinates.
Graphics api
If you wanted to consider also double you have to adapt the default coordinates systems to fit your needs.(cannot render all double there in individual pixels, need to group them in categories)
Eg. say want to place x_points : 0, 0.5, 1
So 0->0, 0.5(double)->1(int) , 1->2
Other pixels could map as 0.2->1, 0.7->2 , -0.9->0
One rule map consider all double range (better say in (-0.5,1])
can be -0.5<d<=0 -> 0,0<d<=0.5 -> 1, 0.5<d<=1 -> 2 where d=input_x(double)
That means you adjust the coordinates systems to fit your needs
is there a method in java for drawing a circle with double variables for its center?
NO(using standard Graphics api). Have just what api is provided, but you could render what ever input you wanted (even based on double) by adjusting coordinates system.
class MyPaint extends JPanel
{
private double x = 0, y=0;
private int width = 30, height = 30;
//adjust coordinates system
//for x in [0,1] have [0,0.1,0.2,0.3 ..]
//from no pixel between (0,1) to 9 pixels (0,0.1, ..,1)
//0->0,0.1->1,0.2->2,0.9->9,1->10
//in that way you have full control of rendering
private double scale_x = 0.1;
//same on y as x
private double scale_y = 0.1;
//pixel scaled on x,y
//drawing with
private int xs,ys;
#Override
public void paintComponent(Graphics g)
{
super.paintComponent(g);
xs = (int) (x/scale_x);
ys = (int) (y/scale_y);
g.drawString("Draw At: " + xs + ", " + ys + " From:" + x+","+y, 10, 30);
g.drawOval(xs, ys, (int) (width/scale_x), (int) (height/scale_y));
}
public void move()
{
//adjustments is better to be >= then scale(x or y) seen as absolute value
//if need 0.01 to be display on individual pixel on x
//then modify scale_x = 0.01 (or even 0.001)
x+=0.1;
y+=0.5;
}
}
I'm trying to fill a triangle using horizontal lines and I can't figure out what's wrong with my current method. Before anyone says to just use fillPolygon, I can't use that. I need to fill it using lines.
It seems to work ok in some situations and completely break in others.
That's how it should look. But then I tried applying my method to a rotating 3D cube and...
I have no idea what's wrong. Also, the red borders are also one of my triangle methods. Those work perfectly and the filled triangles and the outlined triangles have the same vertices inputted.
public void filledTri(int x1,int y1,int x2,int y2,int x3,int y3){
int[] xs = {x1,x2,x3};
int[] ys = {y1,y2,y3};
//Sort vertices in vertical order so A/1 is highest and C/3 is lowest
int I,tempx,tempy;
for(int i=1;i<3;i++){
I = i-1;
tempx = xs[i];
tempy = ys[i];
while(I>=0&&tempy<ys[I]){
xs[I+1] = xs[I];
ys[I+1] = ys[I];
I--;
}
xs[I+1] = tempx;
ys[I+1] = tempy;
}
//Set left and right edges
linepts ab = new linepts(xs[0],ys[0],xs[1],ys[1]),
ac = new linepts(xs[0],ys[0],xs[2],ys[2]);
linepts[] lines = {ab.getEndX() < ac.getEndX() ? ab : ac,
ab.getEndX() > ac.getEndX() ? ab : ac,
new linepts(xs[1],ys[1],xs[2],ys[2])};
//Fill triangle
int startY = ys[0],endY = ys[2];
for(int y=startY;y<=endY;y++){
if(y>ys[1])
horizontalLine((int)Math.round(lines[2].getX(y)),
y,
(int)Math.round(lines[1].getX(y)));
else
horizontalLine((int)Math.round(lines[0].getX(y)),
y,
(int)Math.round(lines[1].getX(y)));
}
getX(int y) gets me the x coordinate where the line passes through the y value. If it's a horizontal line it just returns the line's start x
Point A is the highest on screen and the lowest value, B is the middle, and C is the lowest on screen and highest value
I'm using a buffered image on a jframe to draw it if that helps.
I've seen what you are doing in a Software Renderer tutorial. It is explained in this and this episodes.
What he does there is scanning the longest to get every pixel on that line, it stores the min X value and max X value, (given by the other 2 lines). He originally makes it for specific triangles, but then he upgrades the code to accept generic triangles.
Here's a nice diagram to explain that:
I assume what you're experiencing is because of projecting 3D triangles into 2D ones (clipping, triangles get infinite coordinates, or because you're program doesn't takes too well empty triangles.
One way is to draw the lines to an image, then use that image in a TexturePaint to fill a Shape (the triangle in this case).
It might look something like this: (if you use a single image containing one red line, put it over a random BG color, and use a smoothed 1.5 pixel stroke to draw the shape itself in blue).
import java.awt.*;
import java.awt.geom.*;
import java.awt.image.BufferedImage;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import java.util.*;
public class LinesFillShape {
private JComponent ui = null;
LinesFillShape() {
initUI();
}
public final void initUI() {
if (ui != null) {
return;
}
ui = new JPanel(new BorderLayout(4, 4));
ui.setBorder(new EmptyBorder(4, 4, 4, 4));
ui.add(new JLabel(new ImageIcon(getImage())));
}
private void drawPolygon(Graphics2D g, int sz, Random r) {
int[] xpoints = {
r.nextInt(sz), r.nextInt(sz), r.nextInt(sz)
};
int[] ypoints = {
r.nextInt(sz), r.nextInt(sz), r.nextInt(sz)
};
Polygon p = new Polygon(xpoints, ypoints, 3);
Color bg = new Color(r.nextInt(255),r.nextInt(255),r.nextInt(255));
g.setColor(bg);
g.fill(p);
g.setPaint(
new TexturePaint(getTexture(),
new Rectangle2D.Double(0, 0, 8, 8)));
g.fill(p);
g.setStroke(new BasicStroke(1.5f));
g.setColor(Color.BLUE);
g.draw(p);
}
private BufferedImage getImage() {
int sz = 600;
BufferedImage bi = new BufferedImage(sz, sz, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = bi.createGraphics();
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
Random r = new Random();
drawPolygon(g, sz, r);
drawPolygon(g, sz, r);
drawPolygon(g, sz, r);
g.dispose();
return bi;
}
private BufferedImage getTexture() {
BufferedImage bi = new BufferedImage(8, 8, BufferedImage.TYPE_INT_ARGB);
Graphics g = bi.getGraphics();
g.setColor(Color.RED);
// TODO: something more interesting here..
g.drawLine(0, 0, 0, 8);
g.dispose();
return bi;
}
public JComponent getUI() {
return ui;
}
public static void main(String[] args) {
Runnable r = () -> {
try {
UIManager.setLookAndFeel(
UIManager.getSystemLookAndFeelClassName());
} catch (Exception useDefault) {
}
LinesFillShape o = new LinesFillShape();
JFrame f = new JFrame(o.getClass().getSimpleName());
f.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
f.setLocationByPlatform(true);
f.setContentPane(o.getUI());
f.pack();
f.setMinimumSize(f.getSize());
f.setVisible(true);
};
SwingUtilities.invokeLater(r);
}
}
I have not scrutinized your code but I can tell you that you are not always joining intersections with the relevant sides.
You can work as follows:
For a given scanline (some Y),
compare the ordinates of the endpoints of the three sides in pairs (Y0-Y1, Y1-Y2, Y2-Y0),
there will be zero or two sides that straddle Y; use the condition (Yi > Y) != (Yi+1 > Y) (indexes modulo 3), and no other,
for the sides that straddle Y, compute the intersection point.
You will scan from min(Y0, Y1, Y2) to max(Y0, Y1, Y2) and each time join the two intersections.
I'm trying to draw a circle with a random center inside a big bigger circular surface. (I'm actually trying to simulate a human and his eyesight inside a room!) I need to draw a random line (call it line1) passing through its center which will intersect with the surface. line1 does not necessarily pass the center of circular surface. I also need to draw two lines forming 60 degree, facing on one side of line1. Can anyone help me with that?
I created an example of what I need to draw.
import java.awt.Color;
import java.awt.Frame;
import java.awt.Graphics;
import java.awt.Point;
import java.util.Random;
import javax.swing.JFrame;
public class ShapeTest extends JFrame{
int width=500;
int height=500;
public ShapeTest(){
setSize(width,height);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setResizable(false);
setLocationRelativeTo(null);
setVisible(true);
}
public static void main(String a[]){
new ShapeTest();
}
public void paint(Graphics g){
// Circular Surface
drawCircleByCenter(g, width/2, height/2, width/2);
Random r = new Random();
Point center = new Point();
center.x=r.nextInt(width/2);
center.y=r.nextInt(width/2);
drawCircleByCenter(g, center.x, center.y, width/15);
}
void drawCircleByCenter(Graphics g, int x, int y, int radius){
//g.setColor(Color.LIGHT_GRAY);
g.drawOval(x-radius, y-radius, 2*radius, 2*radius);
}
}
Start by changing your method to draw a circle based on its center and radius to a method which returns a Ellipse2D object representing the circle. This will allow us to do some clipping and other things with the shape besides just draw it.
Setting the clip to be the shape of your large circle prevents stray marks from being made where you don't want them (think "color inside the lines"). This is important because when we draw the circles and lines inside the big circle, some of them will be too big and would otherwise mark outside the bounds of the big circle.
Once we set the clip, we use the method Line2D getVector(Point2D, double, length) with an origin at the center of the large circle, a random angle and a random length (capped to keep the small blue circle inside the big circle). Think of this a random polar coordinate with the center of the large circle as the origin. The end point of this vector is used to mark the center of the small circle.
Using the center of the small circle as a starting point, we can generate two vectors in opposite directions (just negate the length of one to get it going the other direction) by using a random direction angle. We use a length equal to the diameter of the big circle to make certain that the lines will always go all the way up to the edge of the big circle (but not past, thanks to our clip).
We simply add 60 and 120 degrees to the angle of our blue dashed line and draw two green lines calculating the vectors the same way we did for the two blue dashed lines, except we don't need to create ones with negated lengths. We can also add a normal vector in for good measure simply by adding 90 degrees to the angle of the blue dashed line.
Lastly, we pick some random polar coordinates (just like we did for the small blue circle) to represent some people, and using the intersection of the people with the areas created by the various lines, we can see where they are at and draw them up with color coded values.
Now that we have all the people, we eliminate the clip and draw the big circle and voila!
Check out Draw a line at a specific angle in Java for details on how I calculated the vectors for the lines.
But enough talk, here's the code:
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.geom.Area;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Line2D;
import java.awt.geom.Path2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.Random;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
public class ShapeTest extends JFrame {
private static final long serialVersionUID = 1L;
private int width = 500;
private int height = 500;
private int padding = 50;
private BufferedImage graphicsContext;
private JPanel contentPanel = new JPanel();
private JLabel contextRender;
private Stroke dashedStroke = new BasicStroke(3.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_ROUND, 2f, new float[] {3f, 3f}, 0f);
private Stroke solidStroke = new BasicStroke(3.0f);
private RenderingHints antialiasing;
private Random random = new Random();
public static void main(String[] args) {
//you should always use the SwingUtilities.invodeLater() method
//to perform actions on swing elements to make certain everything
//is happening on the correct swing thread
Runnable swingStarter = new Runnable()
{
#Override
public void run(){
new ShapeTest();
}
};
SwingUtilities.invokeLater(swingStarter);
}
public ShapeTest(){
antialiasing = new RenderingHints(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
graphicsContext = new BufferedImage(width + (2 * padding), width + (2 * padding), BufferedImage.TYPE_INT_RGB);
contextRender = new JLabel(new ImageIcon(graphicsContext));
contentPanel.add(contextRender);
contentPanel.setSize(width + padding * 2, height + padding * 2);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setResizable(false);
this.setContentPane(contentPanel);
//take advantage of auto-sizing the window based on the size of its contents
this.pack();
this.setLocationRelativeTo(null);
this.paint();
setVisible(true);
}
public void paint() {
Graphics2D g2d = graphicsContext.createGraphics();
g2d.setRenderingHints(antialiasing);
//Set up the font to print on the circles
Font font = g2d.getFont();
font = font.deriveFont(Font.BOLD, 14f);
g2d.setFont(font);
FontMetrics fontMetrics = g2d.getFontMetrics();
//clear the background
g2d.setColor(Color.WHITE);
g2d.fillRect(0, 0, graphicsContext.getWidth(), graphicsContext.getHeight());
//set up the large circle
Point2D largeCircleCenter = new Point2D.Double((double)width / 2 + padding, (double)height / 2 + padding);
double largeCircleRadius = (double)width / 2;
Ellipse2D largeCircle = getCircleByCenter(largeCircleCenter, largeCircleRadius);
//here we build the small circle
Point2D smallCircleCenter = new Point2D.Double();
double smallCircleRadius = 15;
//we need to make certain it is confined inside the larger circle
//so we choose the following values carefully
//we want to go a random direction from the circle, so chose an
//angle randomly in any direction
double smallCenterVectorAngle = random.nextDouble() * 360.0d;
//and we want to be a random distance from the center of the large circle, but
//we limit the distance based on the radius of the small circle to prevent it
//from appearing outside the large circle
double smallCenterVectorLength = random.nextDouble() * (largeCircleRadius - smallCircleRadius);
Line2D vectorToSmallCenter = getVector(largeCircleCenter, smallCenterVectorAngle, smallCenterVectorLength);
//the resulting end point of the vector is a random distance from the center of the large circle
//in a random direction, and guaranteed to not place the small circle outside the large
smallCircleCenter.setLocation(vectorToSmallCenter.getP2());
Ellipse2D smallCircle = getCircleByCenter(smallCircleCenter, smallCircleRadius);
//before we draw any of the circles or lines, set the clip to the large circle
//to prevent drawing outside our boundaries
g2d.setClip(largeCircle);
//chose a random angle for the line through the center of the small circle
double angle = random.nextDouble() * 360.0d;
//we create two lines that start at the center and go out at the angle in
//opposite directions. We use 2*largeCircleRadius to make certain they
//will be large enough to fill the circle, and the clip we set prevent stray
//marks outside the big circle
Line2D centerLine1 = getVector(smallCircleCenter, angle, largeCircleRadius * 2);
Line2D centerLine2 = getVector(smallCircleCenter, angle, -largeCircleRadius * 2);
//now we just add 20 and 120 to our angle for the center-line, start at the center
//and again, use largeCircleRadius*2 to make certain the lines are big enough
Line2D sightVector1 = getVector(smallCircleCenter, angle + 60, largeCircleRadius * 2);
Line2D sightVector2 = getVector(smallCircleCenter, angle + 120, largeCircleRadius * 2);
Path2D visible = new Path2D.Double();
visible.moveTo(sightVector1.getX2(), sightVector1.getY2());
visible.lineTo(smallCircleCenter.getX(), smallCircleCenter.getY());
visible.lineTo(sightVector2.getX2(), sightVector2.getY2());
visible.closePath();
Path2D greenSide = new Path2D.Double();
greenSide.moveTo(centerLine1.getX2(), centerLine1.getY2());
greenSide.lineTo(smallCircleCenter.getX(), smallCircleCenter.getY());
greenSide.lineTo(centerLine2.getX2(), centerLine2.getY2());
greenSide.lineTo(sightVector1.getX2(), sightVector1.getY2());
greenSide.closePath();
int personCount = 5;
Area visibleArea = new Area(visible);
visibleArea.intersect(new Area(largeCircle));
Area greenSideArea = new Area(greenSide);
greenSideArea.intersect(new Area(largeCircle));
//we create a list of the people in the circle to
//prevent overlap
ArrayList<Shape> people = new ArrayList<Shape>();
people.add(smallCircle);
int i = 0;
personLoop: while (i < personCount){
double personCenterVectorAngle = random.nextDouble() * 360.0d;
double personCenterVectorLength = random.nextDouble() * (largeCircleRadius - smallCircleRadius);
Line2D vectorToPersonCenter = getVector(largeCircleCenter, personCenterVectorAngle, personCenterVectorLength);
Point2D personCircleCenter = vectorToPersonCenter.getP2();
Ellipse2D personCircle = getCircleByCenter(personCircleCenter, smallCircleRadius);
//this little loop lets us skip a person if they have overlap
//with another person, since people don't generally overlap
Area personArea = new Area(personCircle);
for (Shape person : people)
{
Area overlapArea = new Area(person);
overlapArea.intersect(personArea);
//this means that we have found a conflicting
//person, so should skip them
if (!overlapArea.isEmpty()){
continue personLoop;
}
}
people.add(personCircle);
personArea.intersect(visibleArea);
Area greenSideAreaTest = new Area(personCircle);
greenSideAreaTest.intersect(greenSideArea);
if (personArea.isEmpty()){
if (greenSideAreaTest.isEmpty()){
g2d.setColor(Color.orange);
System.out.println("Person " + i + " is behind the blue line");
}
else {
System.out.println("Person " + i + " is in front of the blue line");
g2d.setColor(Color.cyan);
}
}
else
{
System.out.println("Person " + i + " is between the green lines");
g2d.setColor(Color.magenta);
}
//alternatively to circles intersecting the area of interest, we can check whether the center
//is in the area of interest which may make more intuitive sense visually
// if (visibleArea.contains(personCircleCenter)){
// System.out.println("Person " + i + " is between the green lines");
// g2d.setColor(Color.magenta);
// }
// else {
// if (greenSideArea.contains(personCircleCenter)) {
// System.out.println("Person " + i + " is in front of the blue line");
// g2d.setColor(Color.cyan);
// }
// else{
// g2d.setColor(Color.orange);
// System.out.println("Person " + i + " is behind the blue line");
// }
// }
g2d.fill(personCircle);
g2d.setColor(Color.black);
String itemString = "" + i;
Rectangle2D itemStringBounds = fontMetrics.getStringBounds(itemString, g2d);
double textX = personCircleCenter.getX() - (itemStringBounds.getWidth() / 2);
double textY = personCircleCenter.getY() + (itemStringBounds.getHeight()/ 2);
g2d.drawString("" + i, (float)textX, (float)textY);
i++;
}
//fill the small circle with blue
g2d.setColor(Color.BLUE);
g2d.fill(smallCircle);
//draw the two center lines lines
g2d.setStroke(dashedStroke);
g2d.draw(centerLine1);
g2d.draw(centerLine2);
//create and draw the black offset vector
Line2D normalVector = getVector(smallCircleCenter, angle + 90, largeCircleRadius * 2);
g2d.setColor(Color.black);
g2d.draw(normalVector);
//draw the offset vectors
g2d.setColor(new Color(0, 200, 0));
g2d.draw(sightVector1);
g2d.draw(sightVector2);
//we save the big circle for last, to cover up any stray marks under the stroke
//of its perimeter. We also set the clip back to null to prevent the large circle
//itselft from accidentally getting clipped
g2d.setClip(null);
g2d.setStroke(solidStroke);
g2d.setColor(Color.BLACK);
g2d.draw(largeCircle);
g2d.dispose();
//force the container for the context to re-paint itself
contextRender.repaint();
}
private static Line2D getVector(Point2D start, double degrees, double length){
//we just multiply the unit vector in the direction we want by the length
//we want to get a vector of correct direction and magnitute
double endX = start.getX() + (length * Math.sin(Math.PI * degrees/ 180.0d));
double endY = start.getY() + (length * Math.cos(Math.PI * degrees/ 180.0d));
Point2D end = new Point2D.Double(endX, endY);
Line2D vector = new Line2D.Double(start, end);
return vector;
}
private static Ellipse2D getCircleByCenter(Point2D center, double radius)
{
Ellipse2D.Double myCircle = new Ellipse2D.Double(center.getX() - radius, center.getY() - radius, 2 * radius, 2 * radius);
return myCircle;
}
}
The logic of the geometry turned out to be more tricky than I'd presumed, but this is what I think you are after.
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import java.awt.image.BufferedImage;
import java.io.*;
import java.util.Random;
import javax.imageio.ImageIO;
import javax.swing.*;
class HumanEyesightLines {
int rad = 150;
int radSmall = 15;
int pad = 10;
JPanel gui = new JPanel(new BorderLayout());
BufferedImage img = new BufferedImage(
2 * (rad + pad),
2 * (rad + pad),
BufferedImage.TYPE_INT_RGB);
Timer timer;
JLabel imgDisplay;
Random rnd = new Random();
RenderingHints rh = new RenderingHints(
RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
HumanEyesightLines() {
imgDisplay = new JLabel(new ImageIcon(img));
gui.add(imgDisplay);
File f = new File(System.getProperty("user.home"));
final File f0 = new File("HumanEyesiteLines");
f0.mkdirs();
try {
Desktop.getDesktop().open(f0);
} catch (IOException ex) {
ex.printStackTrace();
}
ActionListener animationListener = new ActionListener() {
int ii = 0;
#Override
public void actionPerformed(ActionEvent e) {
paintImage();
ii++;
if (ii < 100) {
System.out.println(ii);
File f1 = new File(f0, "eg" + ii + ".png");
try {
ImageIO.write(img, "png", f1);
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
};
timer = new Timer(100, animationListener);
paintImage();
}
float[] dash = {3f, 3f};
float phase = 0f;
private final void paintImage() {
Graphics2D g = img.createGraphics();
g.setRenderingHints(rh);
g.setStroke(new BasicStroke(2f));
// fill the BG
g.setColor(Color.WHITE);
g.fillRect(0, 0, 2 * (rad + pad), 2 * (rad + pad));
// draw the big circle
Point center = new Point(rad + pad, rad + pad);
Shape bigCircle = new Ellipse2D.Double(pad, pad, 2 * rad, 2 * rad);
g.setColor(Color.MAGENTA.darker());
g.fill(bigCircle);
// set the clip to that of the big circle
g.setClip(bigCircle);
// draw the small circle
int xOff = rnd.nextInt(rad) - rad / 2;
int yOff = rnd.nextInt(rad) - rad / 2;
int x = center.x - xOff;
int y = center.y - yOff;
Shape smallCircle = new Ellipse2D.Double(
x - radSmall, y - radSmall,
2 * radSmall, 2 * radSmall);
g.setColor(Color.YELLOW);
g.fill(smallCircle);
g.setColor(Color.ORANGE);
g.draw(smallCircle);
g.setStroke(new BasicStroke(
1.5f,
BasicStroke.CAP_BUTT,
BasicStroke.JOIN_ROUND,
2f,
dash,
phase));
// I don't know what the rule is for where the blue line goes, so
// will use the top left corner of the image as a 2nd anchor point.
int x0 = 0;
int y0 = 0;
double grad = (double) (y - y0) / (double) (x - x0);
// now calculate the RHS point from y = mx + b
// where b = 0 and m is the gradient
int x1 = 2 * (pad + rad);
int y1 = (int) (grad * x1);
Line2D.Double line1 = new Line2D.Double(x0, y0, x1, y1);
g.setColor(Color.BLUE);
g.draw(line1);
//find the perpendicular gradient.
double perpGrad = -1d / grad;
double perpTheta = Math.atan(perpGrad);
// angle from perp
double diffTheta = Math.PI / 6d;
g.setColor(Color.GREEN);
double viewLine1Theta = perpTheta + diffTheta;
Line2D.Double viewLine1 = getLine(x, y, viewLine1Theta);
double viewLine2Theta = perpTheta - diffTheta;
Line2D.Double viewLine2 = getLine(x, y, viewLine2Theta);
g.draw(viewLine1);
g.draw(viewLine2);
g.setColor(Color.BLACK);
Line2D.Double viewPerp = getLine(x, y, perpTheta);
g.draw(viewPerp);
g.setColor(Color.RED);
g.draw(bigCircle);
g.dispose();
imgDisplay.repaint();
}
/**
* Returns a Line2D starting at the point x1,y1 at angle theta.
*/
private final Line2D.Double getLine(double x1, double y1, double theta) {
double m;
double b;
double x2;
double y2;
if (theta < (-Math.PI / 2d)) {
System.out.println("CHANGE IT! " + theta);
m = Math.tan(theta);
b = y1 - (m * x1);
x2 = 0;
y2 = (m * x2) + b;
} else {
m = Math.tan(theta);
b = y1 - (m * x1);
x2 = 2 * (rad + pad);
y2 = (m * x2) + b;
}
/*
* System.out.println("Perp theta: " + theta); System.out.println("Line
* grad: " + m); System.out.println("Line off: " + b);
* System.out.println("x1,y1: " + x1 + "," + y1);
* System.out.println("x2,y2: " + x2 + "," + y2);
*
*/
return new Line2D.Double(x1, y1, x2, y2);
}
public JComponent getGui() {
return gui;
}
public void start() {
timer.start();
}
public void stop() {
timer.stop();
}
public static void main(String[] args) {
Runnable r = new Runnable() {
#Override
public void run() {
HumanEyesightLines hel = new HumanEyesightLines();
hel.start();
JOptionPane.showMessageDialog(null, hel.getGui());
hel.stop();
}
};
// Swing GUIs should be created and updated on the EDT
// http://docs.oracle.com/javase/tutorial/uiswing/concurrency
SwingUtilities.invokeLater(r);
}
}
My final goal is to have a method, lets say:
Rectangle snapRects(Rectangle rec1, Rectangle rec2);
Imagine a Rectangle having info on position, size and angle.
Dragging and dropping the ABDE rectangle close to the BCGF rectangle would call the method with ABDE as first argument and BCGF as second argument, and the resulting rectangle is a rectangle lined up with BCGF's edge.
The vertices do not have to match (and preferrably won't so the snapping isn't so restrictive).
I can only understand easily how to give the same angle, but the position change is quite confusing to me. Also, i believe even if i reached a solution it would be quite badly optimized (excessive resource cost), so I would appreciate guidance on this.
(This has already been asked but no satisfatory answer was given and the question forgotten.)
------------------------------------------------------------------
Edit: It seems my explanation was insufficient so I will try to clarify my wishes:
The following image shows the goal of the method in a nutshell:
Forget about "closest rectangle", imagine there are just two rectangles. The lines inside the rectangles represent the direction they are facing (visual aid for the angle).
There is a static rectangle, which is not to be moved and has an angle (0->360), and a rectangle (also with an angle) which I want to Snap to the closest edge of the static rectangle. By this, i mean, i want the least transformations possible for the "snap to edge" to happen.
This brings many possible cases, depending on the rotation of the rectangles and their position relative to each other.
The next image shows the static rectangle and how the position of the "To Snap" rectangle changes the snapping result:
The final rotations might not be perfect since it was done by eye, but you get the point, it matters the relative position and also both angles.
Now, in my point of view, which may be completely naive, I see this problem solved on two important and distinct steps on transforming the "To Snap" rectangle: Positioning and Rotation
Position: The objective of the new position is to stick to the closest edge, but since we want it to stick paralell to the static rectangle, the angle of the static rectangle matters. The next image shows examples of positioning:
In this case, the static rectangle has no angle, so its easy to determine up, down, left and right. But with angle, there are alot more possibilities:
As for the rotation, the goal is for the "to snap" rectangle to rotate the minimum needed to become paralell with the static rectangle:
As a final note, in regard of implementation input, the goal is to actually drag the "to snap" rectangle to wherever position i wish around the static rectangle and by pressing a keyboard key, the snap happens.
Also, it appears i have exagerated a little when i asked for optimization, to be honest i do not need or require optimization, I do prefer an easy to read, step by step clear code (if its the case), rather than any optimization at all.
I hope i was clear this time, sorry for the lack of clarity in the first place, if you have any more doubts please do ask.
The problem is obviously underspecified: What does "line up" for the edges mean? A common start point (but not necessarily a common end point)? A common center point for both edges? (That's what I assumed now). Should ALL edges match? What is the criterion for deciding WHICH edge of the first rectangle should be "matched" with WHICH edge of the second rectangle? That is, imagine one square consists exactly of the center points of the edges of the other square - how should it be aligned then?
(Secondary question: In how far is optimization (or "low resource cost") important?)
However, I wrote a few first lines, maybe this can be used to point out more clearly what the intended behavior should be - namely by saying in how far the intended behavior differs from the actual behavior:
EDIT: Old code omitted, update based on the clarification:
The conditions for the "snapping" are still not unambiguous. For example, it is not clear whether the change in position or the change in the angle should be preferred. But admittedly, I did not figure out in detail all possible cases where this question could arise. In any case, based on the updated question, this might be closer to what you are looking for.
NOTE: This code is neither "clean" nor particularly elegant or efficient. The goal until now was to find a method that delivers "satisfactory" results. Optimizations and beautifications are possible.
The basic idea:
Given are the static rectangle r1, and the rectangle to be snapped, r0
Compute the edges that should be snapped together. This is divided in two steps:
The method computeCandidateEdgeIndices1 computes the "candidate edges" (resp. their indices) of the static rectangle that the moving rectangle may be snapped to. This is based on the folowing criterion: It checks how many vertices (corners) of the moving rectangle are right of the particular edge. For example, if all 4 vertices of the moving rectangle are right of edge 2, then edge 2 will be a candidate for snapping the rectangle to.
Since there may be multiple edges for which the same number of vertices may be "right", the method computeBestEdgeIndices computes the candidate edge whose center has the least distance to the center of any edge of the moving rectangle. The indices of the respective edges are returned
Given the indices of the edges to be snapped, the angle between these edges is computed. The resulting rectangle will be the original rectangle, rotated by this angle.
The rotated rectangle will be moved so that the centers of the snapped edges are at the same point
I tested this with several configurations, and the results at least seem "feasible" for me. Of course, this does not mean that it works satisfactory in all cases, but maybe it can serve as a starting point.
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;
import java.awt.geom.AffineTransform;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
public class RectangleSnap
{
public static void main(String[] args)
{
SwingUtilities.invokeLater(new Runnable()
{
#Override
public void run()
{
createAndShowGUI();
}
});
}
private static void createAndShowGUI()
{
JFrame f = new JFrame();
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
RectangleSnapPanel panel = new RectangleSnapPanel();
f.getContentPane().add(panel);
f.setSize(1000,1000);
f.setLocationRelativeTo(null);
f.setVisible(true);
}
}
class SnapRectangle
{
private Point2D position;
private double sizeX;
private double sizeY;
private double angleRad;
private AffineTransform at;
SnapRectangle(
double x, double y,
double sizeX, double sizeY, double angleRad)
{
this.position = new Point2D.Double(x,y);
this.sizeX = sizeX;
this.sizeY = sizeY;
this.angleRad = angleRad;
at = AffineTransform.getRotateInstance(
angleRad, position.getX(), position.getY());
}
double getAngleRad()
{
return angleRad;
}
double getSizeX()
{
return sizeX;
}
double getSizeY()
{
return sizeY;
}
Point2D getPosition()
{
return position;
}
void draw(Graphics2D g)
{
Color oldColor = g.getColor();
Rectangle2D r = new Rectangle2D.Double(
position.getX(), position.getY(), sizeX, sizeY);
AffineTransform at = AffineTransform.getRotateInstance(
angleRad, position.getX(), position.getY());
g.draw(at.createTransformedShape(r));
g.setColor(Color.RED);
for (int i=0; i<4; i++)
{
Point2D c = getCorner(i);
Ellipse2D e = new Ellipse2D.Double(c.getX()-3, c.getY()-3, 6, 6);
g.fill(e);
g.drawString(""+i, (int)c.getX(), (int)c.getY()+15);
}
g.setColor(Color.GREEN);
for (int i=0; i<4; i++)
{
Point2D c = getEdgeCenter(i);
Ellipse2D e = new Ellipse2D.Double(c.getX()-3, c.getY()-3, 6, 6);
g.fill(e);
g.drawString(""+i, (int)c.getX(), (int)c.getY()+15);
}
g.setColor(oldColor);
}
Point2D getCorner(int i)
{
switch (i)
{
case 0:
return new Point2D.Double(position.getX(), position.getY());
case 1:
{
Point2D.Double result = new Point2D.Double(
position.getX(), position.getY()+sizeY);
return at.transform(result, null);
}
case 2:
{
Point2D.Double result = new Point2D.Double
(position.getX()+sizeX, position.getY()+sizeY);
return at.transform(result, null);
}
case 3:
{
Point2D.Double result = new Point2D.Double(
position.getX()+sizeX, position.getY());
return at.transform(result, null);
}
}
return null;
}
Line2D getEdge(int i)
{
Point2D p0 = getCorner(i);
Point2D p1 = getCorner((i+1)%4);
return new Line2D.Double(p0, p1);
}
Point2D getEdgeCenter(int i)
{
Point2D p0 = getCorner(i);
Point2D p1 = getCorner((i+1)%4);
Point2D c = new Point2D.Double(
p0.getX() + 0.5 * (p1.getX() - p0.getX()),
p0.getY() + 0.5 * (p1.getY() - p0.getY()));
return c;
}
void setPosition(double x, double y)
{
this.position.setLocation(x, y);
at = AffineTransform.getRotateInstance(
angleRad, position.getX(), position.getY());
}
}
class RectangleSnapPanel extends JPanel implements MouseMotionListener
{
private final SnapRectangle rectangle0;
private final SnapRectangle rectangle1;
private SnapRectangle snappedRectangle0;
RectangleSnapPanel()
{
this.rectangle0 = new SnapRectangle(
200, 300, 250, 200, Math.toRadians(-21));
this.rectangle1 = new SnapRectangle(
500, 300, 200, 150, Math.toRadians(36));
addMouseMotionListener(this);
}
#Override
protected void paintComponent(Graphics gr)
{
super.paintComponent(gr);
Graphics2D g = (Graphics2D)gr;
g.setColor(Color.BLACK);
rectangle0.draw(g);
rectangle1.draw(g);
if (snappedRectangle0 != null)
{
g.setColor(Color.BLUE);
snappedRectangle0.draw(g);
}
}
#Override
public void mouseDragged(MouseEvent e)
{
rectangle0.setPosition(e.getX(), e.getY());
snappedRectangle0 = snapRects(rectangle0, rectangle1);
repaint();
}
#Override
public void mouseMoved(MouseEvent e)
{
}
private static SnapRectangle snapRects(
SnapRectangle r0, SnapRectangle r1)
{
List<Integer> candidateEdgeIndices1 =
computeCandidateEdgeIndices1(r0, r1);
int bestEdgeIndices[] = computeBestEdgeIndices(
r0, r1, candidateEdgeIndices1);
int bestEdgeIndex0 = bestEdgeIndices[0];
int bestEdgeIndex1 = bestEdgeIndices[1];
System.out.println("Best to snap "+bestEdgeIndex0+" to "+bestEdgeIndex1);
Line2D bestEdge0 = r0.getEdge(bestEdgeIndex0);
Line2D bestEdge1 = r1.getEdge(bestEdgeIndex1);
double edgeAngle = angleRad(bestEdge0, bestEdge1);
double rotationAngle = edgeAngle;
if (rotationAngle <= Math.PI)
{
rotationAngle = Math.PI + rotationAngle;
}
else if (rotationAngle <= -Math.PI / 2)
{
rotationAngle = Math.PI + rotationAngle;
}
else if (rotationAngle >= Math.PI)
{
rotationAngle = -Math.PI + rotationAngle;
}
SnapRectangle result = new SnapRectangle(
r0.getPosition().getX(), r0.getPosition().getY(),
r0.getSizeX(), r0.getSizeY(), r0.getAngleRad()-rotationAngle);
Point2D edgeCenter0 = result.getEdgeCenter(bestEdgeIndex0);
Point2D edgeCenter1 = r1.getEdgeCenter(bestEdgeIndex1);
double dx = edgeCenter1.getX() - edgeCenter0.getX();
double dy = edgeCenter1.getY() - edgeCenter0.getY();
result.setPosition(
r0.getPosition().getX()+dx,
r0.getPosition().getY()+dy);
return result;
}
// Compute for the edge indices for r1 in the given list
// the one that has the smallest distance to any edge
// of r0, and return this pair of indices
private static int[] computeBestEdgeIndices(
SnapRectangle r0, SnapRectangle r1,
List<Integer> candidateEdgeIndices1)
{
int bestEdgeIndex0 = -1;
int bestEdgeIndex1 = -1;
double minCenterDistance = Double.MAX_VALUE;
for (int i=0; i<candidateEdgeIndices1.size(); i++)
{
int edgeIndex1 = candidateEdgeIndices1.get(i);
for (int edgeIndex0=0; edgeIndex0<4; edgeIndex0++)
{
Point2D p0 = r0.getEdgeCenter(edgeIndex0);
Point2D p1 = r1.getEdgeCenter(edgeIndex1);
double distance = p0.distance(p1);
if (distance < minCenterDistance)
{
minCenterDistance = distance;
bestEdgeIndex0 = edgeIndex0;
bestEdgeIndex1 = edgeIndex1;
}
}
}
return new int[]{ bestEdgeIndex0, bestEdgeIndex1 };
}
// Compute the angle, in radians, between the given lines,
// in the range (-2*PI, 2*PI)
private static double angleRad(Line2D line0, Line2D line1)
{
double dx0 = line0.getX2() - line0.getX1();
double dy0 = line0.getY2() - line0.getY1();
double dx1 = line1.getX2() - line1.getX1();
double dy1 = line1.getY2() - line1.getY1();
double a0 = Math.atan2(dy0, dx0);
double a1 = Math.atan2(dy1, dx1);
return (a0 - a1) % (2 * Math.PI);
}
// In these methods, "right" refers to screen coordinates, which
// unfortunately are upside down in Swing. Mathematically,
// these relation is "left"
// Compute the "candidate" edges of r1 to which r0 may
// be snapped. These are the edges to which the maximum
// number of corners of r0 are right of
private static List<Integer> computeCandidateEdgeIndices1(
SnapRectangle r0, SnapRectangle r1)
{
List<Integer> bestEdgeIndices = new ArrayList<Integer>();
int maxRight = 0;
for (int i=0; i<4; i++)
{
Line2D e1 = r1.getEdge(i);
int right = countRightOf(e1, r0);
if (right > maxRight)
{
maxRight = right;
bestEdgeIndices.clear();
bestEdgeIndices.add(i);
}
else if (right == maxRight)
{
bestEdgeIndices.add(i);
}
}
//System.out.println("Candidate edges "+bestEdgeIndices);
return bestEdgeIndices;
}
// Count the number of corners of the given rectangle
// that are right of the given line
private static int countRightOf(Line2D line, SnapRectangle r)
{
int count = 0;
for (int i=0; i<4; i++)
{
if (isRightOf(line, r.getCorner(i)))
{
count++;
}
}
return count;
}
// Returns whether the given point is right of the given line
// (referring to the actual line *direction* - not in terms
// of coordinates in 2D!)
private static boolean isRightOf(Line2D line, Point2D point)
{
double d00 = line.getX1() - point.getX();
double d01 = line.getY1() - point.getY();
double d10 = line.getX2() - point.getX();
double d11 = line.getY2() - point.getY();
return d00 * d11 - d10 * d01 > 0;
}
}
I've been trying to rotate a polygon around a specified center point but everything I've tried has failed miserably. I've googled for example and found many but not a single one seems to work.
The result I'm trying to replicate is similar with the first answer to this
How to rotate an image gradually in Swing?
The difference is that I need the polygon to actually rotate, just drawing it in an angle won't cut it. (it's for simple physics modelling)
Here's my code, with several different methods I've tried to implement
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.Polygon;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.util.Timer;
import java.util.TimerTask;
import javax.swing.JPanel;
public class rotationPanel extends JPanel {
private static final int SIZE = 500;
private static final Shape outline = makeShape();
Point p;
Point p2;
Point p3;
Point p4;
Point[] points;
Point[] npoints;
Point center;
Polygon poly;
double angle;
Timer timer;
long start;
long sleepTime;
static int runTime;
public rotationPanel(){
setSize(500,500);
setBackground(Color.DARK_GRAY);
setVisible(true);
runTime = 100; //ms
start = 0;
sleepTime = 0;
timer = new Timer();
center = new Point(250,250);
p = new Point(200,200);
p2 = new Point(150,150);
p3 = new Point(250,150);
p4 = new Point(200,100);
/*
points = new Point[4];
points[0]=p;
points[1]=p2;
points[2]=p3;
points[3]=p4;
npoints = new Point[4];
npoints[0]=p;
npoints[1]=p2;
npoints[2]=p3;
npoints[3]=p4;
poly = new Polygon();
*/
}
public void mainloop(){
start= System.currentTimeMillis();
//rotate(points,2);
p = rotatePoint(p,center);
p2 = rotatePoint(p2,center);
p3 = rotatePoint(p3,center);
p4 = rotatePoint(p4,center);
repaint();
sleepTime = runTime -(System.currentTimeMillis()-start);
System.out.println("Looped. Sleeping for:" +sleepTime+"ms");
if(sleepTime>0)
timer.schedule(new loop(), sleepTime);
else
mainloop();
}
private static Shape makeShape() {
AffineTransform at = new AffineTransform();
at.translate(SIZE/2, SIZE/2);
at.scale(20, 20);
at.rotate(Math.toRadians(35));
return at.createTransformedShape(initPoly());
}
/** Create a U shaped outline. */
private static Polygon initPoly() {
Polygon poly = new Polygon();
poly.addPoint( 1, 0);
poly.addPoint( 1, -2);
poly.addPoint( 2, -2);
poly.addPoint( 2, 1);
poly.addPoint(-2, 1);
poly.addPoint(-2, -2);
poly.addPoint(-1, -2);
poly.addPoint(-1, 0);
return poly;
}
public void rotatePoint(Point pt, double rotationAngle){
AffineTransform.getRotateInstance
(Math.toRadians(rotationAngle), center.x, center.y)
.transform(pt,pt);
}
public Point rotatePoint(Point pt, Point center)
{
angle = (Math.toRadians(150));
double cosAngle = Math.cos(angle);
double sinAngle = Math.sin(angle);
pt.x = center.x + (int) ((pt.x-center.x)*cosAngle-(pt.y-center.y)*sinAngle);
pt.y = center.y + (int) ((pt.x-center.x)*sinAngle+(pt.y-center.y)*cosAngle);
return pt;
}
public void rotate(Point[] pts, int angle){
AffineTransform.getRotateInstance
(Math.toRadians(angle), center.x, center.y)
.transform(pts,0,npoints,0,4);
points = new Point[4];
points[0]=npoints[0];
points[1]=npoints[1];
points[2]=npoints[2];
points[3]=npoints[3];
}
public void paintComponent(Graphics g) {
super.paintComponent(g);
g.setColor(Color.BLUE);
g.fillRect(center.x-4, center.y-4, 8, 8);
g.setColor(Color.YELLOW);
//g.fillRect(p.x-4, p.y-4, 8, 8);
//g.fillRect(p2.x-4, p2.y-4, 8, 8);
//g.fillRect(p3.x-4, p3.y-4, 8, 8);
//g.fillRect(p4.x-4, p4.y-4, 8, 8);
g.fillRect(p.x, p.y, 2, 2);
g.fillRect(p2.x, p2.y, 2, 2);
g.fillRect(p3.x, p3.y, 2, 2);
g.fillRect(p4.x, p4.y, 2, 2);
}
class loop extends TimerTask{
public void run() {
mainloop();
}
}
}
As you didn't help me vary much I was forced to figure this out by myself. Here we go:
The correct approach (or at least one of them) is to use affine transform to the points of the polygon you wish to rotate. The catch is that you cannot rotate the same polygon over and over again as it will severely deform due to the continuous rounding.
So the trick is to keep " an original version" of the polygon and always rotate that one.
Of course, this approach is only critical when rotating the polygon several times. If you want to only rotate it once you can simply use the values from the polygon you want to rotate.
Here's a little example I managed to put together:
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Polygon;
import java.awt.RenderingHints;
import java.awt.geom.AffineTransform;
import java.util.Timer;
import java.util.TimerTask;
import javax.swing.JFrame;
import javax.swing.JPanel;
public class rotationPanel extends JPanel {
private static final long serialVersionUID = 117L;
private static final int SIZE = 500;
// point arrays which contain the points that are rotated around the center
Point[] points1;
Point[] points2;
Point[] points3;
// The center of rotation
Point center;
// the polygons being rotated
Polygon poly1;
Polygon poly2;
Polygon poly3;
// the angle of rotation
double angle;
Timer timer;
long start;
long sleepTime;
static int runTime;
public rotationPanel(){
setSize(500,500);
setBackground(Color.DARK_GRAY);
setVisible(true);
// time loop is set to run at fixed rate of 50 ms
runTime = 50;
start = 0;
sleepTime = 0;
timer = new Timer();
angle = 0;
// initializing the arrays (not neccesary)
points1 = getOriginalPoints(1);
points3 = getOriginalPoints(3);
points2 = getOriginalPoints(2);
// setting the rotation to the middle of the screen
center = new Point(250,250);
// start the looping
mainloop();
}
public void mainloop(){
start= System.currentTimeMillis();
// rotate the points the spcified angle and store the rotated
//points to the correct array
rotatePointMatrix(getOriginalPoints(1),angle,points1);
rotatePointMatrix(getOriginalPoints(2),angle,points2);
rotatePointMatrix(getOriginalPoints(3),angle,points3);
// Make the points into a polygon
poly1 = polygonize(points1);
poly2 = polygonize(points2);
poly3 = polygonize(points3);
// increase the angle by one degree, resulting to rotation in the longer run
angle++;
if (angle>=360){
angle=0;
}
// restatring the sequence
repaint();
sleepTime = runTime -(System.currentTimeMillis()-start);
System.out.println("Looped. Sleeping for:" +sleepTime+"ms");
if(sleepTime>0)
timer.schedule(new loop(), sleepTime);
else
mainloop();
}
public void rotatePointMatrix(Point[] origPoints, double angle, Point[] storeTo){
/* We ge the original points of the polygon we wish to rotate
* and rotate them with affine transform to the given angle.
* After the opeariont is complete the points are stored to the
* array given to the method.
*/
AffineTransform.getRotateInstance
(Math.toRadians(angle), center.x, center.y)
.transform(origPoints,0,storeTo,0,5);
}
public Polygon polygonize(Point[] polyPoints){
//a simple method that makes a new polygon out of the rotated points
Polygon tempPoly = new Polygon();
for(int i=0; i < polyPoints.length; i++){
tempPoly.addPoint(polyPoints[i].x, polyPoints[i].y);
}
return tempPoly;
}
public Point[] getOriginalPoints(int type){
/* In this example the rotated "polygon" are stored in this method.
* The Point is that if we want to rotate a polygon constatnly/frequently
* we cannot use the values of an already rotated polygon as this will
* lead to the polygon deforming severely after few translations due
* to the points being constantly rounded. So the trick is to save the
* original Points of the polygon and always rotate that one to the new
* angle instead of rotating the same one again and again.
*/
Point[] originalPoints = new Point[5];
if(type == 2){
originalPoints[0]= new Point(200, 100);
originalPoints[1]= new Point(250, 50);
originalPoints[2]= new Point(300, 100);
originalPoints[3]= new Point(300, 400);
originalPoints[4]= new Point(200, 400);
}
else if(type == 1){
originalPoints[0]= new Point(210, 150);
originalPoints[1]= new Point(250, 150);
originalPoints[2]= new Point(250, 190);
originalPoints[3]= new Point(230, 220);
originalPoints[4]= new Point(210, 190);
}
else{
originalPoints[0]= new Point(250, 300);
originalPoints[1]= new Point(290, 300);
originalPoints[2]= new Point(290, 340);
originalPoints[3]= new Point(270, 370);
originalPoints[4]= new Point(250, 340);
}
return originalPoints;
}
public void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g;
g2d.setRenderingHint(
RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setColor(Color.GRAY);
g2d.fillPolygon(poly2);
g2d.setColor(Color.yellow);
g2d.fillPolygon(poly1);
g2d.setColor(Color.yellow);
g2d.fillPolygon(poly3);
g2d.setColor(Color.WHITE);
for(int i=0; i < points1.length; i++){
g2d.fillRect(points1[i].x-1, points1[i].y-1, 3, 3);
g2d.fillRect(points3[i].x-1, points3[i].y-1, 3, 3);
}
g2d.setColor(Color.BLUE);
g2d.fillOval(center.x-4, center.y-4, 8, 8);
g2d.setColor(Color.yellow);
g2d.drawString("Angle: "+angle, 10,450);
}
class loop extends TimerTask{
public void run() {
mainloop();
}
}
public static void main(String[] args){
JFrame f = new JFrame();
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.add(new rotationPanel());
f.setSize(500,500);
f.setVisible(true);
}
}
I hope this helps! Don't hesitate to contact me if you run into trouble!
Here is a simple method to build a polygon from a set of points, rotated around a center point, at a specified angle:
/**
* Builds a polygon from a set of points, rotated around a point, at the
* specified rotation angle.
*
* #param centerX the int center x coordinate around which to rotate
* #param centerY the int center y coordinate around which to rotate
* #param xp the int[] of x points which make up our polygon points. This
* array is parallel to the yp array where each index in this array
* corresponds to the same index in the yp array.
* #param yp the int[] of y points which make up our polygon points. This
* array is parallel to the xp array where each index in this array
* corresponds to the same index in the xp array.
* #param rotationAngle the double angle in which to rotate the provided
* coordinates (specified in degrees).
* #return a Polygon of the provided coordinates rotated around the center point
* at the specified angle.
* #throws IllegalArgumentException when the provided x points array is not the
* same length as the provided y points array
*/
private Polygon buildPolygon(int centerX, int centerY, int[] xp, int[] yp, double rotationAngle) throws IllegalArgumentException {
// copy the arrays so that we dont manipulate the originals, that way we can
// reuse them if necessary
int[] xpoints = Arrays.copyOf(xp,xp.length);
int[] ypoints = Arrays.copyOf(yp,yp.length);
if(xpoints.length != ypoints.length){
throw new IllegalArgumentException("The provided x points are not the same length as the provided y points.");
}
// create a list of Point2D pairs
ArrayList<Point2D> list = new ArrayList();
for(int i = 0; i < ypoints.length; i++){
list.add(new Point2D.Double(xpoints[i], ypoints[i]));
}
// create an array which will hold the rotated points
Point2D[] rotatedPoints = new Point2D[list.size()];
// rotate the points
AffineTransform transform = AffineTransform.getRotateInstance(Math.toRadians(rotationAngle), centerX, centerY);
transform.transform(list.toArray(new Point2D[0]), 0, rotatedPoints, 0, rotatedPoints.length);
// build the polygon from the rotated points and return it
int[] ixp = new int[list.size()];
int[] iyp = new int[list.size()];
for(int i = 0; i < ixp.length; i++){
ixp[i] = (int)rotatedPoints[i].getX();
iyp[i] = (int)rotatedPoints[i].getY();
}
return new Polygon(ixp, iyp, ixp.length);
}