How do I rotate an arrow within a circle using Java2D?

5

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 have the arrow turn inside the circle so that the center of the circle is the fixed reference point of one of the arrowheads.

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

  

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

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

Although I understand the formula, I'm not sure how to apply in my class LineArrow , even because I draw it based on coordinates, and how it can change angle, I'm not sure how to calculate its size, regardless of position where 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 apply 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's a compilable 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 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):

    
asked by anonymous 10.10.2017 / 13:39

2 answers

4

You extract the angle from the positions of the arrows and you 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 clockwise by 10 degrees:

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 the rotate button 3 times and turning 30 degrees:

Worksperfectlyifthecanvasisresized:

ChangesI'vemade:

  • Donotuseinstancevariablesforwhatcanbedonewithlocalvariables.

  • Youwillhardlywanttousepackagevisibility.Sodonotforgettheprivatemodifiers.

  • ImadetheCircleandSpinArrowclassesimmutable.Dealingwithimmutableclassesisoftensimplerthanwithchangeableclasses.Theangle,whichischangeable,isintheBoardclass.

  • BecauseSpinArrowisimmutable,itdoesnothaveaspin()method.Whathappensisthatthisobjecthasashortlife,beinginstantiatedwithinthemethodpaintComponent(Graphics)ofBoardalreadywithitsdefinitivevaluesandbeingdiscardedtothegarbagecollectorinthatsamemethod.

  • Idisengagedthetipsizeofthelinethickness.

  • Doyouknowthat" - Math.PI / 2 " in rotate ? That is a gambiarra. The reason is that the triangle was set face down when it should be to the right, and that 90 degree rotation (π / 2) fixes that. The best would be to set it back to the right side so you do not have to turn it 90 degrees later, and that's what I did. I'm sorry I did not realize that yesterday.

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

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

  • Note that endX and endY is defined with the being and cosine of the given angle.

  • I added antialiasing on Board . Both the circle and the arrow look much better with it.

10.10.2017 / 19:53
3

To apply the formula

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

In your case, A is the end of the line. 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 line to "jump" to each "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));
    }
}

Q: I do not have much swing experience so I do not know if init(); after pack() is the best way to initialize Board values. But it has to be separated from spin() .

    
10.10.2017 / 18:56