How to rotate an arrow inside a circle using Java2d?

Asked

Viewed 622 times

4

Previously, I asked "How to draw an arrow using Java2d?" and now with the arrow drawn correctly and positioned within my circle, I would like to make the arrow rotate within the circle so that the center of the circle is the fixed frame of one of the arrow tips.

They told me that to calculate the rotation, I would need to use the formula:

A={x+L cos(θ),y+L sin(θ)}

where x and y are the coordinates at the new turning point, L would be the size of the arrow and θ would be the angle of the turn.

Although I understand the formula, I’m not able to apply in my class LineArrow, because I draw it based on coordinates, and as it can change angle, I’m not sure how to calculate its size, regardless of the position in which it is in the circle. In the example I used the vertical position, but this position could be any one. I also do not know if it is the ideal to be applied in this code.

My class LineArrow is as follows:

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Polygon;
import java.awt.geom.AffineTransform;

public class LineArrow {

    private int x;
    private int y;
    private int endX;
    private int endY;
    private Color color;
    private int thickness;
    private static final Polygon ARROW_HEAD = new Polygon();

    static {
        ARROW_HEAD.addPoint(0, 0);
        ARROW_HEAD.addPoint(-5, -10);
        ARROW_HEAD.addPoint(5, -10);
    }

    public LineArrow(int x, int y, int x2, int y2, Color color, int thickness) {
        super();
        this.x = x;
        this.y = y;
        this.endX = x2;
        this.endY = y2;

        this.color = color;
        this.thickness = thickness;
    }

    public void draw(Graphics g) {

        Graphics2D g2 = (Graphics2D) g;

        // Calcula o ângulo da seta.
        double angle = Math.atan2(endY - y, endX - x);

        g2.setColor(color);
        g2.setStroke(new BasicStroke(thickness));

        // Desenha a linha. Corta 10 pixels na ponta para a ponta não ficar
        // grossa.
        g2.drawLine(x, y, (int) (endX - 10 * Math.cos(angle)), (int) (endY - 10 * Math.sin(angle)));

        // Obtém o AffineTransform original.
        AffineTransform tx1 = g2.getTransform();

        // Cria uma cópia do AffineTransform.
        AffineTransform tx2 = (AffineTransform) tx1.clone();

        // Translada e rotaciona o novo AffineTransform.
        tx2.translate(endX, endY);
        tx2.scale(thickness / 2, thickness / 2);
        tx2.rotate(angle - Math.PI / 2);

        // Desenha a ponta com o AffineTransform transladado e rotacionado.
        g2.setTransform(tx2);
        g2.fill(ARROW_HEAD);

        // Restaura o AffineTransform original.
        g2.setTransform(tx1);
    }

    public void spin() {
        // ????
    }
}

Here is a compileable example:

import java.awt.*;
import java.awt.geom.AffineTransform;
import javax.swing.*;
import javax.swing.border.EmptyBorder;

public class SpinArrowTest extends JFrame {

    private static final long serialVersionUID = 1L;
    private JPanel contentPane;
    private JPanel board;
    private JPanel controlsPane;
    private JButton rotateButton;

    public static void main(String[] args) {
        EventQueue.invokeLater(() -> new SpinArrowTest().setVisible(true));
    }

    public SpinArrowTest() {
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setPreferredSize(new Dimension(400, 300));
        this.contentPane = new JPanel();
        this.contentPane.setBorder(new EmptyBorder(5, 5, 5, 5));
        this.contentPane.setLayout(new BorderLayout(0, 0));
        setContentPane(this.contentPane);

        this.board = new Board();

        this.contentPane.add(this.board, BorderLayout.CENTER);

        this.controlsPane = new JPanel(new GridLayout(0, 1, 0, 0));
        this.controlsPane.setBorder(new EmptyBorder(5, 1, 1, 1));

        this.rotateButton = new JButton("Rotate");
        this.rotateButton.addActionListener(e -> {

        });
        this.controlsPane.add(this.rotateButton);

        this.contentPane.add(this.controlsPane, BorderLayout.SOUTH);
        pack();
    }
}

Class Main panel where the animation and drawing will take place:

class Board extends JPanel {

    private static final long serialVersionUID = 1L;
    private Circle circle;
    private LineArrow line;

    public void spin() {
        line.spin();
        repaint();
    }

    @Override
    protected void paintComponent(Graphics g) {

        super.paintComponent(g);

        int widthRectangle = getWidth();
        int heightReclangle = getHeight();

        int x, y, diameter;

        if (widthRectangle <= heightReclangle) {
            diameter = widthRectangle;
            y = heightReclangle / 2 - diameter / 2;
            x = 0;
        } else {
            diameter = heightReclangle;
            x = widthRectangle / 2 - diameter / 2;
            y = 0;

        }
        circle = new Circle(x, y, diameter, Color.red);
        circle.draw(g);

        line = new LineArrow(x + diameter / 2, y + diameter / 2, x + diameter / 2, y + diameter, Color.white, 3);
        line.draw(g);
    }
}

Class representing the circle:

class Circle {

    int x;
    int y;
    int diameter;
    Color color;

    public Circle(int x, int y, int diameter, Color color) {
        super();
        this.x = x;
        this.y = y;
        this.diameter = diameter;
        this.color = color;
    }

    public void draw(Graphics g) {

        Graphics2D g2 = (Graphics2D) g;
        g2.setColor(color);
        g2.setPaint(new GradientPaint(x, y, color, x + diameter / 2, y + diameter / 2, color.darker()));
        g2.fillOval(x, y, diameter, diameter);
    }

}

Class representing the arrow that will rotate within the circle

class LineArrow {

    private int x;
    private int y;
    private int endX;
    private int endY;
    private Color color;
    private int thickness;
    private static final Polygon ARROW_HEAD = new Polygon();

    static {
        ARROW_HEAD.addPoint(0, 0);
        ARROW_HEAD.addPoint(-5, -10);
        ARROW_HEAD.addPoint(5, -10);
    }

    public LineArrow(int x, int y, int x2, int y2, Color color, int thickness) {
        super();
        this.x = x;
        this.y = y;
        this.endX = x2;
        this.endY = y2;

        this.color = color;
        this.thickness = thickness;
    }

    public void draw(Graphics g) {

        Graphics2D g2 = (Graphics2D) g;

        // Calcula o ângulo da seta.
        double angle = Math.atan2(endY - y, endX - x);

        g2.setColor(color);
        g2.setStroke(new BasicStroke(thickness));

        // Desenha a linha. Corta 10 pixels na ponta para a ponta não ficar
        // grossa.
        g2.drawLine(x, y, (int) (endX - 10 * Math.cos(angle)), (int) (endY - 10 * Math.sin(angle)));

        // Obtém o AffineTransform original.
        AffineTransform tx1 = g2.getTransform();

        // Cria uma cópia do AffineTransform.
        AffineTransform tx2 = (AffineTransform) tx1.clone();

        // Translada e rotaciona o novo AffineTransform.
        tx2.translate(endX, endY);
        tx2.scale(thickness / 2, thickness / 2);
        tx2.rotate(angle - Math.PI / 2);

        // Desenha a ponta com o AffineTransform transladado e rotacionado.
        g2.setTransform(tx2);
        g2.fill(ARROW_HEAD);

        // Restaura o AffineTransform original.
        g2.setTransform(tx1);
    }

    public void spin() {
        // ????
    }
}

The result is the figure below (static):

inserir a descrição da imagem aqui

2 answers

4


You extract the angle from the positions of the arrows and want to rotate the positions. A better approach is to do the opposite: rotate the angle and from the angle extract the position.

Follow the complete code. Clicking the Rotate button will rotate the arrow by 10 degrees clockwise:

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.GridLayout;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.border.EmptyBorder;

public class SpinArrowTest extends JFrame {

    private static final long serialVersionUID = 1L;

    public static void main(String[] args) {
        EventQueue.invokeLater(SpinArrowTest::new);
    }

    public SpinArrowTest() {
        setTitle("Clique no botão Rotate");
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setPreferredSize(new Dimension(400, 300));
        JPanel contentPane = new JPanel();
        contentPane.setBorder(new EmptyBorder(5, 5, 5, 5));
        contentPane.setLayout(new BorderLayout(0, 0));
        setContentPane(contentPane);

        Board board = new Board();

        contentPane.add(board, BorderLayout.CENTER);

        JPanel controlsPane = new JPanel(new GridLayout(0, 1, 0, 0));
        controlsPane.setBorder(new EmptyBorder(5, 1, 1, 1));

        JButton rotateButton = new JButton("Rotate");
        rotateButton.addActionListener(e -> board.spin());
        controlsPane.add(rotateButton);

        contentPane.add(controlsPane, BorderLayout.SOUTH);
        pack();
        setLocationRelativeTo(null);
        setVisible(true);
    }
}
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import javax.swing.JPanel;

public class Board extends JPanel {

    private static final long serialVersionUID = 1L;
    private double angleDegrees;

    public Board() {
        angleDegrees = 90;
    }

    public void spin() {
        angleDegrees += 10;
        angleDegrees %= 360;
        repaint();
    }

    @Override
    protected void paintComponent(Graphics g) {
        Graphics2D g2 = (Graphics2D) g;
        g2.addRenderingHints(new RenderingHints(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON));

        super.paintComponent(g2);

        int widthRectangle = getWidth();
        int heightReclangle = getHeight();

        int x, y, diameter;

        if (widthRectangle <= heightReclangle) {
            diameter = widthRectangle;
            y = heightReclangle / 2 - diameter / 2;
            x = 0;
        } else {
            diameter = heightReclangle;
            x = widthRectangle / 2 - diameter / 2;
            y = 0;
        }
        Circle circle = new Circle(x, y, diameter, Color.red);
        circle.draw(g2);

        LineArrow line = new LineArrow(x + diameter / 2, y + diameter / 2, angleDegrees, diameter / 2, Color.white, 3, 20);
        line.draw(g2);
    }
}
import java.awt.Color;
import java.awt.GradientPaint;
import java.awt.Graphics2D;

public class Circle {

    private final int x;
    private final int y;
    private final int diameter;
    private final Color color;

    public Circle(int x, int y, int diameter, Color color) {
        super();
        this.x = x;
        this.y = y;
        this.diameter = diameter;
        this.color = color;
    }

    public void draw(Graphics2D g2) {
        g2.setColor(color);
        g2.setPaint(new GradientPaint(x, y, color, x + diameter / 2, y + diameter / 2, color.darker()));
        g2.fillOval(x, y, diameter, diameter);
    }
}
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Polygon;
import java.awt.geom.AffineTransform;

public class LineArrow {

    private final int x;
    private final int y;
    private final int endX;
    private final int endY;
    private final double angleRadians;
    private final Color color;
    private final int thickness;
    private final double scale;

    private static final int TRIANGLE_LENGTH = 2;
    private static final Polygon ARROW_HEAD = new Polygon();

    static {
        ARROW_HEAD.addPoint(TRIANGLE_LENGTH, 0);
        ARROW_HEAD.addPoint(0, -TRIANGLE_LENGTH / 2);
        ARROW_HEAD.addPoint(0, TRIANGLE_LENGTH / 2);
    }

    public LineArrow(int x, int y, double angleDegrees, int length, Color color, int thickness, int headSize) {
        super();
        this.x = x;
        this.y = y;
        this.color = color;
        this.thickness = thickness;

        // Converte o ângulo para radianos.
        this.angleRadians = Math.toRadians(angleDegrees);

        // Calcula a escala a ser aplicada ao desenhar a ponta.
        this.scale = headSize / TRIANGLE_LENGTH;

        // Calcula a posição final da linha de acordo com o ângulo e com o
        // comprimento. Corta do comprimento o tamanho da ponta.
        this.endX = (int) (x + (length - headSize) * Math.cos(angleRadians));
        this.endY = (int) (y + (length - headSize) * Math.sin(angleRadians));
    }

    public void draw(Graphics2D g2) {
        // Define a cor e a espessura da linha.
        g2.setColor(color);
        g2.setStroke(new BasicStroke(thickness));

        // Desenha a linha.
        g2.drawLine(x, y, endX, endY);

        // Obtém o AffineTransform original.
        AffineTransform tx1 = g2.getTransform();

        // Cria uma cópia do AffineTransform.
        AffineTransform tx2 = (AffineTransform) tx1.clone();

        // Translada e rotaciona o novo AffineTransform.
        tx2.translate(endX, endY);
        tx2.scale(scale, scale);
        tx2.rotate(angleRadians);

        // Desenha a ponta com o AffineTransform transladado e rotacionado.
        g2.setTransform(tx2);
        g2.fill(ARROW_HEAD);

        // Restaura o AffineTransform original.
        g2.setTransform(tx1);
    }
}

Here a screenshot after clicking 3 times on the Rotate button and rotate 30 degrees:

Screenshot 1

Works perfectly if the screen is resized:

Screenshot 2

Screenshot 3

Changes I’ve made:

  • Do not use instance variables for what can be done with local variables.

  • You will hardly want to use package visibility. So don’t forget the modifiers private.

  • I did the classes Circle and SpinArrow are immutable. Dealing with immutable classes is usually simpler than changing classes. The angle, which is mutable, stays in the class Board.

  • Like the SpinArrow is unchanging, he has no method spin(). What happens is that this object has a short life, being instantiated within the method paintComponent(Graphics) of Board already with its definitive values and being discarded to the garbage collector in this same method.

  • I decoupled the tip size of the line thickness.

  • You know that "- Math.PI / 2" in the rotate? That is a scam. The reason is that the triangle was set downward when it should be to the right, and that 90-degree rotation (π / 2) fixes that. The best thing would be to have him face the right way so I don’t have to spin him 90 degrees later, and that’s what I did. I’m sorry I didn’t realize that yesterday.

  • In your previous question I had set the triangle to size 10 because it would be 10 pixels in size. Now that its size can be specified by the class Board (which is even using 20 pixels), it uses an independent pixel scale. The scale he uses is two units of length and one unit of width for each side.

  • Before, he calculated the final position of the line (endX and endY) by picking up the desired final position and subtracting the arrow length. And then the arrow was drawn with the tip in the desired position (so one of the vertices of the triangle was in (0, 0)). Now, as the endX and the endY are the end of the length of the pointless line, so the triangle is set to be drawn from that position, and so the point (0, 0) is now at the base.

  • Note that the endX and the endY is defined with the being and the co-sine of the given angle.

  • I added antialiasing on Board. Both the circle and arrow are much better with this.

  • 1

    This approach dispenses with the question formula, less mathematical! (hehe

  • 1

    The interesting thing is, I thought I was going to have to sacrifice responsiveness, but that way, everything stays resized and looks the same. And I didn’t have to worry about deducing the size of LineArrow to calculate the angle.

  • A doubt: By "immutable", you mean that the classes that represent the two figures can have nothing changed after being instantiated, except the angle in the case of the Linearrow class?

  • @An unchanging class is like java.lang.String or the java.lang.Integer. The values of all instance variables are defined only in the constructor and never change.

  • @Nor the angle of LineArrow can be changed. What happens is that a new instance of LineArrow with a different angle is created in the Board.

  • I see you’ve created an instance of Graphics2D on the Board to apply anti-aliasing, but Linearrow and Circle continue to receive Graphics and doing that same casting within them. If I change the two classes to receive the Graphics2d you already create on the Board it would be an optimization or it is indifferent?

  • 1

    @Article Actually I didn’t create an instance, I just used a cast. It’s the same object (ie, g == g2). If you want, you can change the type of the Circle and of LineArrow for Graphics2D to make it simpler. I wouldn’t call it optimization, but simplification. In fact, I’ll edit the answer to do just that.

Show 2 more comments

3

To apply the formula

A={x+L×cos(θ),y+L×sin(θ)}

in your case, A is the end point of the straight. So:

angle += 0.2;
endX = (int) (x+length*Math.cos(angle));
endY = (int) (y+length*Math.sin(angle));

angle is so much that you want the straight "jump" every "Rotate".

Code:

import java.awt.*;
import java.awt.geom.AffineTransform;
import javax.swing.*;
import javax.swing.border.EmptyBorder;

public class SpinArrowTest extends JFrame {

    private static final long serialVersionUID = 1L;
    private JPanel contentPane;
    private JPanel board;
    private JPanel controlsPane;
    private JButton rotateButton;

    public static void main(String[] args) {
        EventQueue.invokeLater(() -> new SpinArrowTest().setVisible(true));
    }

    public SpinArrowTest() {
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setPreferredSize(new Dimension(400, 300));
        this.contentPane = new JPanel();
        this.contentPane.setBorder(new EmptyBorder(5, 5, 5, 5));
        this.contentPane.setLayout(new BorderLayout(0, 0));
        setContentPane(this.contentPane);

        this.board = new Board();

        this.contentPane.add(this.board, BorderLayout.CENTER);

        this.controlsPane = new JPanel(new GridLayout(0, 1, 0, 0));
        this.controlsPane.setBorder(new EmptyBorder(5, 1, 1, 1));

        this.rotateButton = new JButton("Rotate");
        this.rotateButton.addActionListener(e -> {
            ((Board)board).spin();
        });
        this.controlsPane.add(this.rotateButton);

        this.contentPane.add(this.controlsPane, BorderLayout.SOUTH);
        pack();
        ((Board)board).init();
    }
}

// painel principal onde ocorrerá a animação e desenho

class Board extends JPanel {

    private static final long serialVersionUID = 1L;
    private Circle circle;
    private LineArrow line;
    int widthRectangle;
    int heightReclangle;

    int x, y, diameter;

    public void init(){
        int widthRectangle = getWidth();
        int heightReclangle = getHeight();
        if (widthRectangle <= heightReclangle) {
            diameter = widthRectangle;
            y = heightReclangle / 2 - diameter / 2;
            x = 0;
        } else {
            diameter = heightReclangle;
            x = widthRectangle / 2 - diameter / 2;
            y = 0;

        }
        circle = new Circle(x, y, diameter, Color.red);
        line = new LineArrow(x + diameter / 2, y + diameter / 2, x + diameter, y  + diameter / 2, Color.white, 3);
    }
    public void spin() {
        line.spin();
        repaint();
    }

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        circle.draw(g);
        line.draw(g);
    }


}

// CLASSE QUE REPRESENTA O CIRCULO

class Circle {

    int x;
    int y;
    int diameter;
    Color color;

    public Circle(int x, int y, int diameter, Color color) {
        super();
        this.x = x;
        this.y = y;
        this.diameter = diameter;
        this.color = color;
    }

    public void draw(Graphics g) {

        Graphics2D g2 = (Graphics2D) g;
        g2.setColor(color);
        g2.setPaint(new GradientPaint(x, y, color, x + diameter / 2, y + diameter / 2, color.darker()));
        g2.fillOval(x, y, diameter, diameter);
    }

}

// CLASSE QUE REPRESENTA A SETA QUE IRÁ GIRAR DENTRO DO CIRCULO

class LineArrow {

    private int x;
    private int y;
    private int endX;
    private int endY;
    private double length;
    private double angle;
    private Color color;
    private int thickness;
    private static final Polygon ARROW_HEAD = new Polygon();

    static {
        ARROW_HEAD.addPoint(0, 0);
        ARROW_HEAD.addPoint(-5, -10);
        ARROW_HEAD.addPoint(5, -10);
    }

    public LineArrow(int x, int y, int x2, int y2, Color color, int thickness) {
        super();
        this.x = x;
        this.y = y;
        this.endX = x2;
        this.endY = y2;
        angle = Math.atan2(endY - y, endX - x);

        this.color = color;
        this.thickness = thickness;

        int a = endX-x;
        int b = endY-y;
        length = Math.sqrt(a*a + b*b);
    }

    public void draw(Graphics g) {

        Graphics2D g2 = (Graphics2D) g;

        // Calcula o ângulo da seta.
        double angle = Math.atan2(endY - y, endX - x);

        g2.setColor(color);
        g2.setStroke(new BasicStroke(thickness));

        // Desenha a linha. Corta 10 pixels na ponta para a ponta não ficar
        // grossa.
        g2.drawLine(x, y, (int) (endX - 10 * Math.cos(angle)), (int) (endY - 10 * Math.sin(angle)));

        // Obtém o AffineTransform original.
        AffineTransform tx1 = g2.getTransform();

        // Cria uma cópia do AffineTransform.
        AffineTransform tx2 = (AffineTransform) tx1.clone();

        // Translada e rotaciona o novo AffineTransform.
        tx2.translate(endX, endY);
        tx2.scale(thickness / 2, thickness / 2);
        tx2.rotate(angle - Math.PI / 2);

        // Desenha a ponta com o AffineTransform transladado e rotacionado.
        g2.setTransform(tx2);
        g2.fill(ARROW_HEAD);

        // Restaura o AffineTransform original.
        g2.setTransform(tx1);


    }

    public void spin() {
        angle += 0.2;
        endX = (int) (x+length*Math.cos(angle));
        endY = (int) (y+length*Math.sin(angle));
    }
}

p.s.: I don’t have much experience with swing so I don’t know if the init(); after the pack() is the best way to initialize the Board values. But it has to be separated from the spin().

  • I’ve done it, and it doesn’t work. Alias, length is not only the distance between points y, since if the line is horizontal, this account will fail

  • Hi Articuno. I copied and pasted the code above a class I executed and rotates. Can copy and paste (the full code), and trust =D

  • As I said length is not always the difference between the y’s points. I started the arrow vertically just for demonstration, it doesn’t mean it will always start in this position.

  • Oh yes, that’s in my comment: length = endY - y; // n deve pegar assim, só tirando vantagem de que é uma reta na vertical. His doubt seemed to be about how to apply the rotation formula.

  • The distance should be calculated using the good old of Italics. When you have a while I can see this.

  • So, but the question is not only how to rotate, but how to calculate the size independent of the position of the arrow. So no matter how I boot it, the angle will always be calculated correctly. Another detail, I can not give init after pack(), this can give me :/

  • The angle is always calculated correctly. The only variable is how to catch length.

  • I updated my answer so that length is calculated regardless of where the straight starts (note that now, for example, it starts horizontal).

  • A doubt, because endX = (int)(x+length*Math.cos(angle));endY = (int) (y+length*Math.sin(angle)); and not endX = (int) (endX+length*Math.cos(angle)); endY = (int) (endY+length*Math.sin(angle));? I had understood that the points of the formula should be the ones that moved, so only the points of the tip.

  • The end points of the line are those that move. Their location is calculated from the fixed point (x,y), the center of the circle, and length the length of the line. I find it easy to think of length as being a "rod" with a fixed tip on (x,y) and running Angle. And the formula says where the other end of the rod should go (endX, Endy).

  • Maria thank you for your helpfulness, the calculations really worked, but now my screen is no longer adaptive. I believe the reason is this init() after pack(). If you test the example of the question, the circle and arrow fit as they resize to any side. now they stay fixed.

Show 6 more comments

Browser other questions tagged

You are not signed in. Login or sign up in order to post.