Related
As you can see on the image, I have a p1 and p2 objects with (x,y) coordinates which I know the values, and I know radius of all these circle objects.
However, I want to calculate new position x,y which would be p3 center point. Basically, as you can see it's p2 position + radius.
I am doing this for java game which is based on libgdx. I would appreciate any math or java language directions/examples.
See code comments for explanation.
import java.awt.*;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import javax.swing.*;
class CenteredCircle extends Ellipse2D.Double {
CenteredCircle(Point2D.Double p, double radius) {
super(p.x - radius, p.y - radius, 2 * radius, 2 * radius);
}
}
public class CircleDemo extends JFrame {
public CircleDemo() {
int width = 640; int height = 480;
setSize(new Dimension(width, height));
setLocationRelativeTo(null);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setVisible(true);
JPanel p = new JPanel() {
#Override
public void paintComponent(Graphics g) {
Graphics2D g2d = (Graphics2D) g;
// center p1
Point2D.Double p1 = new Point2D.Double(getSize().width/2, getSize().height/2);
double radius = 130.0;
// big circle
Shape circle2 = new CenteredCircle(p1, radius);
g2d.draw(circle2);
// 12 small circles
for (int angle = 0; angle < 360; angle += 30) {
// this is the magic part
// a polar co-ordinate has a length and an angle
// by changing the angle we rotate
// the transformed co-ordinate is the center of the small circle
Point2D.Double newCenter = polarToCartesian(radius, angle);
// draw line just for visualization
Line2D line = new Line2D.Double(p1.x, p1.y, p1.x + newCenter.x, p1.y+ newCenter.y);
g2d.draw(line);
// draw the small circle
Shape circle = new CenteredCircle(
new Point2D.Double(p1.x + newCenter.x, p1.y + newCenter.y),
radius/4);
g2d.draw(circle);
}
}
};
setTitle("Circle Demo");
getContentPane().add(p);
}
public static void main(String arg[]) {
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
new CircleDemo();
}
});
}
static Point2D.Double polarToCartesian(double r, double theta) {
theta = (theta * Math.PI) / 180.0; // multiply first, then divide to keep error small
return new Point2D.Double(r * Math.cos(theta), r * Math.sin(theta));
}
// not needed, just for completeness
public static Point2D.Double cartesianToPolar(double x, double y) {
return new Point2D.Double(Math.sqrt(x * x + y * y), (Math.atan2(y, x) * 180) / Math.PI);
}
}
Now using libgdx for the graphics. Thus no need for polar co-ordinates, on the outside.
I am not doing frame rate relative animation. Therefore, this is no perfect match to your code.
Using the following calculation (if (theta >= 360) { theta = 0.0f; }) at the end of the render method will let the animation restart with its original value.
package org.demo;
import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.utils.ScreenUtils;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
public class CircleDemo extends ApplicationAdapter {
ShapeRenderer shapeRenderer;
float theta = 0.0f;
#Override
public void create () {
shapeRenderer = new ShapeRenderer();
}
#Override
public void render () {
ScreenUtils.clear(0, 0.4f, 0.4f, 1);
Vector2 p1 = new Vector2( Gdx.graphics.getWidth() / 2.0f , Gdx.graphics.getHeight() / 2.0f);
Vector2 smallCircleCenter = new Vector2(150.0f, 0.0f);
smallCircleCenter.add(p1); // translate center by p1
shapeRenderer.begin(ShapeRenderer.ShapeType.Line);
// static lines and circles
for (int angle = 0; angle < 360; angle += 30) {
Vector2 lineEnd = new Vector2(smallCircleCenter);
lineEnd.rotateAroundDeg(p1, angle);
shapeRenderer.line(p1, lineEnd);
shapeRenderer.circle(lineEnd.x, lineEnd.y, 20);
}
// animated line and circle in red
shapeRenderer.setColor(0.75f, 0, 0, 1);
Vector2 movingCircleCenter = new Vector2(smallCircleCenter);
movingCircleCenter.rotateAroundDeg(p1, theta);
shapeRenderer.line(p1, movingCircleCenter);
shapeRenderer.circle(movingCircleCenter.x, movingCircleCenter.y, 20);
shapeRenderer.setColor(1, 1, 1, 1);
shapeRenderer.end();
theta++;
// for the screenshot stop at 90 degrees
if (theta >= 90) {
theta = 90.0f;
}
}
#Override
public void dispose () {
shapeRenderer.dispose();
}
}
So I wrote a test in my project, based on your approach:
package com.bigbang.test.impl;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.utils.Array;
import com.bigbang.Game;
import com.bigbang.graphics.g2d.shapes.impl.Ellipse;
import com.bigbang.graphics.g2d.shapes.impl.Line;
import com.bigbang.graphics.gl.Color;
import com.bigbang.math.BBMath;
public class PolarToCartesianTest extends AbstractTest {
private Array<GraphicalObject> graphicalObjectArray;
private GraphicalObject dynamicGraphicalObject;
private float radius, smallCircleRadius;
private float centerX, centerY;
public PolarToCartesianTest(Game game) {
super(game);
}
#Override
public void create() {
radius = 200f;
centerX = game.getScreenController().getScreenWidth() / 2;
centerY = game.getScreenController().getScreenHeight() / 2;
smallCircleRadius = radius / 4;
graphicalObjectArray = new Array<>();
for (int angle = 0; angle < 360; angle += 30) {
GraphicalObject graphicalObject = new GraphicalObject();
graphicalObject.angle = angle;
graphicalObjectArray.add(graphicalObject);
}
dynamicGraphicalObject = new GraphicalObject();
game.getCameraController().getCamera().position.x = game.getScreenController().getScreenWidth() / 2;
game.getCameraController().getCamera().position.y = game.getScreenController().getScreenHeight() / 2;
}
#Override
public void update(float deltaTime) {
for (GraphicalObject graphicalObject : graphicalObjectArray) {
Vector2 polarToCartesianPosition = BBMath.polarToCartesian(radius, graphicalObject.angle);
graphicalObject.line.x1 = centerX + 0;
graphicalObject.line.y1 = centerY + 0;
graphicalObject.line.x2 = centerX + polarToCartesianPosition.x;
graphicalObject.line.y2 = centerY + polarToCartesianPosition.y;
graphicalObject.line.color = Color.WHITE_COLOR;
graphicalObject.ellipse.x = centerX + polarToCartesianPosition.x;
graphicalObject.ellipse.y = centerY + polarToCartesianPosition.y;
graphicalObject.ellipse.width = 2 * smallCircleRadius;
graphicalObject.ellipse.height = 2 * smallCircleRadius;
graphicalObject.ellipse.color = Color.WHITE_COLOR;
}
float shift = 0;
float theta = (shift * smallCircleRadius) * (centerY / centerX);
Vector2 pos = BBMath.polarToCartesian(radius, theta);
dynamicGraphicalObject.line.color = new Color(Color.RED);
dynamicGraphicalObject.line.x1 = centerX + 0;
dynamicGraphicalObject.line.y1 = centerY + 0;
dynamicGraphicalObject.line.x2 = centerX + pos.x;
dynamicGraphicalObject.line.y2 = centerY + pos.y;
dynamicGraphicalObject.ellipse.x = centerX + pos.x;
dynamicGraphicalObject.ellipse.y = centerY + pos.y;
dynamicGraphicalObject.ellipse.width = 2 * smallCircleRadius;
dynamicGraphicalObject.ellipse.height = 2 * smallCircleRadius;
dynamicGraphicalObject.ellipse.color = new Color(Color.RED);
}
#Override
public void draw() {
game.getShapeRenderer().begin(ShapeRenderer.ShapeType.Line);
for (GraphicalObject graphicalObject : graphicalObjectArray) {
graphicalObject.line.draw();
graphicalObject.ellipse.draw();
}
dynamicGraphicalObject.line.draw();
dynamicGraphicalObject.ellipse.draw();
game.getShapeRenderer().end();
}
class GraphicalObject {
Ellipse ellipse;
Line line;
float angle;
public GraphicalObject() {
this.ellipse = new Ellipse(game);
this.line = new Line(game);
}
}
}
Which is same math like in your example, with some modifications:
However, you can notice I have this dynamicGraphicalObject (red circle), which I want to shift position around circle by using theta value calculated as (shift * smallCircleRadius) * (centerY / centerX);. This works perfect for shift=0 value. It's properly positioned/overlapping white. But if I would change shift variable to 1, 2, 3, or 11, you can see that it's not precisely aligned with white circles. Is this floating point issue or am I missing something in calculation of theta ?
shift values used: 2,6 and 11 in order by images
--
SOLUTION:
float fixPrecision = 1.1f;
float theta = (shift * fixPrecision) + ((shift * smallCircleRadius) * (centerY / centerX));
I need to be able to draw very crowded points for a graph, via Swing. E.g: let's look at the following points:
p1=(35.19589389346247,32.10152879327731),
p2 = (35.20319591121872,32.10318254621849),
p3 = (35.20752617756255,32.1025646605042),
p4 = (35.21007339305892,32.10107446554622),
p5 = (35.21310882485876,32.104636394957986),
etc...
I want to draw them, however, as can be seen, their coordinates are very dense to each other. In addition, expanding the scale also didn't work, but just moved the points inside the frame.
Here is a glance at my attempt:
private void Oval(Graphics2D g2, String id, int locationX, int locationY) {
AffineTransform old = g2.getTransform();
g2.scale(4,4);
g2.setPaint(Color.blue);
g2.fillOval(locationX - Radius, locationY - Radius, Radius * 2, Radius * 2);
g2.setPaint(Color.black);
g2.drawString(id, locationX + Radius, locationY - Radius);
g2.setTransform(old);
}
This is the code for the point drawing, considering the panel dimension is (1000,1000).
Here is the output of the points' drawing:
As you can see, they override each other, and it's clearly nothing like I was intended to do. My goal is to separate them so we can see the points clearly.
So, the basic concept is to have some kind of "translation" for the "virtual" coordinates (your points) to the "physical" coordinates (of the screen)
You did try and do this by scaling the graphics context, but the problem with this is it will also scale the size of the balls as well, which isn't really what you want to do.
The following is a really basic example of one possible solution.
It calculates the min/max range of the area represented by the points and then uses the component size to translate the points so that they will fit within the visible space
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JFrame;
import javax.swing.JPanel;
class Test {
public static void main(String[] args) {
new Test();
}
public Test() {
EventQueue.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 GraphPoint {
private String id;
private Point2D point;
public GraphPoint(String id, Point2D point) {
this.id = id;
this.point = point;
}
public String getId() {
return id;
}
public Point2D getPoint() {
return point;
}
}
public class TestPane extends JPanel {
private List<GraphPoint> points;
private int radius = 10;
private double virtualScale = 1.0;
private Point2D minRange;
private Point2D maxRange;
public TestPane() {
points = new ArrayList<>(16);
points.add(new GraphPoint("1", new Point2D.Double(35.19589389346247, 32.10152879327731)));
points.add(new GraphPoint("2", new Point2D.Double(35.20319591121872, 32.10318254621849)));
points.add(new GraphPoint("3", new Point2D.Double(35.20752617756255, 32.1025646605042)));
points.add(new GraphPoint("4", new Point2D.Double(35.21007339305892, 32.10107446554622)));
points.add(new GraphPoint("5", new Point2D.Double(35.21310882485876, 32.104636394957986)));
double minX = Double.MAX_VALUE;
double maxX = Double.MIN_VALUE;
double minY = Double.MAX_VALUE;
double maxY = Double.MIN_VALUE;
for (GraphPoint gp : points) {
minX = Math.min(minX, gp.getPoint().getX());
maxX = Math.max(maxX, gp.getPoint().getX());
minY = Math.min(minY, gp.getPoint().getY());
maxY = Math.max(maxY, gp.getPoint().getY());
}
minRange = new Point2D.Double(minX, minY);
maxRange = new Point2D.Double(maxX, maxY);
double xRange = maxRange.getX() - minRange.getX();
double yRange = maxRange.getY() - minRange.getY();
System.out.println(minRange.getX() + " - " + minRange.getY());
System.out.println(maxRange.getX() + " - " + maxRange.getY());
System.out.println(xRange + " - " + yRange);
}
#Override
public Dimension getPreferredSize() {
return new Dimension(400, 400);
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
for (GraphPoint gp : points) {
paintPoint(g2d, gp);
}
g2d.dispose();
}
private void paintPoint(Graphics2D g2d, GraphPoint gp) {
Graphics2D g2 = (Graphics2D) g2d.create();
Point2D translated = translate(gp);
double xPos = translated.getX();
double yPos = translated.getY();
double offset = radius;
g2.translate(xPos - offset, yPos - offset);
g2.setPaint(Color.blue);
g2.fill(new Ellipse2D.Double(0, 0, offset * 2, offset * 2));
g2.dispose();
}
protected Point2D translate(GraphPoint gp) {
double xRange = maxRange.getX() - minRange.getX();
double yRange = maxRange.getY() - minRange.getY();
double offset = radius;
double width = getWidth() - (offset * 2);
double height = getHeight() - (offset * 2);
double xScale = width / xRange;
double yScale = height / yRange;
Point2D original = gp.getPoint();
double x = offset + ((original.getX() - minRange.getX()) * xScale);
double y = offset + ((original.getY() - minRange.getY()) * yScale);
System.out.println(gp.getId() + " " + x + " x " + y);
return new Point2D.Double(x, y);
}
}
}
I have to stress that this is one possible solution and that your actual requirements might differ, but this should give you starting point from which you can define your own algorithm, for example, you could define your own min/max range (ie 34x30 to 36x33)
the String I have tried now for 1 hour and I didn't get it I did edit your code already you help us a lot. the string above the points the id I mean point "0" this string above the point if u can help us or show us it will be appreciated a lot!
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JFrame;
import javax.swing.JPanel;
class Test {
public static void main(String[] args) {
new Test();
}
public Test() {
EventQueue.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 GraphPoint {
private String id;
private Point2D point;
public GraphPoint(String id, Point2D point) {
this.id = id;
this.point = point;
}
public String getId() {
return id;
}
public Point2D getPoint() {
return point;
}
}
public class TestPane extends JPanel {
private List<GraphPoint> points;
private int radius = 10;
private double virtualScale = 1.0;
private Point2D minRange;
private Point2D maxRange;
public TestPane() {
points = new ArrayList<>(16);
points.add(new GraphPoint("1", new Point2D.Double(35.19589389346247, 32.10152879327731)));
points.add(new GraphPoint("2", new Point2D.Double(35.20319591121872, 32.10318254621849)));
points.add(new GraphPoint("3", new Point2D.Double(35.20752617756255, 32.1025646605042)));
points.add(new GraphPoint("4", new Point2D.Double(35.21007339305892, 32.10107446554622)));
points.add(new GraphPoint("5", new Point2D.Double(35.21310882485876, 32.104636394957986)));
double minX = Double.MAX_VALUE;
double maxX = Double.MIN_VALUE;
double minY = Double.MAX_VALUE;
double maxY = Double.MIN_VALUE;
for (GraphPoint gp : points) {
minX = Math.min(minX, gp.getPoint().getX());
maxX = Math.max(maxX, gp.getPoint().getX());
minY = Math.min(minY, gp.getPoint().getY());
maxY = Math.max(maxY, gp.getPoint().getY());
}
minRange = new Point2D.Double(minX, minY);
maxRange = new Point2D.Double(maxX, maxY);
}
#Override
public Dimension getPreferredSize() {
return new Dimension(400, 400);
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
FontMetrics fm = g2d.getFontMetrics();
double insets = fm.getHeight() + radius;
// I'm lazy, so I'm drawing the lines first, then painting
// the points over the top as I can't be bothered to workout
// a clever way to ensure the lines are always painted under
// the dots
GraphPoint lastPoint = null;
for (GraphPoint gp : points) {
if (lastPoint != null) {
paintLine(g2d, lastPoint, gp, insets);
}
lastPoint = gp;
}
for (GraphPoint gp : points) {
paintPoint(g2d, gp, insets);
}
g2d.dispose();
}
private void paintLine(Graphics2D g2d, GraphPoint from, GraphPoint to, double insets) {
Point2D fromPoint = translate(from, insets);
Point2D toPoint = translate(to, insets);
g2d.setColor(Color.RED);
g2d.draw(new Line2D.Double(fromPoint, toPoint));
}
private void paintPoint(Graphics2D g2d, GraphPoint gp, double insets) {
Graphics2D g2 = (Graphics2D) g2d.create();
Point2D translated = translate(gp, insets);
double xPos = translated.getX();
double yPos = translated.getY();
double offset = radius;
g2.translate(xPos - offset, yPos - offset);
g2.setPaint(Color.blue);
g2.fill(new Ellipse2D.Double(0, 0, offset * 2, offset * 2));
FontMetrics fm = g2d.getFontMetrics();
String text = gp.getId();
double x = xPos - (fm.stringWidth(text) / 2);
double y = (yPos - radius - fm.getHeight()) + fm.getAscent();
g2d.drawString(text, (float)x, (float)y);
g2.dispose();
}
protected Point2D translate(GraphPoint gp, double insets) {
double xRange = maxRange.getX() - minRange.getX();
double yRange = maxRange.getY() - minRange.getY();
double offset = insets;
double width = getWidth() - (offset * 2);
double height = getHeight() - (offset * 2);
double xScale = width / xRange;
double yScale = height / yRange;
Point2D original = gp.getPoint();
double x = offset + ((original.getX() - minRange.getX()) * xScale);
double y = offset + ((original.getY() - minRange.getY()) * yScale);
System.out.println(gp.getId() + " " + x + " x " + y);
return new Point2D.Double(x, y);
}
}
}
The trick with the text is in understanding that I've already translated the origin point of the context, so the 0x0 position is actually the middle of the circle, so you need to subtract the text offset from it.
I've also made a slight update to allow the workflow to pass in a insets value which will decrease the available visible area to "help" compensate for the text
another question can make a line with an arrow in the tip
See...
Java make a directed line and make it move
How to draw 2d arrows with Java Swing?
for some ideas. Just beware, this becomes increasingly more complex, as the arrows need to be rotated towards the point they are looking, something like Connect two circles with a line might help
How i want it to look like:
The circles move along with the arrow around the center circle.
How it is looking at the moment:
I want to draw a 2 lines between two circles. however these circles move all around the screen and i dont know a methodical way to draw lines between them. For example, I always have the top left corner of the two circles i want to draw a line between but thats it. I need help to draw a line in java that will adjust based on its position so that the lines move around the edge as the circles move
for (int z = 0; z < lines.size(); z++) {
if (lines.get(z).getfState().equals(states.get(a).getText()) && !lines.get(z).getfState().equals(lines.get(z).getnState())) {
transition.get(z).setIcon(null);
for (int x = 0; x < states.size(); x++) {
if (states.get(x).getText().equals(lines.get(z).getnState()) && states.get(a).getText().equals(lines.get(z).getfState())) {
int xbegin = (int) states.get(a).getBounds().getX();
int ybegin = (int) states.get(a).getBounds().getY();
int xend = (int) states.get(x).getBounds().getX();
int yend = (int) states.get(x).getBounds().getY();
if (xbegin > xend) {
Path2D.Double rect = new Path2D.Double(drawArrowLine(xbegin, ybegin, xend, yend, 10, 7));
OutlineIcon transit = new OutlineIcon(drawArrowLine(xbegin, ybegin, xend + 30, yend, 10, 7), Color.BLACK);
transition.get(z).setIcon(transit);
transition.get(z).setBounds(rect.getBounds().x, rect.getBounds().y, rect.getBounds().width + 20, rect.getBounds().height + 20);
jPanel2.revalidate();
jPanel2.repaint();
} else {
if (xend - xbegin < 75) {
xbegin = xbegin - 20;
xend = xend - 20;
}
xbegin = xbegin + 5;
ybegin = ybegin + 25;
xend = xend + 5;
yend = yend + 25;
Path2D.Double rect = new Path2D.Double(drawArrowLine(xbegin, ybegin, xend - 10, yend, 10, 7));
OutlineIcon transit = new OutlineIcon(drawArrowLine(xbegin, ybegin, xend - 10, yend, 10, 7), Color.BLACK);
transition.get(z).setIcon(transit);
transition.get(z).setBounds(rect.getBounds().x, rect.getBounds().y, rect.getBounds().width + 20, rect.getBounds().height + 20);
jPanel2.revalidate();
jPanel2.repaint();
}
}
}
} else if (lines.get(z).getnState().equals(states.get(a).getText()) && !lines.get(z).getfState().equals(lines.get(z).getnState())) {
transition.get(z).setIcon(null);
for (int x = 0; x < states.size(); x++) {
if (states.get(x).getText().equals(lines.get(z).getfState()) && states.get(a).getText().equals(lines.get(z).getnState())) {
int xend = (int) states.get(a).getBounds().getX();
int yend = (int) states.get(a).getBounds().getY();
int xbegin = (int) states.get(x).getBounds().getX();
int ybegin = (int) states.get(x).getBounds().getY();
if (xbegin > xend) {
Path2D.Double rect2 = new Path2D.Double(drawArrowLine(xbegin, ybegin, xend, yend, 10, 7));
OutlineIcon transit = new OutlineIcon(drawArrowLine(xbegin, ybegin, xend + 30, yend, 10, 7), Color.BLACK);
transition.get(z).setIcon(transit);
transition.get(z).setBounds(rect2.getBounds().x, rect2.getBounds().y, rect2.getBounds().width + 20, rect2.getBounds().height + 20);
jPanel2.revalidate();
jPanel2.repaint();
} else {
if (xend - xbegin < 75) {
xbegin = xbegin + 20;
xend = xend + 20;
}
xbegin = xbegin + 5;
ybegin = ybegin + 25;
xend = xend + 5;
yend = yend + 25;
Path2D.Double rect2 = new Path2D.Double(drawArrowLine(xbegin, ybegin, xend - 10, yend, 10, 7));
OutlineIcon transit = new OutlineIcon(drawArrowLine(xbegin, ybegin, xend - 10, yend, 10, 7), Color.BLACK);
transition.get(z).setIcon(transit);
transition.get(z).setBounds(rect2.getBounds().x, rect2.getBounds().y, rect2.getBounds().width + 20, rect2.getBounds().height + 20);
jPanel2.revalidate();
jPanel2.repaint();
}
}
}
public static Path2D.Double createArrowForLine(
int fromPointx,
int fromPointy,
double rotationDeg,
double length,
double wingsAngleDeg) {
double ax = fromPointx;
double ay = fromPointy;
double radB = Math.toRadians(-rotationDeg + wingsAngleDeg);
double radC = Math.toRadians(-rotationDeg - wingsAngleDeg);
Path2D resultPath = new Path2D.Double();
resultPath.moveTo(length * Math.cos(radB) + ax, length * Math.sin(radB) + ay);
resultPath.lineTo(ax, ay);
resultPath.lineTo(length * Math.cos(radC) + ax, length * Math.sin(radC) + ay);
return (Path2D.Double) resultPath;
}
Although there have been some hiccups in the question, and the code provided so far looks questionable, the core of the question as it stands now is quite interesting...
There are different options for solving this. From the images that you provided so far, it looks like the circles always have the same size, which makes things far simpler. For circles with different sizes, you'd really have to compute the tangents of the circles, in the desired direction, mutually considering the radius of the other circle. Of course, this is possible, but a bit less trivial.
For the case that you have equally-sized circles, you can
Compute the difference of the centers of two circles
Divide this by the distance, to obtain the (normalized) direction
Rotate this direction by 90°
Scale the rotated direction vector by the radius
Add the scaled and rotated vector to the circle center
This will yield one endpoint of such a line. The rotation about 90° can be done once in clockwise and once in counterclockwise direction, to obtain the "upper" and "lower" endpoint for the line, respectively.
Image was updated with the EDIT, see below
The actual computation is done in the computeLine method of the MCVE below. Note that this example uses the "simple" approach, although it uses circles of slightly different sizes. The effect is that, when the difference between the sizes of two circles is too large (compared to the distance between the circles, basically), then the lines may slightly intersect the circles. But the solution should be a reasonable trade-off between simplicity and general applicability. Particularly, for equally-sized circles, there will be no intersections at all.
Code was updated with the EDIT, see below
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
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.Ellipse2D;
import java.awt.geom.Line2D;
import java.awt.geom.Path2D;
import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
public class LinesAtCirclesTest
{
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);
JPanel linesAtCirclesTestPanel = new LinesAtCirclesTestPanel();
f.getContentPane().add(linesAtCirclesTestPanel);
f.setSize(400,400);
f.setLocationRelativeTo(null);
f.setVisible(true);
}
}
class LinesAtCirclesTestPanel extends JPanel
implements MouseListener, MouseMotionListener
{
private Point2D draggedCenter;
private List<Point2D> centers = new ArrayList<Point2D>();
private List<Double> radii = new ArrayList<Double>();
public LinesAtCirclesTestPanel()
{
addMouseListener(this);
addMouseMotionListener(this);
addCircle(100, 100, 30);
addCircle(200, 300, 50);
addCircle(300, 200, 40);
}
private void addCircle(double x, double y, double radius)
{
centers.add(new Point2D.Double(x,y));
radii.add(radius);
}
#Override
protected void paintComponent(Graphics gr)
{
super.paintComponent(gr);
Graphics2D g = (Graphics2D)gr;
g.setRenderingHint(
RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
for (int i=0; i<centers.size(); i++)
{
Point2D center0 = centers.get(i);
double radius0 = radii.get(i);
Shape ellipse = new Ellipse2D.Double(
center0.getX() - radius0, center0.getY() - radius0,
radius0 + radius0, radius0 + radius0);
g.setColor(Color.LIGHT_GRAY);
g.fill(ellipse);
g.setColor(Color.BLACK);
g.draw(ellipse);
}
g.setColor(Color.RED);
for (int i=0; i<centers.size() - 1; i++)
{
Point2D center0 = centers.get(i);
double radius0 = radii.get(i);
Point2D center1 = centers.get(i+1);
double radius1 = radii.get(i+1);
g.draw(createArrow(computeLine(center0, radius0, center1, radius1, true)));
g.draw(createArrow(computeLine(center0, radius0, center1, radius1, false)));
}
}
private static Shape createArrow(Line2D line)
{
double dx = line.getX2() - line.getX1();
double dy = line.getY2() - line.getY1();
double angleToX = Math.atan2(dy, dx);
final double angleRad = Math.toRadians(30);
final double headLength = 20.0f;
double dxL = Math.cos(Math.PI + angleToX + angleRad) * headLength;
double dyL = Math.sin(Math.PI + angleToX + angleRad) * headLength;
double dxR = Math.cos(Math.PI + angleToX - angleRad) * headLength;
double dyR = Math.sin(Math.PI + angleToX - angleRad) * headLength;
Path2D arrow = new Path2D.Double();
arrow.moveTo(line.getX1(), line.getY1());
arrow.lineTo(line.getX2(), line.getY2());
arrow.lineTo(line.getX2() + dxL, line.getY2() + dyL);
arrow.moveTo(line.getX2(), line.getY2());
arrow.lineTo(line.getX2() + dxR, line.getY2() + dyR);
return arrow;
}
private static Line2D computeLine(
Point2D center0, double radius0,
Point2D center1, double radius1,
boolean upper)
{
double dx = center1.getX() - center0.getX();
double dy = center1.getY() - center0.getY();
double invLength = 1.0 / Math.hypot(dx, dy);
double dirX = dx * invLength;
double dirY = dy * invLength;
double rotDirX = dirY;
double rotDirY = -dirX;
if (upper)
{
rotDirX = -dirY;
rotDirY = dirX;
}
double x0 = center0.getX() + rotDirX * radius0;
double y0 = center0.getY() + rotDirY * radius0;
double x1 = center1.getX() + rotDirX * radius1;
double y1 = center1.getY() + rotDirY * radius1;
if (upper)
{
return new Line2D.Double(x1, y1, x0, y0);
}
return new Line2D.Double(x0, y0, x1, y1);
}
#Override
public void mousePressed(MouseEvent e)
{
draggedCenter = null;
for (int i=0; i<centers.size(); i++)
{
Point2D center = centers.get(i);
double radius = radii.get(i);
if (e.getPoint().distance(center) < radius)
{
draggedCenter = center;
}
}
}
#Override
public void mouseReleased(MouseEvent e)
{
draggedCenter = null;
}
#Override
public void mouseDragged(MouseEvent e)
{
if (draggedCenter == null)
{
return;
}
draggedCenter.setLocation(e.getPoint());
repaint();
}
#Override
public void mouseMoved(MouseEvent e)
{
// Not used
}
#Override
public void mouseClicked(MouseEvent e)
{
// Not used
}
#Override
public void mouseEntered(MouseEvent e)
{
// Not used
}
#Override
public void mouseExited(MouseEvent e)
{
// Not used
}
}
EDIT in response to the comment:
The original code computed Line2D objects. Creating an arrow from a line is, in the simplest case, basically done with a bit of trigonometry, and many resources exist for this on the web.
In response to the comment, I extended the example to show simple arrows, as depicted in the above image.
However, when taking a closer look at this, one may notice several degrees of freedom for such an arrow:
Should the head length be absolute or relative to the arrow?
Should the head width be absolute or relative to the arrow?
(Or: What should be the angle of the arrow head?)
Should the arrow head be filled, or consist of lines?
Should the "trunk" of the arrow be a single line, or an outline shape?
What should be the width of the trunk?
...
In order to cover some of these degrees of freedom, I created an ArrowCreator class a while ago, and there's also a sample showing how it may be used.
I wrote a Java program to take a triangle and either rotate, shift, or rotate and shift it, based upon a button click preformed by the user.
Beforehand, I instruct the user to enter in ranges of logical coordinates to determine how pixel coordinates will map to a real x-y coordinate system.
Initially, I have the triangle appearing in the middle of the screen, and after a button is clicked, the triangle is shown after a certain operation is preformed on it (i.e rotation, shifting, etc.)
However, after the operation is completed and the triangle is redrawn, I see an input box also drawn in the top-left corner of the JPanel.
I'm not sure how this keeps getting drawn there.
Code:
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
public class RotateAndShiftTriangles extends JFrame {
public static void main(String[] args) { new RotateAndShiftTriangles(); }
RotateAndShiftTriangles() {
super("Drawing 50 Triangles");
final JPanel drawingPanel = new DrawTriangles();
JPanel buttonPanel = new JPanel();
JButton rotate = new JButton("Rotate"),
shift = new JButton("Shift"),
rotateShift = new JButton("Rotate and Shift"),
reset = new JButton ("Reset");
drawingPanel.setBackground(Color.WHITE);
buttonPanel.add(rotate);
buttonPanel.add(shift);
buttonPanel.add(rotateShift);
buttonPanel.add(reset);
addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent e) {
System.exit(0);
}
});
DrawTriangles.rWidth = Float.parseFloat(JOptionPane.showInputDialog("Input rWidth"));
DrawTriangles.rHeight = Float.parseFloat(JOptionPane.showInputDialog("Input rHeight"));
rotate.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
DrawTriangles.rotate = true;
drawingPanel.repaint();
}
});
shift.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
DrawTriangles.shift = true;
drawingPanel.repaint();
}
});
rotateShift.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
DrawTriangles.rotate = true;
DrawTriangles.shift = true;
drawingPanel.repaint();
}
});
reset.addActionListener(new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
DrawTriangles.reset = true;
drawingPanel.repaint();
}
});
setSize(600, 400);
add("South", buttonPanel);
add("Center", drawingPanel);
setVisible(true);
}
}
class DrawTriangles extends JPanel {
static float rWidth, rHeight, pixelSize;
static int maxX, maxY, minMaxXY, centerX, centerY;
static boolean rotate = false, shift = false, reset = false;
float angle = 0;
void initialize() {
Dimension d = getSize();
maxX = d.width - 1; maxY = d.height - 1;
pixelSize = Math.max(rWidth / maxX, rHeight / maxY);
minMaxXY = Math.min(maxX, maxY);
centerX = maxX/2; centerY = maxY/2;
}
public int iX2(float x) { return Math.round(x); }
public int iY2(float y) { return maxY - Math.round(y); }
public static int iX(float x) { return Math.round(centerX + x / pixelSize); }
public static int iY(float y) { return Math.round(centerY - y / pixelSize); }
public static float fx(int x) { return (x - centerX) * pixelSize; }
public static float fy(int y) { return (centerY - y) * pixelSize; }
public void paint(Graphics g) {
super.paintComponent(g);
initialize();
int left = iX(-rWidth/2), right = iX(rWidth/2);
int top = iY(rHeight/2), bot = iY(-rHeight/2);
g.drawString("X: " + -rWidth/2 + " Y: " + rHeight/2, left, top + 10);
g.drawString("X: " + rWidth/2 + " Y: " + rHeight/2, right - 55, top + 10);
g.drawString("X: " + -rWidth/2 + " Y: " + -rHeight/2, left, bot);
g.drawString("X: " + rWidth/2 + " Y: " + -rHeight/2, right - 55, bot);
g.setColor(Color.BLUE);
g.drawRect(left, top, right - left, bot - top);
float side = 0.95f * minMaxXY, sideHalf = 0.5F * side,
h = sideHalf * (float)Math.sqrt(3),
xA, yA, xB, yB, xC, yC,
xA1, yA1, xB1, yB1, xC1, yC1, p, q;
q = 0.05F;
p = 1 - q;
xA = centerX - sideHalf;
yA = centerY - 0.5F * h;
xB = centerX + sideHalf;
yB = yA;
xC = centerX;
yC = centerY + 0.5F * h;
if(!reset) {
if(rotate) {
angle += Float.parseFloat(JOptionPane.showInputDialog("Input Angle of Rotation (in degrees)"));
float xR = fx(Integer.parseInt(JOptionPane.showInputDialog("Input X Coordinate for Rotation"))),
yR = fx(Integer.parseInt(JOptionPane.showInputDialog("Input Y Coordinate for Rotation")));
xA = rotateX(xA, yA, xR, yR, angle);
yA = rotateY(xA, yA, xR, yR, angle);
xB = rotateX(xB, yB, xR, yR, angle);
yB = rotateY(xB, yB, xR, yR, angle);
xC = rotateX(xC, yC, xR, yR, angle);
yC = rotateY(xC, yC, xR, yR, angle);
rotate = false;
}
if(shift) {
float xShift = -fx(Integer.parseInt(JOptionPane.showInputDialog("Input X Coordinate for Shift"))),
yShift = -fx(Integer.parseInt(JOptionPane.showInputDialog("Input Y Coordinate for Shift")));
xA += xShift;
yA += yShift;
xB += xShift;
yB += yShift;
xC += xShift;
yC += yShift;
shift = false;
}
}
g.setColor(Color.RED);
for (int i = 0; i < 50; i++) {
g.drawLine(iX2(xA), iY2(yA), iX2(xB), iY2(yB));
g.drawLine(iX2(xB), iY2(yB), iX2(xC), iY2(yC));
g.drawLine(iX2(xC), iY2(yC), iX2(xA), iY2(yA));
if(i == 0) {
g.setColor(Color.BLACK);
g.drawString("A: X- " + xA + " Y- " + yA, 0, 50);
g.drawString("B: X- " + xB + " Y- " + yB, 0, 60);
g.drawString("C: X- " + xC + " Y- " + yC, 0, 70);
g.setColor(Color.RED);
}
xA1 = p * xA + q * xB;
yA1 = p * yA + q * yB;
xB1 = p * xB + q * xC;
yB1 = p * yB + q * yC;
xC1 = p * xC + q * xA;
yC1 = p * yC + q * yA;
xA = xA1; xB = xB1; xC = xC1;
yA = yA1; yB = yB1; yC = yC1;
}
if(reset)
angle = 0;
reset = false;
}
public float rotateX(float x, float y, float xR, float yR, float angle) {
angle *= (Math.PI / 180.0);
float c = (float)Math.cos(angle), s = (float)Math.sin(angle),
xF = x - xR, yF = y - yR,
rx = xF * c - yF * s;
return rx + xR;
}
public float rotateY(float x, float y, float xR, float yR, float angle) {
angle *= (Math.PI / 180.0);
float c = (float)Math.cos(angle), s = (float)Math.sin(angle),
xF = x - xR, yF = y - yR,
ry = xF * s + yF * c;
return ry + yR;
}
}
I keep getting this
You are triggering JOptionPane popups inside your paint() method.
Calls to .paint() and its siblings should limit themselves to redrawing the object, nothing else. As is, your code will cause your .paint() method to block until the popup is closed, then continue processing where it left off, potentially picking up artifacts still on the screen. As you can see here, the background is painted (by the call to super.paintComponent()) then the popup is drawn and closed, then the rest of your .paint() method runs, but since the background has already been painted, nothing repaints over where the popup was.
You should move code like:
angle += Float.parseFloat(JOptionPane.showInputDialog("Input Angle of Rotation (in degrees)"));
float xR = fx(Integer.parseInt(JOptionPane.showInputDialog("Input X Coordinate for Rotation"))),
yR = fx(Integer.parseInt(JOptionPane.showInputDialog("Input Y Coordinate for Rotation")));
and
float xShift = -fx(Integer.parseInt(JOptionPane.showInputDialog("Input X Coordinate for Shift"))),
yShift = -fx(Integer.parseInt(JOptionPane.showInputDialog("Input Y Coordinate for Shift")));
out into the appropriate ActionListener methods, set the necessary values, and then use them from within your paint() method.
You should also be consistent about using .paint() and .paintComponent(), like #camickr suggests, don't have one method call its sibling's super.
public void paint(Graphics g) {
super.paintComponent(g);
Don't know if it is the only problem but, custom painting is done by overriding the paintComponent() method:
public void paintComponent(Graphics g) {
super.paintComponent(g);
Edit:
Other comments, not directly related to the problem, but important for proper design:
add("South", buttonPanel);
add("Center", drawingPanel);
Don't use hard coded literals. The layout manager will provide variable you can use. Also, that form of the add(...) method is not recommended (read the API). The new form is:
add(buttonPanel, BordeLayout.PAGE_END);
add("Center", BorderLayout.CENTER);
Don't use static methods and variables. If you want to change a property of your class then create "setter" method. For example create a setter method:
public void setRotate(Boolean rotate)
{
this.rotate = rotate
repaint();
}
Also, not that the setter method invokes the repaint() method. This is because your custom class (not the code that uses the class) should be responsible for doing the repaint.
Then invoke the setter method:
//DrawTriangles.rotate = true; // wrong
drawingPanel.setRotate(true);
Looks like this only happens if dialogs are displayed. I've modified the code and hardcoded some values, it worked without problems.
if(!reset) {
if(rotate) {
angle += Float.parseFloat("15");
float xR = fx(3),
yR = fx(3);
// other stuff...
}
I suggest you try displaying dialogs and setting corresponding values before repainting the components, something similar to this:
shift.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
float xShift = -DrawTriangles.fx(Integer.parseInt(JOptionPane.showInputDialog("Input X Coordinate for Shift"))),
yShift = -DrawTriangles.fx(Integer.parseInt(JOptionPane.showInputDialog("Input Y Coordinate for Shift")));
drawingPanel.xShift = xShift;
drawingPanel.yShift = yShift;
DrawTriangles.shift = true;
drawingPanel.repaint();
}
});
Using BufferedImage corrected the drawing but still the exception occurred.
public void paint(Graphics gg) {
BufferedImage bf = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_RGB);
Graphics2D g = bf.createGraphics();
g.setColor(Color.WHITE);
g.fillRect(0, 0, getWidth(), getHeight());
...
gg.drawImage(bf, 0, 0, null);
if(reset)
angle = 0;
reset = false;
}
I've got a GUI that generates a graphics image using the Mandelbrot set.
I've implemented some zoom buttons, but I'd like to be able to change the centre of my GUI with a mouseclick (make mouse coordinates the new centre-point).
It's proving to be quite difficult. Any suggestions?
My attempt can be found at the moveGraph method.
Thanks in advance!
package assn4_12mgs;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.image.BufferedImage;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
public class MandelBrot extends JFrame{
MandelPanel mp;
double xMax = 2.26;
double xMin = -2.24;
double yMax = 2.26;
double yMin = -2.24;
double yMove, xMove;
static double ESCAPE_MODULUS = 2.0;
static int MAX_ITERATIONS = 32;
public MandelBrot(){
super();
setLayout(new BorderLayout());
setTitle("Graham's Mandelbrot GUI");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setSize(600,700);
setResizable(false);
mp = new MandelPanel();
mp.addMouseListener(new MouseAdapter() {
public void mouseReleased(MouseEvent evt) {
MoveGraph(evt);
}
});
JPanel panel = new JPanel(new FlowLayout());
JButton zoomIn = new JButton("+");
zoomIn.addMouseListener(new MouseAdapter() {
public void mouseReleased(MouseEvent evt) {
ZoomIn(evt);
}
});
JButton zoomOut = new JButton("-");
zoomOut.addMouseListener(new MouseAdapter() {
public void mouseReleased(MouseEvent evt) {
ZoomOut(evt);
}
});
JButton reset = new JButton("Reset");
reset.addMouseListener(new MouseAdapter() {
public void mouseReleased(MouseEvent evt) {
reset(evt);
}
});
panel.add(reset, BorderLayout.WEST); ///How to change positioning?
panel.add(zoomOut, BorderLayout.EAST);
panel.add(zoomIn, BorderLayout.EAST);
add(panel, BorderLayout.SOUTH);
add(mp, BorderLayout.NORTH);
}
private void MoveGraph(MouseEvent evt){
int x = evt.getPoint().x;
int y = evt.getPoint().y;
xMove = x/100;
yMove = y/100;
mp.repaint();
}
private void ZoomIn(MouseEvent evt){
xMax /= 2;
xMin /= 2;
yMax /= 2;
yMin /= 2;
mp.repaint();
}
private void ZoomOut(MouseEvent evt){
xMax *= 2;
xMin *= 2;
yMax *= 2;
yMin *= 2;
mp.repaint();
}
private void reset(MouseEvent evt){
xMax = 2.26;
xMin = -2.24;
yMax = 2.26;
yMin = -2.24;
xMove = 0;
yMove = 0;
mp.repaint();
}
public class MandelPanel extends JPanel {
public MandelPanel() {
setPreferredSize(new Dimension(600,600));
}
#Override
public void paintComponent(Graphics g)
{
super.paintComponent(g);
// draw here
int row, col;
ComplexNumber c, z;
double xPos, yPos;
double modulus = 0;
boolean escaped = false;
int iterations = 0;
int desiredColour;
// Calculate the scale factor to go from pixels to actual units
int height = mp.getHeight(); // drawingZone is the JPanel drawing area
int width = mp.getWidth();
double xScale = (xMax - xMin) / width; // These are min and max values in actual
double yScale = (yMax - yMin) / height; // coordinates.
Graphics2D g2D = (Graphics2D)g;
BufferedImage pretty = new BufferedImage(width, height,
BufferedImage.TYPE_3BYTE_BGR);
// Iterate through the entire panel, pixel by pixel
for (row = 0; row < height; row++) {
// Calculate the actual y position
yPos = yMax - row * yScale;// - yMove
for (col = 0; col < width; col++) {
// Calculate the actual x position
xPos = xMin + col * xScale;// + xMove;
// Create the complex number for this position
c = new ComplexNumber(xPos, yPos);
z = new ComplexNumber(0, 0);
iterations = 0;
// Iterate the fractal equation z = z*z + c
// until z either escapes or the maximum number of iterations
// is reached
do {
z = (z.multiply(z)).add(c);
modulus = z.abs();
escaped = modulus > ESCAPE_MODULUS;
iterations++;
} while (iterations < MAX_ITERATIONS && !escaped);
// Set the colour according to what stopped the above loop
if (escaped) {
desiredColour = setEscapeColour(iterations);
} else {
desiredColour = setColour(modulus);
}
pretty.setRGB(col, row, desiredColour);
} // end for
} // end for
g2D.drawImage(pretty, null, 0, 0);
//yMove = 0;
//xMove = 0;
}
}
// Sets gray level for escape situation
private static int setEscapeColour(int numIterations) {
float grayLevel = 0.5F - (float) numIterations / MAX_ITERATIONS;
grayLevel = Math.max(grayLevel, 0.1F);
return new Color(grayLevel, grayLevel, grayLevel).getRGB();
} // end setEscapeColour
// Sets colour level for interior situation
// The algorithm used here is *totally* empirical!
private static int setColour(double modulus) {
float factor = (float) (modulus / ESCAPE_MODULUS);
float incr = (float) Math.log10(factor * 5.5);
float r = Math.min(Math.abs(10.0F * incr) * factor, 1.0F);
float g = Math.min(Math.abs(6.0F * incr) * factor, 1.0F);
float b = Math.min(Math.abs(0.5F * factor + incr), 1.0F);
return new Color(r, g, b).getRGB();
} // end setColour
public static void main(String args[]){
MandelBrot manBrot = new MandelBrot();
manBrot.setVisible(true);
}
}
We take a point on which a user has clicked as a center point for drawing, then calculate the window (half the distance up and down, left and right) around that point. See
/**
* Calculate real coordinates of the point we clicked.
*/
double xDist = Math.abs(xMax) + Math.abs(xMin);
double yDist = Math.abs(yMax) + Math.abs(yMin);
double xTr = xDist / 600.0 * ((double) e.getX());
double yTr = yDist / 700.0 * ((double) e.getY());
/**
* Calculate the window coordinates. It is relative to the point where the user clicked.
*/
xMax = xTr + xDist/2.0;
xMin = xTr - xDist/2.0;
yMax = yTr + yDist/2.0;
yMin = yTr - yDist/2.0;
** The Code**
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.image.BufferedImage;
import javax.swing.JOptionPane;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
public class MandelBrot
extends JFrame
{
public static class ComplexNumber
{
double re;
double im;
public void setRe(double re)
{
this.re = re;
}
public double re()
{
return re;
}
public void setIm(double im)
{
this.im = im;
}
public double im()
{
return im;
}
public ComplexNumber(double re, double im)
{
this.re = re;
this.im = im;
}
public ComplexNumber add(ComplexNumber c)
{
setRe(re() + c.re());
setIm(im() + c.im());
return this;
}
public ComplexNumber multiply(ComplexNumber z)
{
setRe(re()*z.re() - im()*z.im());
setIm(im()*z.re() - re()*z.im());
return this;
}
public double abs()
{
return Math.sqrt(re()*re() + im()*im());
}
}
MandelPanel mp;
double xMax = 2.26;
double xMin = -2.24;
double yMax = 2.26;
double yMin = -2.24;
double yMove, xMove;
static double ESCAPE_MODULUS = 2.0;
static int MAX_ITERATIONS = 32;
public MandelBrot()
{
super();
setLayout(new BorderLayout());
setTitle("Graham's Mandelbrot GUI");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setSize(600, 700);
setResizable(false);
mp = new MandelPanel();
mp.addMouseListener(new MouseAdapter()
{
public void mouseClicked(MouseEvent e)
{
JOptionPane.showMessageDialog(
null,
"You clicked on a picture area (x,y): (" + e.getX() + "," + e.getY() + ")" ,
"Info",
JOptionPane.INFORMATION_MESSAGE);
double xDist = Math.abs(xMax) + Math.abs(xMin);
double yDist = Math.abs(yMax) + Math.abs(yMin);
double xTr = xDist / 600.0 * ((double) e.getX());
double yTr = yDist / 700.0 * ((double) e.getY());
xMax = xTr + xDist/2.0;
xMin = xTr - xDist/2.0;
yMax = yTr + yDist/2.0;
yMin = yTr - yDist/2.0;
System.out.format("%f %f %f %f %n", xDist, yDist, xTr, yTr);
System.out.format("%f %f %f %f %n", xMin, xMax, yMin, yMax);
mp.repaint();
}
public void mouseReleased(MouseEvent evt)
{
MoveGraph(evt);
}
});
JPanel panel = new JPanel(new FlowLayout());
JButton zoomIn = new JButton("+");
zoomIn.addMouseListener(new MouseAdapter()
{
public void mouseReleased(MouseEvent evt)
{
ZoomIn(evt);
}
});
JButton zoomOut = new JButton("-");
zoomOut.addMouseListener(new MouseAdapter()
{
public void mouseReleased(MouseEvent evt)
{
ZoomOut(evt);
}
});
JButton reset = new JButton("Reset");
reset.addMouseListener(new MouseAdapter()
{
public void mouseReleased(MouseEvent evt)
{
reset(evt);
}
});
panel.add(reset, BorderLayout.WEST); ///How to change positioning?
panel.add(zoomOut, BorderLayout.EAST);
panel.add(zoomIn, BorderLayout.EAST);
add(panel, BorderLayout.SOUTH);
add(mp, BorderLayout.NORTH);
}
private void MoveGraph(MouseEvent evt)
{
int x = evt.getPoint().x;
int y = evt.getPoint().y;
xMove = x / 100;
yMove = y / 100;
mp.repaint();
}
private void ZoomIn(MouseEvent evt)
{
xMax /= 2;
xMin /= 2;
yMax /= 2;
yMin /= 2;
mp.repaint();
}
private void ZoomOut(MouseEvent evt)
{
xMax *= 2;
xMin *= 2;
yMax *= 2;
yMin *= 2;
mp.repaint();
}
private void reset(MouseEvent evt)
{
xMax = 2.26;
xMin = -2.24;
yMax = 2.26;
yMin = -2.24;
xMove = 0;
yMove = 0;
mp.repaint();
}
public class MandelPanel
extends JPanel
{
public MandelPanel()
{
setPreferredSize(new Dimension(600, 600));
}
#Override
public void paintComponent(Graphics g)
{
super.paintComponent(g);
// draw here
int row, col;
ComplexNumber c, z;
double xPos, yPos;
double modulus = 0;
boolean escaped = false;
int iterations = 0;
int desiredColour;
// Calculate the scale factor to go from pixels to actual units
int height = mp.getHeight(); // drawingZone is the JPanel drawing area
int width = mp.getWidth();
double xScale = (xMax - xMin) / width; // These are min and max values in actual
double yScale = (yMax - yMin) / height; // coordinates.
Graphics2D g2D = (Graphics2D) g;
BufferedImage pretty = new BufferedImage(width, height,
BufferedImage.TYPE_3BYTE_BGR);
// Iterate through the entire panel, pixel by pixel
for (row = 0; row < height; row++)
{
// Calculate the actual y position
yPos = yMax - row * yScale;// - yMove
for (col = 0; col < width; col++)
{
// Calculate the actual x position
xPos = xMin + col * xScale;// + xMove;
// Create the complex number for this position
c = new ComplexNumber(xPos, yPos);
z = new ComplexNumber(0, 0);
iterations = 0;
// Iterate the fractal equation z = z*z + c
// until z either escapes or the maximum number of iterations
// is reached
do
{
z = (z.multiply(z)).add(c);
modulus = z.abs();
escaped = modulus > ESCAPE_MODULUS;
iterations++;
}
while (iterations < MAX_ITERATIONS && !escaped);
// Set the colour according to what stopped the above loop
if (escaped)
{
desiredColour = setEscapeColour(iterations);
}
else
{
desiredColour = setColour(modulus);
}
pretty.setRGB(col, row, desiredColour);
} // end for
} // end for
g2D.drawImage(pretty, null, 0, 0);
//yMove = 0;
//xMove = 0;
}
}
// Sets gray level for escape situation
private static int setEscapeColour(int numIterations)
{
float grayLevel = 0.5F - (float) numIterations / MAX_ITERATIONS;
grayLevel = Math.max(grayLevel, 0.1F);
return new Color(grayLevel, grayLevel, grayLevel).getRGB();
} // end setEscapeColour
// Sets colour level for interior situation
// The algorithm used here is *totally* empirical!
private static int setColour(double modulus)
{
float factor = (float) (modulus / ESCAPE_MODULUS);
float incr = (float) Math.log10(factor * 5.5);
float r = Math.min(Math.abs(10.0F * incr) * factor, 1.0F);
float g = Math.min(Math.abs(6.0F * incr) * factor, 1.0F);
float b = Math.min(Math.abs(0.5F * factor + incr), 1.0F);
return new Color(r, g, b).getRGB();
} // end setColour
public static void main(String args[])
{
MandelBrot manBrot = new MandelBrot();
manBrot.setVisible(true);
}
}