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);
}
}
}
Related
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)*.
I cant wrap my mind about how to solve this problem. I have created 3 triangles:
I have the XY coordinates of all the individual triangles. I want to calculate the center of gravity if they are put together as shown, but only using the filled part, and wherever they intersec, you do not count the mass twice. How can I go about doing this in java?
Can I somehow combine these into 1 of some kind of object, and then do a numeric calculation of each area and find a middle ground, or is there a better way?
Convert your shape to a single polygon (there are two intersections to be computed).
Then use the centroid formula for polygons.
Another option is to fill all triangles in the same color (by polygon or seed filling), then seed fill the resultant area, while accumulating the X and Y coordinates on the fly.
First, you have to determine for what you actually want to compute the center of gravity. Obviously, when the triangles intersect (and the overlapping area should not be counted twice), then you are not computing the center of gravity of triangles, but the center of gravity of their intersection area.
Fortunately, such an intersection area can easily be computed with the Area class. And according to one comment, you already have an Area describing this area.
So one option to compute the center of gravity of this area is to compute the average of all border points of the Area.
Note that this only works when the Area does not have holes.
Otherwise, you have to compute the area of the Area.
Here is a possible implementation:
import java.awt.Shape;
import java.awt.geom.Area;
import java.awt.geom.Path2D;
import java.awt.geom.PathIterator;
import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
public class CenterOfGravity
{
public static void main(String[] args)
{
Path2D p0 = new Path2D.Double();
p0.moveTo(100, 100);
p0.lineTo(200, 100);
p0.lineTo(150, 50);
p0.closePath();
Path2D p1 = new Path2D.Double();
p1.moveTo(150, 100);
p1.lineTo(250, 100);
p1.lineTo(200, 50);
p1.closePath();
Area a = new Area();
a.add(new Area(p0));
a.intersect(new Area(p1));
Point2D cog = computeCenterOfGravity(a);
System.out.println(cog);
}
private static Point2D computeCenterOfGravity(Shape shape)
{
return computeAverage(computePoints(shape, 1.0));
}
private static Point2D computeAverage(
Collection<? extends Point2D> points)
{
double x = 0;
double y = 0;
for (Point2D point : points)
{
x += point.getX();
y += point.getY();
}
if (!points.isEmpty())
{
x /= points.size();
y /= points.size();
}
return new Point2D.Double(x, y);
}
public static List<Point2D> computePoints(
Shape shape, double flatness)
{
List<Point2D> result = new ArrayList<Point2D>();
PathIterator pi = shape.getPathIterator(null, flatness);
double[] coords = new double[6];
while (!pi.isDone())
{
int segment = pi.currentSegment(coords);
switch (segment)
{
case PathIterator.SEG_MOVETO:
case PathIterator.SEG_LINETO:
result.add(new Point2D.Double(coords[0], coords[1]));
break;
case PathIterator.SEG_CLOSE:
break;
case PathIterator.SEG_QUADTO:
case PathIterator.SEG_CUBICTO:
default:
throw new AssertionError(
"Invalid segment in flattened path!");
}
pi.next();
}
return result;
}
}
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:
I have got a list of points in 2-dimension.
For example:
x=c(4,3,3,5,6,6,4)
and
y=c(5,3,1,0,1,3,5)
Plot of these 2-D points is
I would like to draw a wrapper of this point set like this :
Note that, the perpendicular distance between the boundary(wrapper) and the nearest point is 2 unit.
Note that: I have a number of point sets like the above point set. I would like to do the same thing for all the sets.
I want to have this boundary polygon. Could anyone please suggest me how to do this.
Any ideas greatly appreciated, Janak.
To achieve this you can use this simple algorithm.
First we will need the center of your coordinates (red dot).
This can be done by adding all of your x-values and divide the result by their amount, same with the y-values.
The next step will be to calculate a rectange which wraps a current coordinate and the center point. (Don't forget to add your offset of 2 units here)
We will do this for all coordinates
At this point we could already stop. Just render all of those rectangles and then your coordinates on top of the picture, but let's improve this just a little bit more.
We don't actually need all those rectangles, what we want is a polygon to wrap those points.
This polygon is defined by the intersection of our retangles and their edges (blue dots).
Note that we will only need the edges and intersections which are the furthest away from our center.
We now can connect those blue points by connecting points who share one common coordinate and are 'neighbours'.
Update:
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
public class PolyWrapper {
public static void main(String[] args){
//your example coords:
int[] x_coords = {4,3,3,5,6,6,4};
int[] y_coords = {5,3,1,0,1,3,5};
//make sure the coordinates have the same length, else they won't match
if(x_coords.length != y_coords.length){
System.err.println("Bad parameters given. X and Y don't match!");
System.exit(1);
}
//this will hold our points:
ArrayList<Point2D> points = new ArrayList<>();
for(int i = 0; i < x_coords.length; i++){
Point2D p = new Point2D.Double(x_coords[i], y_coords[i]);
points.add(p);
}
//lets get the center of all those points:
final Point2D center = get_center(points);
ArrayList<Rectangle2D> rectangles = new ArrayList<>();
//now lets create those wrapping rectangles:
for(Point2D p : points){
Rectangle2D r = new Rectangle2D.Double();
r.setFrameFromDiagonal(center, p);
rectangles.add(r);
}
//now show the wrapping rectangles:
for(Rectangle2D r : rectangles){
System.out.println(r.toString());
}
}
//this method returns the center of a list of points
public static Point2D get_center(ArrayList<Point2D> points){
double x = 0,y =0;
for(Point2D p : points){
x += p.getX();
y += p.getY();
}
x = x / points.size();
y = y / points.size();
Point2D c = new Point2D.Double();
c.setLocation(x, y);
return c;
}
}
So here is some example code. I have not found the time to finish it yet, but since your question is really interesting I will keep working on this.
So far this code calculates the center-point and creates rectangles around the center and given coordinates.
This output provides the upper left corner of each rectangle, it's width and height.
Sample output:
java.awt.geom.Rectangle2D$Double[x=4.0,y=2.5714285714285716,w=0.4285714285714288,h=2.4285714285714284]
java.awt.geom.Rectangle2D$Double[x=3.0,y=2.5714285714285716,w=1.4285714285714288,h=0.4285714285714284]
java.awt.geom.Rectangle2D$Double[x=3.0,y=1.0,w=1.4285714285714288,h=1.5714285714285716]
java.awt.geom.Rectangle2D$Double[x=4.428571428571429,y=0.0,w=0.5714285714285712,h=2.5714285714285716]
java.awt.geom.Rectangle2D$Double[x=4.428571428571429,y=1.0,w=1.5714285714285712,h=1.5714285714285716]
java.awt.geom.Rectangle2D$Double[x=4.428571428571429,y=2.5714285714285716,w=1.5714285714285712,h=0.4285714285714284]
java.awt.geom.Rectangle2D$Double[x=4.0,y=2.5714285714285716,w=0.4285714285714288,h=2.4285714285714284]
P.s.:
I tried to improve the algorithm from this point but encountered a problem which seems to be hard to solve - Maybe I will start a new question about this problem.
(It is about the picture with the blue dots. Once you have all points from the rectangles and their intersections it is hard to find out which of the resulting points are actually necessary for our polygon). I think I am close to a solution, so watch out for my next edits.
Using Java, this becomes very simple. The program demonstrates the result by plotting it. The outline may also be obtained by iterating the area.getPathIterator(at), which will return all points, one by one.
import java.awt.*;
import java.awt.geom.*;
import java.util.*;
public class PointSet {
public static final int W = 2;
Area area = new Area();
public void add( double x, double y ){
area.add( new Area( new Rectangle2D.Double( x-W, y-W,2*W, 2*W ) ) );
}
public void plot(){
Board board = new Board();
board.go( area );
}
public static void main( String[] args ){
PointSet ps = new PointSet();
ps.add( 4, 5);
ps.add( 3, 3);
ps.add( 3, 1);
ps.add( 5, 0);
ps.add( 6, 1);
ps.add( 6, 3);
ps.plot();
}
}
and:
import java.awt.*;
import java.awt.geom.*;
import javax.swing.*;
import java.util.*;
public class Board extends JPanel {
Area area;
void go( Area area ) {
this.area = area;
JFrame frame = new JFrame("Circle Test");
frame.getContentPane().add(this);
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
repaint();
frame.setPreferredSize(new Dimension(800,800));
frame.pack();
frame.setVisible(true);
}
public void paintComponent(Graphics g) {
AffineTransform at = new AffineTransform();
at.translate( 100, 100 );
at.scale( 50, 50 );
PathIterator pit = area.getPathIterator( at );
Path2D path = new Path2D.Double();
path.append( pit, true );
Graphics2D g2d = (Graphics2D)g;
g2d.draw( path );
}
}
I'm working on a 3D space trading game with some people, and one of the things I've been assigned to do is to make a guidance computer 'tunnel' that the ship travels through, with the tunnel made of squares that the user flies through to their destination, increasing in number as the user gets closer to the destination.
It's only necessary to render the squares for the points ahead of the ship, since that's all that's visible to the user. On their way to a destination, the ship's computer is supposed to put up squares on the HUD that represent fixed points in space between you and the destination, which are small in the distance and get larger as the points approach the craft.
I've had a go at implementing this and can't seem to figure it out, mainly using logarithms (Math.log10(x) and such). I tried to get to get the ship position in 'logarithmic space' to help find out what index to start from when drawing the squares, but then the fact that I only have distance to the destination to work with confuses the matter, especially when you consider that the number of squares has to vary dynamically to make sure they stay fixed at the right locations in space (i.e., the squares are positioned at intervals of 200 or so before being transformed logarithmically).
With regard to this, I had a working implementation with the ship between a start of 0.0d and end of 1.0d, although the implementation wasn't so nice. Anyway, the problem essentially boils down to a 1d nature. Any advice would be appreciated with this issue, including possible workarounds to achieve the same effect or solutions.
(Also, there's a Youtube video showing this effect: http://www.youtube.com/watch?v=79F9Nj7GgfM&t=3m5s)
Cheers,
Chris
Edit: rephrased the entire question.
Edit: new testbed code:
package st;
import java.awt.BorderLayout;
import java.awt.Canvas;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferStrategy;
import java.text.DecimalFormat;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
public class StUI2 extends JFrame {
public static final double DEG_TO_RAD = Math.PI / 180.0d;
public static final DecimalFormat decimalFormat = new DecimalFormat("0.0000");
public static final Font MONO = new Font("Monospaced", Font.PLAIN, 10);
public class StPanel extends Canvas {
protected final Object imgLock = new Object();
protected int lastWidth = 1, lastHeight = 1;
protected boolean first = true;
protected Color bgColour = Color.DARK_GRAY, gridColour = Color.GRAY;
double shipWrap = 700;
double shipFrame = 100;
double shipPos = 0;
long lastUpdateTimeMS = -1;
long currUpdateTimeMS = -1;
public StPanel() {
setFocusable(true);
setMinimumSize(new Dimension(1, 1));
setAlwaysOnTop(true);
}
public void internalPaint(Graphics2D g) {
synchronized (imgLock) {
if (lastUpdateTimeMS < 0) {
lastUpdateTimeMS = System.currentTimeMillis();
}
currUpdateTimeMS = System.currentTimeMillis();
long diffMS = currUpdateTimeMS - lastUpdateTimeMS;
g.setFont(MONO);
shipPos += (60d * ((double)diffMS / 1000));
if (shipPos > shipWrap) {
shipPos = 0d;
}
double shipPosPerc = shipPos / shipWrap;
double distToDest = shipWrap - shipPos;
double compression = 1000d / distToDest;
g.setColor(bgColour);
Dimension d = getSize();
g.fillRect(0, 0, (int)d.getWidth(), (int)d.getHeight());
//int amnt2 = (int)unlog10((1000d / distToDest));
g.setColor(Color.WHITE);
g.drawString("shipPos: " + decimalFormat.format(shipPos), 10, 10);
g.drawString("distToDest: " + decimalFormat.format(distToDest), 10, 20);
g.drawString("shipWrap: " + decimalFormat.format(shipWrap), 150, 10);
int offset = 40;
g.setFont(MONO);
double scalingFactor = 10d;
double dist = 0;
int curri = 0;
int i = 0;
do {
curri = i;
g.setColor(Color.GREEN);
dist = distToDest - getSquareDistance(distToDest, scalingFactor, i);
double sqh = getSquareHeight(dist, 100d * DEG_TO_RAD);
g.drawLine(30 + (int)dist, (offset + 50) - (int)(sqh / 2d), 30 + (int)dist, (offset + 50) + (int)(sqh / 2d));
g.setColor(Color.LIGHT_GRAY);
g.drawString("i: " + i + ", dist: " + decimalFormat.format(dist), 10, 120 + (i * 10));
i++;
} while (dist < distToDest);
g.drawLine(10, 122, 200, 122);
g.drawString("last / i: " + curri + ", dist: " + decimalFormat.format(dist), 10, 122 + (i * 10));
g.setColor(Color.MAGENTA);
g.fillOval(30 + (int)shipPos, offset + 50, 4, 4);
lastUpdateTimeMS = currUpdateTimeMS;
}
}
public double getSquareDistance(double initialDist, double scalingFactor, int num) {
return Math.pow(scalingFactor, num) * num * initialDist;
}
public double getSquareHeight(double distance, double angle) {
return distance / Math.tan(angle);
}
/* (non-Javadoc)
* #see java.awt.Canvas#paint(java.awt.Graphics)
*/
#Override
public void paint(Graphics g) {
internalPaint((Graphics2D)g);
}
public void redraw() {
synchronized (imgLock) {
Dimension d = getSize();
if (d.width == 0) d.width = 1;
if (d.height == 0) d.height = 1;
if (first || d.getWidth() != lastWidth || d.getHeight() != lastHeight) {
first = false;
// remake buf
GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
//create an object that represents the device that outputs to screen (video card).
GraphicsDevice gd = ge.getDefaultScreenDevice();
gd.getDefaultConfiguration();
createBufferStrategy(2);
lastWidth = (int)d.getWidth();
lastHeight = (int)d.getHeight();
}
BufferStrategy strategy = getBufferStrategy();
Graphics2D g = (Graphics2D)strategy.getDrawGraphics();
internalPaint(g);
g.dispose();
if (!strategy.contentsLost()) strategy.show();
}
}
}
protected final StPanel canvas;
protected Timer viewTimer = new Timer(1000 / 60, new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
canvas.redraw();
}
});
{
viewTimer.setRepeats(true);
viewTimer.setCoalesce(true);
}
/**
* Create the applet.
*/
public StUI2() {
JPanel panel = new JPanel(new BorderLayout());
setContentPane(panel);
panel.add(canvas = new StPanel(), BorderLayout.CENTER);
setVisible(true);
setDefaultCloseOperation(EXIT_ON_CLOSE);
setSize(800, 300);
setTitle("Targetting indicator test #2");
viewTimer.start();
}
public static double unlog10(double x) {
return Math.pow(10d, x);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
StUI2 ui = new StUI2();
}
});
}
}
Assuming you want the squares to be equal height (when you reach them), you can calculate a scaling factor based on the distance to the destination (d) and the required height of the squares upon reaching them (h).
From these two pieces of information you can calculate the inverse tangent (atan) of the angle (alpha) between the line connecting the ship to the destination (horizontal line in your image) and the line connecting the top of the squares with the destination (angled line in your image).
EDIT: corrected formula
Using the angle, you can calculate the height of the square (h') at any given distance from the destination: you know the distance to the destination (d') and the angle (alpha); The height of the square at distance d' is h'=r'*sin(alpha) -- sin(alpha)=cos(alpha)*tan(alpha) and r'=d'/cos(alpha) (the distance between the destination and the top of the square -- the "radius"). Or more easily: h'=d'*tan(alpha).
Note: adopting the algorithm to varying height (when you reach them) squares is relatively simple: when calculating the angle, just assume a (phantom) square of fixed height and scale the squares relatively to that.
If the height of the square at distance d' is calculated for you by your graphic library, all the better, you only need to figure out the distances to place the squares.
What distances to place the squares from the destination?
1) If you want a varying number of squares shown (in front of the ship), but potentially infinite number of squares to consider (based on d), you can chose the distance of the closest square to the destination (d1) and calculate the distances of other squares by the formula s^k*k*d1, where s (scaling factor) is a number > 1 for the k'th square (counting from the destination). You can stop the algorithm when the result is larger than d.
Note that if d is sufficiently large, the squares closest to the distance will block the destination (there are many of them and their heights are small due to the low angle). In this case you can introduce a minimal distance (possibly based on d), below which you do not display the squares -- you will have to experiment with the exact values to see what looks right/acceptable.
2) If you want a fixed amount of squares (sn) showing always, regardless of d, you can calculate the distances of the squares from the destination by the formula d*s^k, where s is a number < 1, k is the index of the square (counting from the ship). The consideration about small squares probably don't apply here unless sn is high.
To fix the updated code, change the relavant part to:
double dist = 0;
double d1 = 10;
int curri = 0;
int i = 1;
int maxSquareHeight = 40;
double angle = Math.atan(maxSquareHeight/distToDest);
while (true)
{
curri = i;
g.setColor(Color.GREEN);
dist = getSquareDistance(d1, scalingFactor, i);
if (dist > distToDest) {
break;
}
double sqh = getSquareHeight(dist, angle);
g.drawLine(30 + (int)(shipWrap - dist), offset+50-(int)(sqh / 2d), 30 + (int)(shipWrap - dist), offset+50+(int)(sqh / 2d));
g.setColor(Color.LIGHT_GRAY);
i++;
}
public double getSquareHeight(double distance, double angle) {
return distance * Math.tan(angle);
}
You should also reduce scalingFactor to the magnitude of ~1.5.
EDIT: If you replace the formula s^k*k*d1 with s^(k-1)*k*d1, then the first square will be exactly at distance d1.
EDIT: fixed square height calculating formula
EDIT: updated code