简体   繁体   中英

Java Draw Arc Between 2 Points

I'm having trouble drawing the smallest arc described by 3 points: the arc center, an "anchored" end point, and a second point that gives the other end of the arc by determining a radius. I used the law of cosines to determine the length of the arc and tried using atan for the starting degree, but the starting position for the arc is off.

I managed to get the arc to lock onto the anchor point (x1,y1) when it's in Quadrant 2, but that will only work when it is in Quadrant 2.

Solutions I can see all have a bunch of if-statements to determine the location of the 2 points relative to each other, but I'm curious if I'm overlooking something simple. Any help would be greatly appreciated.

SSCCE:

import javax.swing.JComponent;
import javax.swing.JFrame;

import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.geom.*;
import java.awt.*;
import java.util.*;

class Canvas extends JComponent {
    float circleX, circleY, x1, y1, x2, y2, dx, dy, dx2, dy2, radius, radius2;
    Random random = new Random();

    public Canvas() {

        //Setup. 

        x1 = random.nextInt(250);
        y1 = random.nextInt(250);

        //Cant have x2 == circleX
        while (x1 == 150 || y1 == 150)
        {
            x1 = random.nextInt(250);
            y1 = random.nextInt(250);
        }

        circleX = 150; //circle center is always dead center.
        circleY = 150;


        //Radius between the 2 points must be equal.
        dx = Math.abs(circleX-x1);
        dy = Math.abs(circleY-y1);

        //c^2 = a^2 + b^2 to solve for the radius
        radius = (float) Math.sqrt((float)Math.pow(dx, 2) + (float)Math.pow(dy, 2));

        //2nd random point
        x2 = random.nextInt(250);
        y2 = random.nextInt(250);

        //I need to push it out to radius length, because the radius is equal for both points.
        dx2 = Math.abs(circleX-x2);
        dy2 = Math.abs(circleY-y2);
        radius2 = (float) Math.sqrt((float)Math.pow(dx2, 2) + (float)Math.pow(dy2, 2));

        dx2 *= radius/radius2;
        dy2 *= radius/radius2;

        y2 = circleY+dy2;
        x2 = circleX+dx2;
        //Radius now equal for both points.
    }

    public void paintComponent(Graphics g2) {
        Graphics2D g = (Graphics2D) g2;
        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                RenderingHints.VALUE_ANTIALIAS_ON);
        g.setStroke(new BasicStroke(2.0f, BasicStroke.CAP_BUTT,
                BasicStroke.JOIN_BEVEL));

        Arc2D.Float centerPoint = new Arc2D.Float(150-2,150-2,4,4, 0, 360, Arc2D.OPEN);
        Arc2D.Float point1 = new Arc2D.Float(x1-2, y1-2, 4, 4, 0, 360, Arc2D.OPEN);
        Arc2D.Float point2 = new Arc2D.Float(x2-2, y2-2, 4, 4, 0, 360, Arc2D.OPEN);

        //3 points drawn in black
        g.setColor(Color.BLACK);
        g.draw(centerPoint);
        g.draw(point1);
        g.draw(point2);

        float start = 0;
        float distance;

        //Form a right triangle to find the length of the hypotenuse.
        distance = (float) Math.sqrt(Math.pow(Math.abs(x2-x1),2) + Math.pow(Math.abs(y2-y1), 2));

        //Law of cosines to determine the internal angle between the 2 points.
        distance = (float) (Math.acos(((radius*radius) + (radius*radius) - (distance*distance)) / (2*radius*radius)) * 180/Math.PI);

        float deltaY = circleY - y1;
        float deltaX = circleX - x1;

        float deltaY2 = circleY - y2;
        float deltaX2 = circleX - x2;

        float angleInDegrees = (float) ((float) Math.atan((float) (deltaY / deltaX)) * 180 / Math.PI);
        float angleInDegrees2 = (float) ((float) Math.atan((float) (deltaY2 / deltaX2)) * 180 / Math.PI);

        start = angleInDegrees;

        //Q2 works.
        if (x1 < circleX)
        {
            if (y1 < circleY)
            {
                start*=-1;
                start+=180;
            } else if (y2 > circleX) {
                start+=180;
                start+=distance;
            }
        }

        //System.out.println("Start: " + start);
        //Arc drawn in blue
        g.setColor(Color.BLUE);
        Arc2D.Float arc = new Arc2D.Float(circleX-radius,  //Center x 
                                          circleY-radius,  //Center y Rotates around this point.
                                          radius*2,
                                          radius*2,
                                          start, //start degree
                                          distance, //distance to travel
                                          Arc2D.OPEN); //Type of arc.
        g.draw(arc);
    }
}

public class Angle implements MouseListener {

    Canvas view;
    JFrame window;

    public Angle() {
        window = new JFrame();
        view = new Canvas();
        view.addMouseListener(this);
        window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        window.setBounds(30, 30, 400, 400);
        window.getContentPane().add(view);
        window.setVisible(true);
    }

    public static void main(String[] a) {
        new Angle();
    }

    @Override
    public void mouseClicked(MouseEvent arg0) {
        window.getContentPane().remove(view);
        view = new Canvas();
        window.getContentPane().add(view);
        view.addMouseListener(this);
        view.revalidate();
        view.repaint();
    }

    @Override
    public void mouseEntered(MouseEvent arg0) {
        // TODO Auto-generated method stub

    }

    @Override
    public void mouseExited(MouseEvent arg0) {
        // TODO Auto-generated method stub

    }

    @Override
    public void mousePressed(MouseEvent arg0) {
        // TODO Auto-generated method stub

    }

    @Override
    public void mouseReleased(MouseEvent arg0) {
        // TODO Auto-generated method stub

    }
}
package curve;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Line2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import javax.imageio.ImageIO;

public class Main
{

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) throws IOException
    {
        PointF pFrom = new PointF(-10f, 30.0f);
        PointF pTo = new PointF(-100f, 0.0f);
        List<PointF> points = generateCurve(pFrom, pTo, 100f, 7f, true, true);

        System.out.println(points);

        // Calculate the bounds of the curve
        Rectangle2D.Float bounds = new Rectangle2D.Float(points.get(0).x, points.get(0).y, 0, 0);
        for (int i = 1; i < points.size(); ++i) {
            bounds.add(points.get(i).x, points.get(i).y);
        }
        bounds.add(pFrom.x, pFrom.y);
        bounds.add(pTo.x, pTo.y);

        BufferedImage img = new BufferedImage((int) (bounds.width - bounds.x + 50), (int) (bounds.height - bounds.y + 50), BufferedImage.TYPE_4BYTE_ABGR_PRE);
        Graphics2D g = img.createGraphics();
        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

        g.translate(25.0f - bounds.getX(), 25.0f - bounds.getY());
        g.setStroke(new BasicStroke(1.0f));


        g.setColor(Color.DARK_GRAY);
        g.drawLine(-1000, 0, 1000, 0);
        g.drawLine(0, -1000, 0, 1000);

        g.setColor(Color.RED);
        for (int i = 0; i < points.size(); ++i) {
            if (i > 0) {
                Line2D.Float f = new Line2D.Float(points.get(i - 1).x, points.get(i - 1).y, points.get(i).x, points.get(i).y);
                System.out.println("Dist : " + f.getP1().distance(f.getP2()));
//                g.draw(f);
            }

            g.fill(new Ellipse2D.Float(points.get(i).x - 0.8f, points.get(i).y - 0.8f, 1.6f, 1.6f));

        }
        g.setColor(Color.BLUE);
        g.fill(new Ellipse2D.Float(pFrom.x - 1, pFrom.y - 1, 3, 3));
        g.fill(new Ellipse2D.Float(pTo.x - 1, pTo.y - 1, 3, 3));

        g.dispose();

        ImageIO.write(img, "PNG", new File("result.png"));
    }

    static class PointF
    {

        public float x, y;

        public PointF(float x, float y)
        {
            this.x = x;
            this.y = y;
        }

        @Override
        public String toString()
        {
            return "(" + x + "," + y + ")";
        }
    }

    private static List<PointF> generateCurve(PointF pFrom, PointF pTo, float pRadius, float pMinDistance, boolean shortest, boolean side)
    {

        List<PointF> pOutPut = new ArrayList<PointF>();

        // Calculate the middle of the two given points.
        PointF mPoint = new PointF(pFrom.x + pTo.x, pFrom.y + pTo.y);
        mPoint.x /= 2.0f;
        mPoint.y /= 2.0f;
        System.out.println("Middle Between From and To = " + mPoint);


        // Calculate the distance between the two points
        float xDiff = pTo.x - pFrom.x;
        float yDiff = pTo.y - pFrom.y;
        float distance = (float) Math.sqrt(xDiff * xDiff + yDiff * yDiff);
        System.out.println("Distance between From and To = " + distance);

        if (pRadius * 2.0f < distance) {
            throw new IllegalArgumentException("The radius is too small! The given points wont fall on the circle.");
        }

        // Calculate the middle of the expected curve.
        float factor = (float) Math.sqrt((pRadius * pRadius) / ((pTo.x - pFrom.x) * (pTo.x - pFrom.x) + (pTo.y - pFrom.y) * (pTo.y - pFrom.y)) - 0.25f);
        PointF circleMiddlePoint = new PointF(0, 0);
        if (side) {
            circleMiddlePoint.x = 0.5f * (pFrom.x + pTo.x) + factor * (pTo.y - pFrom.y);
            circleMiddlePoint.y = 0.5f * (pFrom.y + pTo.y) + factor * (pFrom.x - pTo.x);
        } else {
            circleMiddlePoint.x = 0.5f * (pFrom.x + pTo.x) - factor * (pTo.y - pFrom.y);
            circleMiddlePoint.y = 0.5f * (pFrom.y + pTo.y) - factor * (pFrom.x - pTo.x);
        }
        System.out.println("Middle = " + circleMiddlePoint);

        // Calculate the two reference angles
        float angle1 = (float) Math.atan2(pFrom.y - circleMiddlePoint.y, pFrom.x - circleMiddlePoint.x);
        float angle2 = (float) Math.atan2(pTo.y - circleMiddlePoint.y, pTo.x - circleMiddlePoint.x);

        // Calculate the step.
        float step = pMinDistance / pRadius;
        System.out.println("Step = " + step);

        // Swap them if needed
        if (angle1 > angle2) {
            float temp = angle1;
            angle1 = angle2;
            angle2 = temp;

        }
        boolean flipped = false;
        if (!shortest) {
            if (angle2 - angle1 < Math.PI) {
                float temp = angle1;
                angle1 = angle2;
                angle2 = temp;
                angle2 += Math.PI * 2.0f;
                flipped = true;
            }
        }
        for (float f = angle1; f < angle2; f += step) {
            PointF p = new PointF((float) Math.cos(f) * pRadius + circleMiddlePoint.x, (float) Math.sin(f) * pRadius + circleMiddlePoint.y);
            pOutPut.add(p);
        }
        if (flipped ^ side) {
            pOutPut.add(pFrom);
        } else {
            pOutPut.add(pTo);
        }

        return pOutPut;
    }
}

and the use the generateCurve method like this to have a curve between the from and to points..

generateCurve(pFrom, pTo, 100f, 7f, true, false);

Okay, here it is, testing and working. The problems were based on the fact that I don't use graphics much, so I have to remind myself that the coordinate systems are backward, and on the fact that the Javadoc description of the Arc2D constructor is atrocious.

In addition to these, I found that your point creation (for the two points to be connected) was extremely inefficient given the requirements. I had assumed you actually had to receive two arbitrary points and then calculate their angles, etc., but based on what you put on Pastebin, we can define the two points however we please. This benefits us.

Anyway, here's a working version, with none of that gobbledegook from before. Simplified code is simplified:

import javax.swing.JComponent;
import java.awt.geom.*;
import java.awt.*;
import java.util.*;

public class Canvas extends JComponent {
    double circleX, circleY, x1, y1, x2, y2, dx, dy, dx2, dy2, radius, radius2;
    Random random = new Random();
    double distance;
    private static double theta1;
    private static double theta2;
    private static double theta;
    // private static double radius;
    private Point2D point1;
    private Point2D point2;
    private Point2D center;
    private static int direction;
    private static final int CW = -1;
    private static final int CCW = 1;

public Canvas() {
    /*
     * You want two random points on a circle, so let's start correctly,
     * by setting a random *radius*, and then two random *angles*.
     * 
     * This has the added benefit of giving us the angles without having to calculate them
     */

    radius = random.nextInt(175);   //your maximum radius is higher, but we only have 200 pixels in each cardinal direction
    theta1 = random.nextInt(360);   //angle to first point (absolute measurement)
    theta2 = random.nextInt(360);   //angle to second point

    //build the points
    center = new Point2D.Double(200, 200);  //your frame is actually 400 pixels on a side
    point1 = new Point2D.Double(radius * Math.cos(toRadians(theta1)) + center.getX(), center.getY() - radius * Math.sin(toRadians(theta1)));
    point2 = new Point2D.Double(radius * Math.cos(toRadians(theta2)) + center.getX(), center.getY() - radius * Math.sin(toRadians(theta2)));

    theta = Math.abs(theta1 - theta2) <= 180 ? Math.abs(theta1 - theta2) : 360 - (Math.abs(theta1 - theta2));

    if ((theta1 + theta) % 360 == theta2) {
        direction = CCW;
    } else {
        direction = CW;
    }

    System.out.println("theta1: " + theta1 + "; theta2: " + theta2 + "; theta: " + theta + "; direction: " + (direction == CCW ? "CCW" : "CW"));
    System.out.println("point1: (" + (point1.getX() - center.getX()) + ", " + (center.getY() - point1.getY()) + ")");
    System.out.println("point2: (" + (point2.getX() - center.getX()) + ", " + (center.getY() - point2.getY()) + ")");

    // Radius now equal for both points.
}

public double toRadians(double angle) {
    return angle * Math.PI / 180;
}

public double toDegrees(double angle) {
    return angle * 180 / Math.PI;
}

public void paintComponent(Graphics g2) {
    Graphics2D g = (Graphics2D) g2;
    g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
            RenderingHints.VALUE_ANTIALIAS_ON);
    g.setStroke(new BasicStroke(2.0f, BasicStroke.CAP_BUTT,
            BasicStroke.JOIN_BEVEL));

    //centerpoint should be based on the actual center point
    Arc2D.Double centerPoint = new Arc2D.Double(center.getX() - 2, center.getY() - 2, 4, 4, 0,
            360, Arc2D.OPEN);
    //likewise these points
    Arc2D.Double point11 = new Arc2D.Double(point1.getX() - 2, point1.getY() - 2, 4, 4, 0, 360,
            Arc2D.OPEN);
    Arc2D.Double point22 = new Arc2D.Double(point2.getX() - 2, point2.getY() - 2, 4, 4, 0, 360,
            Arc2D.OPEN);

    // 3 points drawn in black
    g.setColor(Color.BLACK);
    g.draw(centerPoint);
    g.draw(point11);
    g.draw(point22);

    // Arc drawn in blue
    g.setColor(Color.BLUE);
    g.draw(new Arc2D.Double(center.getX() - radius, center.getY() - radius, 2 * radius, 2 * radius, theta1, theta * direction, Arc2D.OPEN));
}

}

Perhaps this will help. It tests with click and drag to set the two points rather than random numbers. It's considerably simpler than what you were attempting and other solutions posted so far.

Notes:

  • Math.atan2() is a friend in problems like this.
  • Little helper functions make it easier to reason about your code.
  • It's best practice to use instance variables for independent values only and compute the dependent values in local variables.
  • My code fixes some Swing usage problems like calling Swing functions from the main thread.

Code follows:

import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import javax.swing.*;
import javax.swing.event.MouseInputAdapter;

class TestCanvas extends JComponent {

    float x0 = 150f, y0 = 150f;   // Arc center. Subscript 0 used for center throughout.
    float xa = 200f, ya = 150f;   // Arc anchor point.  Subscript a for anchor.
    float xd = 150f, yd =  50f;   // Point determining arc angle. Subscript d for determiner.

    // Return the distance from any point to the arc center.
    float dist0(float x, float y) {
        return (float)Math.sqrt(sqr(x - x0) + sqr(y - y0));
    }

    // Return polar angle of any point relative to arc center.
    float angle0(float x, float y) {
        return (float)Math.toDegrees(Math.atan2(y0 - y, x - x0));
    }

    @Override
    protected void paintComponent(Graphics g0) {
        Graphics2D g = (Graphics2D) g0;

        // Can always draw the center point.
        dot(g, x0, y0);

        // Get radii of anchor and det point.
        float ra = dist0(xa, ya);
        float rd = dist0(xd, yd);

        // If either is zero there's nothing else to draw.
        if (ra == 0 || rd == 0) { return; }

        // Get the angles from center to points.
        float aa = angle0(xa, ya);
        float ad = angle0(xd, yd);  // (xb, yb) would work fine, too.

        // Draw the arc and other dots.
        g.draw(new Arc2D.Float(x0 - ra, y0 - ra, // box upper left
                2 * ra, 2 * ra,                  // box width and height
                aa, angleDiff(aa, ad),           // angle start, extent 
                Arc2D.OPEN));
        dot(g, xa, ya);

        // Use similar triangles to get the second dot location.
        float xb = x0 + (xd - x0) * ra / rd;
        float yb = y0 + (yd - y0) * ra / rd;
        dot(g, xb, yb);
    }

    // Some helper functions.

    // Draw a small dot with the current color.
    static void dot(Graphics2D g, float x, float y) {
        final int rad = 2;
        g.fill(new Ellipse2D.Float(x - rad, y - rad, 2 * rad, 2 * rad));
    }

    // Return the square of a float.
    static float sqr(float x) { return x * x; }

    // Find the angular difference between a and b, -180 <= diff < 180.
    static float angleDiff(float a, float b) {
        float d = b - a;
        while (d >= 180f) { d -= 360f; }
        while (d < -180f) { d += 360f; }
        return d;
    }

    // Construct a test canvas with mouse handling.
    TestCanvas() {
        addMouseListener(mouseListener);
        addMouseMotionListener(mouseListener);
    }

    // Listener changes arc parameters with click and drag.
    MouseInputAdapter mouseListener = new MouseInputAdapter() {
        boolean mouseDown = false; // Is left mouse button down?

        @Override
        public void mousePressed(MouseEvent e) {
            if (e.getButton() == MouseEvent.BUTTON1) {
                mouseDown = true;
                xa = xd = e.getX();
                ya = yd = e.getY();
                repaint();
            }
        }

        @Override
        public void mouseReleased(MouseEvent e) {
            if (e.getButton() == MouseEvent.BUTTON1) {
                mouseDown = false;
            }
        }

        @Override
        public void mouseDragged(MouseEvent e) {
            if (mouseDown) {
                xd = e.getX();
                yd = e.getY();
                repaint();
            }
        }
    };
}

public class Test extends JFrame {

    public Test() {
        setSize(400, 400);
        setLocationRelativeTo(null);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        getContentPane().add(new TestCanvas());
    }

    public static void main(String[] args) {
        // Swing code must run in the UI thread, so
        // must invoke setVisible rather than just calling it.
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                new Test().setVisible(true);
            }
        });
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM