Search code examples
javatrigonometrygraphics2d

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

    }
}

Solution

  • 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);
                }
            });
        }
    }