Connect two circles with a line - java

I'm drawing two shapes (circles) in a JPanel and I need to connect them with a line. I was doing this by just getting the middle point of the circle and connecting each other, easy.
The problem is that now I need to make single-direction lines, which has an "arrow" at the end, to point out which direction the line goes. So now I can't use the middle point of the circle because I need to connect each other from border to border, so the "arrow' can appear correctly.
On my last try that was the result, nothing good:
PS: In the screenshot I'm not filling the circles just to see the exact position of the line, but normally I would fill it.
I'm having trouble to calculate the exact position of the border I need to start/end my line. Anyone has any idea on how to do this?
EDIT: The circles are movable, they could be in any position, so the line should work in any case.

Okay, so basically, we can break down the problem to basic issues:
Get the angle between the two circles
Draw a line from circumference of one circle to another along this angle
Both these issues aren't hard to solve (and any time spent searching the internet would provide solutions - because that's where I got them from ;))
So, the angle between two points could be calculated using something like...
protected double angleBetween(Point2D from, Point2D to) {
double x = from.getX();
double y = from.getY();
// This is the difference between the anchor point
// and the mouse. Its important that this is done
// within the local coordinate space of the component,
// this means either the MouseMotionListener needs to
// be registered to the component itself (preferably)
// or the mouse coordinates need to be converted into
// local coordinate space
double deltaX = to.getX() - x;
double deltaY = to.getY() - y;
// Calculate the angle...
// This is our "0" or start angle..
double rotation = -Math.atan2(deltaX, deltaY);
rotation = Math.toRadians(Math.toDegrees(rotation) + 180);
return rotation;
}
And the point on a circle can be calculated using something like...
protected Point2D getPointOnCircle(Point2D center, double radians, double radius) {
double x = center.getX();
double y = center.getY();
radians = radians - Math.toRadians(90.0); // 0 becomes the top
// Calculate the outter point of the line
double xPosy = Math.round((float) (x + Math.cos(radians) * radius));
double yPosy = Math.round((float) (y + Math.sin(radians) * radius));
return new Point2D.Double(xPosy, yPosy);
}
Just beware, there's some internal modifications of the results to allow for the difference between the mathematical solution and the way that the Graphics API draws circles
Okay, so big deal you say, how does that help me? Well, I great deal actually.
You'd calculate the angle between the to circles (both to and from, you might be able to simple inverse one angle, but I have the calculation available so I used it). From that, you can calculate the point on each circle where the line will intersect and then you simply need to draw it, something like...
double from = angleBetween(circle1, circle2);
double to = angleBetween(circle2, circle1);
Point2D pointFrom = getPointOnCircle(circle1, from);
Point2D pointTo = getPointOnCircle(circle2, to);
Line2D line = new Line2D.Double(pointFrom, pointTo);
g2d.draw(line);
Runnable Example
Because I've distilled much of the calculations down to communalised properties, I've provided my test code as a runnable example. All the calculations are based on dynamic values, nothing is really hard coded. For example, you can change the size and positions of the circles and the calculations should continue to work...
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
public class Test {
public static void main(String[] args) {
new Test();
}
public Test() {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
ex.printStackTrace();
}
JFrame frame = new JFrame("Testing");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(new TestPane());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class TestPane extends JPanel {
private Ellipse2D circle1;
private Ellipse2D circle2;
private Point2D drawTo;
public TestPane() {
circle1 = new Ellipse2D.Double(10, 10, 40, 40);
circle2 = new Ellipse2D.Double(100, 150, 40, 40);
//addMouseMotionListener(new MouseAdapter() {
// #Override
// public void mouseMoved(MouseEvent e) {
// drawTo = new Point2D.Double(e.getPoint().x, e.getPoint().y);
// repaint();
// }
//});
}
protected Point2D center(Rectangle2D bounds) {
return new Point2D.Double(bounds.getCenterX(), bounds.getCenterY());
}
protected double angleBetween(Shape from, Shape to) {
return angleBetween(center(from.getBounds2D()), center(to.getBounds2D()));
}
protected double angleBetween(Point2D from, Point2D to) {
double x = from.getX();
double y = from.getY();
// This is the difference between the anchor point
// and the mouse. Its important that this is done
// within the local coordinate space of the component,
// this means either the MouseMotionListener needs to
// be registered to the component itself (preferably)
// or the mouse coordinates need to be converted into
// local coordinate space
double deltaX = to.getX() - x;
double deltaY = to.getY() - y;
// Calculate the angle...
// This is our "0" or start angle..
double rotation = -Math.atan2(deltaX, deltaY);
rotation = Math.toRadians(Math.toDegrees(rotation) + 180);
return rotation;
}
protected Point2D getPointOnCircle(Shape shape, double radians) {
Rectangle2D bounds = shape.getBounds();
// Point2D point = new Point2D.Double(bounds.getX(), bounds.getY());
Point2D point = center(bounds);
return getPointOnCircle(point, radians, Math.max(bounds.getWidth(), bounds.getHeight()) / 2d);
}
protected Point2D getPointOnCircle(Point2D center, double radians, double radius) {
double x = center.getX();
double y = center.getY();
radians = radians - Math.toRadians(90.0); // 0 becomes th?e top
// Calculate the outter point of the line
double xPosy = Math.round((float) (x + Math.cos(radians) * radius));
double yPosy = Math.round((float) (y + Math.sin(radians) * radius));
return new Point2D.Double(xPosy, yPosy);
}
#Override
public Dimension getPreferredSize() {
return new Dimension(200, 200);
}
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
g2d.draw(circle1);
g2d.draw(circle2);
// This was used for testing, it will draw a line from circle1 to the
// drawTo point, which, if enabled, is the last known position of the
// mouse
//if (drawTo != null) {
// Point2D pointFrom = center(circle1.getBounds2D());
// g2d.setColor(Color.RED);
// g2d.draw(new Line2D.Double(drawTo, pointFrom));
//
// double from = angleBetween(pointFrom, drawTo);
// System.out.println(NumberFormat.getNumberInstance().format(Math.toDegrees(from)));
//
// Point2D poc = getPointOnCircle(circle1, from);
// g2d.setColor(Color.BLUE);
// g2d.draw(new Line2D.Double(poc, drawTo));
//}
double from = angleBetween(circle1, circle2);
double to = angleBetween(circle2, circle1);
Point2D pointFrom = getPointOnCircle(circle1, from);
Point2D pointTo = getPointOnCircle(circle2, to);
g2d.setColor(Color.RED);
Line2D line = new Line2D.Double(pointFrom, pointTo);
g2d.draw(line);
g2d.dispose();
}
}
}
Arrow head
The intention is to treat the arrow head as a separate entity. The reason is because it's just simpler that way, you also get a more consistent result regardless of the distance between the objects.
So, to start with, I define a new Shape...
public class ArrowHead extends Path2D.Double {
public ArrowHead() {
int size = 10;
moveTo(0, size);
lineTo(size / 2, 0);
lineTo(size, size);
}
}
Pretty simple really. It just creates two lines, which point up, meeting in the middle of the available space.
Then in the paintComponent method, we perform some AffineTransform magic using the available information we already have, namely
The point on our target circles circumference
The angle to our target circle
And transform the ArrowHead shape...
g2d.setColor(Color.MAGENTA);
ArrowHead arrowHead = new ArrowHead();
AffineTransform at = AffineTransform.getTranslateInstance(
pointTo.getX() - (arrowHead.getBounds2D().getWidth() / 2d),
pointTo.getY());
at.rotate(from, arrowHead.getBounds2D().getCenterX(), 0);
arrowHead.transform(at);
g2d.draw(arrowHead);
Now, because I'm crazy, I also tested the code by drawing an arrow pointing at our source circle, just to prove that the calculations would work...
// This just proofs that the previous calculations weren't a fluke
// and that the arrow can be painted pointing to the source object as well
g2d.setColor(Color.GREEN);
arrowHead = new ArrowHead();
at = AffineTransform.getTranslateInstance(
pointFrom.getX() - (arrowHead.getBounds2D().getWidth() / 2d),
pointFrom.getY());
at.rotate(to, arrowHead.getBounds2D().getCenterX(), 0);
arrowHead.transform(at);
g2d.draw(arrowHead);

Let the first circle center coordinates are AX, AY, radius AR, and BX, BY, BR for the second circle.
Difference vector
D = (DX, DY) = (BX - AX, BY - AY)
Normalized
d = (dx, dy) = (DX / Length(D), DY / Length(D))
Start point of arrow
S = (sx, sy) = (AX + dx * AR, AY + dy * AR)
End point
E = (ex, ey) = (BX - dx * BR, BY - dy * BR)
Example:
AX = 0 AY = 0 AR = 1
BX = 4 BY = 3 BR = 2
D = (4, 3)
Length(D) = 5
dx = 4/5
dy = 3/5
sx = 0.8 sy = 0.6
ex = 4 - 2 * 4/5 = 12/5 = 2.4
ey = 3 - 2 * 3/5 = 9/5 = 1.8

Looking at the Screenshot, I think you need to find the top right corner of circle A, and then add half of the total distance to the bottom to y. Next, find the top right corner of circle B, and add half of the distance to the top left corner to x. Finally, make a line connecting the two, and render an arrow on the end of it.
Like this:
private int x1, y1, x2, y2 width = 20, height = 20;
private void example(Graphics g) {
// Set x1, x2, y1, and y2 to something
g.drawOval(x1, y1, width, height);
g.drawOval(x2, y2, width, height);
g.drawLine(x1, y1 + (height/2), x2 + (width/2), y2);
g.drawImage(/*Image of an arrow*/, (x2 + width/2)-2, y2);
}

My trick:
Let the two centers be C0 and C1. Using complex numbers, you map these two points to a horizontal segment from the origin by the transformation
P' = (P - C0) (C1 - C0)* / L
where * denotes conjugation and L = |C1 - C0|. (If you don't like the complex number notation, you can express this with matrices as well.)
Now the visible part of the segment goes from (R0, 0) to (L - R1, 0). The two other vertices of the arrow are at (L - R1 - H, W) and (L - R1 - H, -W) for an arrowhead of height H and width 2W.
By applying the inverse transform you get the original coordinates,
P = C0 + L P' / (C1 - C0)*.

Related

Swing translate scale change order misplacement

I've found something weird when splitting a translate operation around a scaling one with Java Swing. Maybe I'm doing something stupid but I'm not sure where.
In the first version I center the image, scale it and then translate it to the desired position.
In the second version I directly scale the image and then translate to the desired position compensating for having a non centered image.
The two solutions should be equivalent. Also this is important when considering rotations around a point and motion in another.. I've code that does that too... but why this does not work?
Here are the two versions of the code. They are supposed to do the exact same thing but they are not. Here are the screenshots:
First produces: screenshot1
Second produces: screenshot2
I think that the two translation operations in draw1 surrounding the scale operation should be equivalent to the scale translate operation in draw2.
Any suggestion?
MCVE:
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import java.awt.image.*;
import javax.imageio.ImageIO;
import javax.swing.*;
import java.net.URL;
public class Asteroid extends JComponent implements ActionListener {
public static final Dimension FRAME_SIZE = new Dimension(640, 480);
public double x = 200;
public double y = 200;
public int radius = 40;
private AffineTransform bgTransfo;
private final BufferedImage im2;
private JCheckBox draw1Check = new JCheckBox("Draw 1", true);
Asteroid() {
BufferedImage img = null;
try {
img = ImageIO.read(new URL("https://i.stack.imgur.com/CWJdo.png"));
} catch (Exception e) {
e.printStackTrace();
}
im2 = img;
initUI();
}
private final void initUI() {
draw1Check.addActionListener(this);
JFrame frame = new JFrame("FrameDemo");
frame.add(BorderLayout.CENTER, this);
frame.add(BorderLayout.PAGE_START, draw1Check);
frame.pack();
frame.setVisible(true);
frame.setDefaultCloseOperation(frame.EXIT_ON_CLOSE);
}
public static void main(String[] args) {
Asteroid asteroid = new Asteroid();
}
#Override
public Dimension getPreferredSize() {
return FRAME_SIZE;
}
#Override
public void paintComponent(Graphics g0) {
Graphics2D g = (Graphics2D) g0;
g.setColor(Color.white);
g.fillRect(0, 0, 640, 480);
if (draw1Check.isSelected()) {
draw1(g);
} else {
draw2(g);
}
}
public void draw1(Graphics2D g) {//Draw method - draws asteroid
double imWidth = im2.getWidth();
double imHeight = im2.getHeight();
double stretchx = (2.0 * radius) / imWidth;
double stretchy = (2.0 * radius) / imHeight;
bgTransfo = new AffineTransform();
//centering
bgTransfo.translate(-imWidth / 2.0, -imHeight / 2.0);
//scaling
bgTransfo.scale(stretchx, stretchy);
//translation
bgTransfo.translate(x / stretchx, y / stretchy);
//draw correct position
g.setColor(Color.CYAN);
g.fillOval((int) (x - radius), (int) (y - radius), (int) (2 * radius), (int) (2 * radius));
//draw sprite
g.drawImage(im2, bgTransfo, this);
}
public void draw2(Graphics2D g) {//Draw method - draws asteroid
double imWidth = im2.getWidth();
double imHeight = im2.getHeight();
double stretchx = (2.0 * radius) / imWidth;
double stretchy = (2.0 * radius) / imHeight;
bgTransfo = new AffineTransform();
//scale
bgTransfo.scale(stretchx, stretchy);
//translate and center
bgTransfo.translate((x - radius) / stretchx, (y - radius) / stretchy);
//draw correct position
g.setColor(Color.CYAN);
g.fillOval((int) (x - radius), (int) (y - radius), (int) (2 * radius), (int) (2 * radius));
//draw sprite
g.drawImage(im2, bgTransfo, this);
}
#Override
public void actionPerformed(ActionEvent e) {
repaint();
}
}
Not sure if this question is still really open. Anyway here is my answer.
I think the crucial part to understand this behavior is the difference between AffineTransform.concatenate and AffineTransform.preConcatenate methods. The thing is that resulting transformation depends on the order the sub-transformations are applied.
To quote the concatenate JavaDoc
Concatenates an AffineTransform Tx to this AffineTransform Cx in the most commonly useful way to provide a new user space that is mapped to the former user space by Tx. Cx is updated to perform the combined transformation. Transforming a point p by the updated transform Cx' is equivalent to first transforming p by Tx and then transforming the result by the original transform Cx like this: Cx'(p) = Cx(Tx(p))
compare this with preConcatenate:
Concatenates an AffineTransform Tx to this AffineTransform Cx in a less commonly used way such that Tx modifies the coordinate transformation relative to the absolute pixel space rather than relative to the existing user space. Cx is updated to perform the combined transformation. Transforming a point p by the updated transform Cx' is equivalent to first transforming p by the original transform Cx and then transforming the result by Tx like this: Cx'(p) = Tx(Cx(p))
The scale and translate methods are effectively concatenate. Lets call 3 transformations in your draw1 method C (center), S (scale), and T (translate). So your compound transformation is effectively C(S(T(p))). Particularly it means that S is applied to the T but not to the C so your C does not really center the image. A simple fix would be to change the order of S and C but I think that a more proper fix would be something like this:
public void draw3(Graphics2D g) {
//Draw method - draws asteroid
double imWidth = im2.getWidth();
double imHeight = im2.getHeight();
double stretchx = (2.0 * radius) / imWidth;
double stretchy = (2.0 * radius) / imHeight;
AffineTransform bgTransfo = new AffineTransform();
//translation
bgTransfo.translate(x, y);
//scaling
bgTransfo.scale(stretchx, stretchy);
//centering
bgTransfo.translate(-imWidth / 2.0, -imHeight / 2.0);
//draw correct position
g.setColor(Color.CYAN);
g.fillOval((int) (x - radius), (int) (y - radius), (int) (2 * radius), (int) (2 * radius));
//draw sprite
g.drawImage(im2, bgTransfo, this);
}
I think the big advantage of this method is that you don't have to re-calculate the T using stretchx/stretchy

How to rotate an object around another moving object in java?

I'm quite new to Java and want to program an easy sun system where the moon rotates around the earth and the earth around the sun.
Everything works well except the moon doesn't want to move correctly :/
Because the earth diverges from the moon's initial position, the rotation radius of the moon grows accordingly to that distance. And again when the earth gets closer to the moons inertial position, the rotation radius decreases.
If the initial position is (0;0), it works but the moon hits the sun...
So how can I keep the distance between earth and moon constant?
I'm using AffineTransforms and here is a snippet of my code ;)
Thanks in advance!
Ellipse2D.Double MoonFrame = new Ellipse2D.Double(orbitEarth + orbitMoon - radiusMoon, -radiusMoon, radiusMoon*2, radiusMoon*2);
for (int i = 0; i < 360; i++)
{
theta += Math.PI/30;
AffineTransform TransformMoon = AffineTransform.getRotateInstance(theta,TransformEarth.getTranslateX(),TransformEarth.getTranslateY());
g2d.fill(TransformMond.createTransformedShape(MoonFrame));
}
So, your basic question comes down to "how do I find a point on a circle for a give angle" ... seriously, it's that simple
Based on many hours of googling and trial and error, I basically use the following, more or less.
protected Point pointOnCircle() {
double rads = Math.toRadians(orbitAngle - 180); // Make 0 point out to the right...
int fullLength = Math.round((outterRadius));
// Calculate the outter point of the line
int xPosy = Math.round((float) (Math.cos(rads) * fullLength));
int yPosy = Math.round((float) (Math.sin(rads) * fullLength));
return new Point(xPosy, yPosy);
}
The rest basically comes down to properly handling the compounding nature of transformations,
Basically, this takes a base Graphics context, applies the translation to it (the Earth's position) and creates two other contexts off it to apply additional transformations, one for the Earth and one for the moon...
Graphics2D g2d = (Graphics2D) g.create();
int yPos = (getHeight() - size) / 2;
// Transform the offset
g2d.transform(AffineTransform.getTranslateInstance(xPos, yPos));
Graphics2D earthG = (Graphics2D) g2d.create();
// Rotate around the 0x0 point, this becomes the center point
earthG.transform(AffineTransform.getRotateInstance(Math.toRadians(angle)));
// Draw the "earth" around the center point
earthG.drawRect(-(size / 2), -(size / 2), size, size);
earthG.dispose();
// Removes the last transformation
Graphics2D moonG = (Graphics2D) g2d.create();
// Calclate the point on the circle - based on the outterRadius or
// distance from the center point of the earth
Point poc = pointOnCircle();
int moonSize = size / 2;
// This is only a visial guide used to show the position of the earth
//moonG.drawOval(-outterRadius, -outterRadius, outterRadius * 2, outterRadius * 2);
moonG.fillOval(poc.x - (moonSize / 2), poc.y - (moonSize / 2), moonSize, moonSize);
moonG.dispose();
g2d.dispose();
And because I know how much that would have you scratching your head, a runnable example...
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.AffineTransform;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
public class Test {
public static void main(String[] args) {
new Test();
}
public Test() {
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
JFrame frame = new JFrame();
frame.add(new TestPane());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class TestPane extends JPanel {
private double angle;
private double orbitAngle;
private int xPos = 0;
private int size = 20;
private int outterRadius = size * 2;
private int delta = 2;
public TestPane() {
new Timer(40, new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
xPos += delta;
if (xPos + size >= getWidth()) {
xPos = getWidth() - size;
delta *= -1;
} else if (xPos < 0) {
xPos = 0;
delta *= -1;
}
angle += 4;
orbitAngle -= 2;
repaint();
}
}).start();
}
#Override
public Dimension getPreferredSize() {
return new Dimension(400, 200);
}
protected Point pointOnCircle() {
double rads = Math.toRadians(orbitAngle - 180); // Make 0 point out to the right...
int fullLength = Math.round((outterRadius));
// Calculate the outter point of the line
int xPosy = Math.round((float) (Math.cos(rads) * fullLength));
int yPosy = Math.round((float) (Math.sin(rads) * fullLength));
return new Point(xPosy, yPosy);
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
int yPos = (getHeight() - size) / 2;
// Transform the offset
g2d.transform(AffineTransform.getTranslateInstance(xPos, yPos));
Graphics2D earthG = (Graphics2D) g2d.create();
// Rotate around the 0x0 point, this becomes the center point
earthG.transform(AffineTransform.getRotateInstance(Math.toRadians(angle)));
// Draw the "earth" around the center point
earthG.drawRect(-(size / 2), -(size / 2), size, size);
earthG.dispose();
// Removes the last transformation
Graphics2D moonG = (Graphics2D) g2d.create();
// Calclate the point on the circle - based on the outterRadius or
// distance from the center point of the earth
Point poc = pointOnCircle();
int moonSize = size / 2;
// This is only a visial guide used to show the position of the earth
//moonG.drawOval(-outterRadius, -outterRadius, outterRadius * 2, outterRadius * 2);
moonG.fillOval(poc.x - (moonSize / 2), poc.y - (moonSize / 2), moonSize, moonSize);
moonG.dispose();
g2d.dispose();
}
}
}
This moves a "Earth" object, which is rotating in one direction and then rotates the moon around it, in the opposite direction
You can simplify your math by concatenating transforms. Work backwards from the last transform to the first, or use preConcatenate to build them in a more natural order.
Compose complex transforms from simple transforms, for example by building an orbital transform from a translate and a rotate:
// Earth transform.
// Set the orbital radius to 1/3rd the panel width
AffineTransform earthTx = AffineTransform.getTranslateInstance(getWidth() / 3, 0);
// Rotate
earthTx.preConcatenate(AffineTransform.getRotateInstance(angle));
Later transforms (e.g. the moon orbiting the earth) can then be built on top of earlier results:
// Moon transform.
// Set the orbital radius to 1/10th the panel width
AffineTransform moonTx = AffineTransform.getTranslateInstance(getWidth() / 10, 0);
// Rotate
moonTx.preConcatenate(AffineTransform.getRotateInstance(angle));
// Add the earth transform
moonTx.preConcatenate(earthTx);
Full example:
public class Orbit {
public static class OrbitPanel extends JComponent {
int width;
int height;
public OrbitPanel(int width, int height) {
this.width = width;
this.height = height;
}
#Override
public Dimension getPreferredSize() {
return new Dimension(width, height);
}
#Override
public void paint(Graphics g) {
Graphics2D g2 = (Graphics2D) g;
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
// Clear the background.
g2.setColor(getBackground());
g2.fillRect(0, 0, getWidth(), getHeight());
// Sun transform. Just centre it in the window.
AffineTransform sunTx = AffineTransform.getTranslateInstance(getWidth() / 2, getHeight() / 2);
// Draw the sun
g2.setTransform(sunTx);
drawBody(g2, 30, Color.YELLOW);
// Orbital period.
// One rotation every 10s.
double percentRotation = System.currentTimeMillis() % 10000 / 10000.0;
// To radians.
double angle = Math.PI * 2 * percentRotation;
// Earth transform.
// Set the orbital radius to 1/3rd the panel width
AffineTransform earthTx = AffineTransform.getTranslateInstance(getWidth() / 3, 0);
// Rotate
earthTx.preConcatenate(AffineTransform.getRotateInstance(angle));
// Add the sun transform
earthTx.preConcatenate(sunTx);
// Draw the earth
g2.setTransform(earthTx);
drawBody(g2, 10, Color.BLUE);
// Moon transform.
// Set the orbital radius to 1/10th the panel width
AffineTransform moonTx = AffineTransform.getTranslateInstance(getWidth() / 10, 0);
// Rotate
moonTx.preConcatenate(AffineTransform.getRotateInstance(angle));
// Add the earth transform (already includes the sun transform)
moonTx.preConcatenate(earthTx);
// Draw the moon
g2.setTransform(moonTx);
drawBody(g2, 5, Color.DARK_GRAY);
}
private void drawBody(Graphics2D g2, int size, Color color) {
g2.setColor(color);
g2.fillOval(-size / 2, -size / 2, size, size);
}
}
public static void main(String[] args) throws IOException, InterruptedException {
JFrame frame = new JFrame("Orbit");
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
JComponent orbitPanel = new OrbitPanel(250, 250);
frame.add(orbitPanel);
frame.pack();
frame.setVisible(true);
while (true) {
Thread.sleep(20);
orbitPanel.repaint();
}
}
}

How to use AffineTransform with very little coordinates?

I have a set of two dimensions points. Their X and Y are greater than -2 and lesser than 2. Such point could be : (-0.00012 ; 1.2334 ).
I would want to display these points on a graph, using rectangles (a rectangle illustrates a point, and has its coordinates set to its point's ones - moreover, it has a size of 10*10).
Rectangles like (... ; Y) should be displayed above any rectangles like (... ; Y-1) (positive Y direction is up). Thus, I must set the graph's origin not at the top-left hand-corner, but somewhere else.
I'm trying to use Graphics2D's AffineTransform to do that.
I get the minimal value for all the X coordinates
I get the minimal value for all the Y coordinates
I get the maximal value for all the X coordinates
I get the maximal value for all the Y coordinates
I get the distance xmax-xmin and ymax-ymin
Then, I wrote the code I give you below.
Screenshots
Some days ago, using my own method to scale, I had this graph:
(so as I explained, Y are inverted and that's not a good thing)
For the moment, i.e., with the code I give you below, I have only one point that takes all the graph's place! Not good at all.
I would want to have:
(without lines, and without graph's axis. The important here is that points are correctly displayed, according to their coordinates).
Code
To get min and max coordinates value:
x_min = Double.parseDouble((String) list_all_points.get(0).get(0));
x_max = Double.parseDouble((String) list_all_points.get(0).get(0));
y_min = Double.parseDouble((String) list_all_points.get(0).get(1));
y_max = Double.parseDouble((String) list_all_points.get(0).get(1));
for(StorableData s : list_all_points) {
if(Double.parseDouble((String) s.get(0)) < x_min) {
x_min = Double.parseDouble((String) s.get(0));
}
if(Double.parseDouble((String) s.get(0)) > x_max) {
x_max = Double.parseDouble((String) s.get(0));
}
if(Double.parseDouble((String) s.get(1)) < y_min) {
y_min = Double.parseDouble((String) s.get(1));
}
if(Double.parseDouble((String) s.get(1)) > y_max) {
y_max = Double.parseDouble((String) s.get(1));
}
}
To draw a point:
int x, y;
private void drawPoint(Cupple storable_data) {
//x = (int) (storable_data.getNumber(0) * scaling_coef + move_x);
//y = (int) (storable_data.getNumber(1) * scaling_coef + move_y);
x = storable_data.getNumber(0).intValue();
y = storable_data.getNumber(1).intValue();
graphics.fillRect(x, y, 10, 10);
graphics.drawString(storable_data.toString(), x - 5, y - 5);
}
To paint the graph:
#Override
public void paint(Graphics graphics) {
this.graphics = graphics;
Graphics2D graphics_2d = ((Graphics2D) this.graphics);
AffineTransform affine_transform = graphics_2d.getTransform();
affine_transform.scale(getWidth()/(x_max - x_min), getHeight()/(y_max - y_min));
affine_transform.translate(x_min, y_min);
graphics_2d.transform(affine_transform);
for(StorableData storable_data : list_all_points) {
graphics_2d.setColor(Color.WHITE);
this.drawPoint((Cupple) storable_data);
}
I suggest you map each data point to a point on the screen, thus avoiding the following coordinate system pitfalls. Take your list of points and create from them a list of points to draw. Take into account that:
The drawing is pixel-based, so you will want to scale your points (or you would have rectangles 1 to 4 pixels wide...).
You will need to translate all your points because negative values will be outside the boundaries of the component on which you draw.
The direction of the y axis is reversed in the drawing coordinates.
Once that is done, use the new list of points for the drawing and the initial one for calculations. Here is an example:
public class Graph extends JPanel {
private static int gridSize = 6;
private static int scale = 100;
private static int size = gridSize * scale;
private static int translate = size / 2;
private static int pointSize = 10;
List<Point> dataPoints, scaledPoints;
Graph() {
setBackground(Color.WHITE);
// points taken from your example
Point p1 = new Point(-1, -2);
Point p2 = new Point(-1, 0);
Point p3 = new Point(1, 0);
Point p4 = new Point(1, -2);
dataPoints = Arrays.asList(p1, p2, p3, p4);
scaledPoints = dataPoints.stream()
.map(p -> new Point(p.x * scale + translate, -p.y * scale + translate))
.collect(Collectors.toList());
}
#Override
public Dimension getPreferredSize() {
return new Dimension(size, size);
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g;
// draw a grid
for (int i = 0; i < gridSize; i++) {
g2d.drawLine(i * scale, 0, i * scale, size);
g2d.drawLine(0, i * scale, size, i * scale);
}
// draw the rectangle
g2d.setPaint(Color.RED);
g2d.drawPolygon(scaledPoints.stream().mapToInt(p -> p.x).toArray(),
scaledPoints.stream().mapToInt(p -> p.y).toArray(),
scaledPoints.size());
// draw the points
g2d.setPaint(Color.BLUE);
// origin
g2d.fillRect(translate, translate, pointSize, pointSize);
g2d.drawString("(0, 0)", translate, translate);
// data
for (int i = 0; i < dataPoints.size(); i++) {
Point sp = scaledPoints.get(i);
Point dp = dataPoints.get(i);
g2d.fillRect(sp.x, sp.y, pointSize, pointSize);
g2d.drawString("(" + dp.x + ", " + dp.y + ")", sp.x, sp.y);
}
}
public static void main(String[] args) {
JFrame frame = new JFrame();
frame.setContentPane(new Graph());
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
}
And another:
You might want to have the points aligned on the grid intersections and not below and to the right of them. I trust you will figure this one out.
Also, I ordered the points so that drawPolygon will paint the lines in the correct order. If your points are arbitrarily arranged, look for ways to find the outline. If you want lines between all points like in your example, iterate over all combinations of them with drawLine.

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:

Creating an arc between two points for interpolation in Java

I've got a top down game that I'm making and I want my enemies to be able to move across the screen in an arc. Right now they move in a straight line between two edges of the screen. I generate a start position on one edge then find a random position somewhere across the screen and calculate movement speeds my multiplying the sin/cos of the angle by their speed variable.
I'd like to use these to points to generate some arc between them and then use that to move my enemies along it. I thought maybe some sort of spline would do the trick but Im not entirely sure how to create one, nor more significantly how to use it to interpolate my characters. I think at this point its more of a math question than programming but I hope someone can help anyways. Thanks.
Yes, a spline would work for you. Specifically i would recommend a cubic spline, because later on if you wanted do a different shape, maybe a Street Fighter style uppercut, you could re-use the same code. I remember cubic spline being a decent, general solution.
As far as solving for a cubic spline I would recommend you just Google for pseudo code which makes sense to you. That's only if you really want to generically solve for a suitable cubic spline on the fly.
In practice, i imagine the shape you want will be the same almost all the time? If so, you can probably solve a few general cases of a spline and save it to some fast data structure to improve performance. In example, for y=x a suitable array holding the necessary information (pre-processed) would be x[0] = 1,x[1] = 1,x[2] = 2 ... x[n] = n.
In practice, you could come up with an equation to model a simple two point spline. A cubic equation has 4 unknowns. So you have two data points at least, you're starting point and your end point. In addition, you can calculate the derivative of him when he jumps. For your fourth point you could use either another point you want him to jump through, or the derivative when he lands. Then use https://www.wolframalpha.com/ to solve the equation for you. Or use an equation to solve cubics.
Another thing you can do is just calculate the arc using the quadratic equation + gravity + wind resistance. Again, Google knows how to solve that. This page is something i quickly found that looks like it could do the trick. http://www.physicsclassroom.com/class/vectors/Lesson-2/Non-Horizontally-Launched-Projectiles-Problem-Solv
When you intend to use a spline, you can use the Path2D class that is already available in Java. You can assemble an arbitrary path by
moving to a certain point
appending a line segment
appending a quadratic curve segment
appending a cubic curve segment
So assembling this path should be easy: You can just create a quadratic curve that starts at a random point on the left border of the screen and ends at a random point on the right border of the screen. As the control points (for both ends) you can use a point at a random position in the center of the screen.
(BTW: When you manage to represent the path as a generic Path2D, then you can probably imagine that you have quite a lot of freedom when designing the path for the enemies. They could run in circles or zig-zag, just as you like...)
What might be more tricky here is to let the enemies follow this path.
The first step is not sooo tricky yet: You can walk along this path with a PathIterator. But this PathIterator will only return a single segment - namely, the quadratic curve. This can be alleviated by creating a flattening PathIterator. This will convert all curves into line segments (the resolution can be high, so you won't notice any corners).
However, now comes the really tricky part: When iterating these line segments, the movement speed may vary: Depending on the curvature of the original quadratic curve, there may be more or fewer line segments be created. In the worst case, when all 3 points are on one line, then only one line segment would be created, and the enemy would walk across the whole screen in a single step. So you have to make sure that this path is traversed with a constant speed. You have to compute how far you already have been walking when iterating over the path, and possibly interpolate a position between two points of the path.
I quickly assembled an example. It's certainly not bullet-proof, but might serve as a starting point.
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.Path2D;
import java.awt.geom.PathIterator;
import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
public class SplineMovementTest
{
public static void main(String[] args)
{
SwingUtilities.invokeLater(new Runnable()
{
#Override
public void run()
{
createAndShowGUI();
}
});
}
private static PathFollower pathFollower;
private static void createAndShowGUI()
{
JFrame frame = new JFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.getContentPane().setLayout(new BorderLayout());
final Random random = new Random(0);
final SplineMovementPanel p = new SplineMovementPanel();
JButton generateButton = new JButton("Generate");
generateButton.addActionListener(new ActionListener()
{
#Override
public void actionPerformed(ActionEvent e)
{
Shape spline = generateSpline(p,
random.nextDouble(),
random.nextDouble(),
random.nextDouble());
p.setSpline(spline);
pathFollower = new PathFollower(spline);
p.repaint();
}
});
frame.getContentPane().add(generateButton, BorderLayout.NORTH);
startAnimation(p);
frame.getContentPane().add(p, BorderLayout.CENTER);
frame.setSize(800, 800);
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
private static Shape generateSpline(
JComponent c, double yLeft, double yCenter, double yRight)
{
Path2D spline = new Path2D.Double();
double x0 = 0;
double y0 = yLeft * c.getHeight();
double x1 = c.getWidth() / 2;
double y1 = yCenter * c.getHeight();
double x2 = c.getWidth();
double y2 = yRight * c.getHeight();
spline.moveTo(x0, y0);
spline.curveTo(x1, y1, x1, y1, x2, y2);
return spline;
}
private static void startAnimation(final SplineMovementPanel p)
{
Timer timer = new Timer(20, new ActionListener()
{
double position = 0.0;
#Override
public void actionPerformed(ActionEvent e)
{
position += 0.005;
position %= 1.0;
if (pathFollower != null)
{
Point2D point = pathFollower.computePointAt(
position * pathFollower.getPathLength());
p.setObjectLocation(point);
}
}
});
timer.start();
}
}
class PathFollower
{
private final List<Point2D> points;
private final double pathLength;
PathFollower(Shape spline)
{
points = createPointList(spline);
pathLength = computeLength(points);
}
public double getPathLength()
{
return pathLength;
}
Point2D computePointAt(double length)
{
if (length < 0)
{
Point2D p = points.get(0);
return new Point2D.Double(p.getX(), p.getY());
}
if (length > pathLength)
{
Point2D p = points.get(points.size()-1);
return new Point2D.Double(p.getX(), p.getY());
}
double currentLength = 0;
for (int i=0; i<points.size()-1; i++)
{
Point2D p0 = points.get(i);
Point2D p1 = points.get(i+1);
double distance = p0.distance(p1);
double nextLength = currentLength + distance;
if (nextLength > length)
{
double rel = 1 - (nextLength - length) / distance;
double x0 = p0.getX();
double y0 = p0.getY();
double dx = p1.getX() - p0.getX();
double dy = p1.getY() - p0.getY();
double x = x0 + rel * dx;
double y = y0 + rel * dy;
return new Point2D.Double(x,y);
}
currentLength = nextLength;
}
Point2D p = points.get(points.size()-1);
return new Point2D.Double(p.getX(), p.getY());
}
private static double computeLength(List<Point2D> points)
{
double length = 0;
for (int i=0; i<points.size()-1; i++)
{
Point2D p0 = points.get(i);
Point2D p1 = points.get(i+1);
length += p0.distance(p1);
}
return length;
}
private static List<Point2D> createPointList(Shape shape)
{
List<Point2D> points = new ArrayList<Point2D>();
PathIterator pi = shape.getPathIterator(null, 0.1);
double coords[] = new double[6];
while (!pi.isDone())
{
int s = pi.currentSegment(coords);
switch (s)
{
case PathIterator.SEG_MOVETO:
points.add(new Point2D.Double(coords[0], coords[1]));
case PathIterator.SEG_LINETO:
points.add(new Point2D.Double(coords[0], coords[1]));
}
pi.next();
}
return points;
}
}
class SplineMovementPanel extends JPanel
{
void setSpline(Shape shape)
{
this.spline = shape;
}
void setObjectLocation(Point2D objectLocation)
{
this.objectLocation = objectLocation;
repaint();
}
private Shape spline = null;
private Point2D objectLocation = null;
#Override
protected void paintComponent(Graphics gr)
{
super.paintComponent(gr);
Graphics2D g = (Graphics2D)gr;
g.setRenderingHint(
RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
if (spline != null)
{
g.setColor(Color.BLACK);
g.draw(spline);
}
if (objectLocation != null)
{
g.setColor(Color.RED);
int x = (int)objectLocation.getX()-15;
int y = (int)objectLocation.getY()-15;
g.fillOval(x, y, 30, 30);
}
}
}

Categories

Resources