How to make the arrow turn several times until it stops by itself after clicking the button?

7

In the question " How do I rotate an arrow within a circle using Java2D? , I was able to learn how to do the arrow rotates within the circle. But I need to make the arrow spin like a casino roulette, until I stop by myself.

I thought about using SwingWorker or separating into Thread . I have read some interesting suggestions in this answer on SOEn but I am not able to apply it in my code.

I tried to create a Thread in the ActionListener of the rotateButton button but I could not timer it to rotate the arrow automatically with a click. I thought about using swing.Timer , but I could not automate it to stop without intervention too.

How do I do this?

Here is a compilable example from the question answer linked at the beginning of this post:

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;

    public static void main(String[] args) {

        try {
            UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
        } catch (ClassNotFoundException | InstantiationException | IllegalAccessException
                | UnsupportedLookAndFeelException e) {
            e.printStackTrace();
        }

        EventQueue.invokeLater(() -> new SpinArrowTest().setVisible(true));
    }

    public SpinArrowTest() {

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

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

class Board extends JPanel {

    private static final long serialVersionUID = 1L;
    private double angleDegrees; // Em graus.

    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(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 circle = new Circle(x, y, diameter, Color.red);
        circle.draw(g);

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

//CLASSE QUE REPRESENTA O CIRCULO

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(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 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(Graphics g) {

        Graphics2D g2 = (Graphics2D) g;

        // 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);
    }
}
    
asked by anonymous 11.10.2017 / 03:28

1 answer

4

This one here was challenging and also fun. I combined these things:

  • SwingWorker with interim results .

  • Concepts of package java.util.concurrent .

  • Some concepts that I learned working with games such as separating drawing from processing and doing processing by ticks.

  • A little bit of physics (kinematics). After all, if geometry was already complicated, physics is even better!

In particular, what I did was:

  • Use the class ScheduledExecutorService to create a task to run periodically over a fixed time interval (this interval is called tick ). I get an instance of it through the Executors.newScheduledThreadPool(int) .

  • Use an instance of AtomicReference to hold a double value that can be changed visible to different threads.

  • I choose a random angular velocity to start spinning the roulette wheel and decrement it within the ScheduledExecutorService task based on a friction value until it reaches zero. This way, it decreases progressively with each tick.

  • At each tick, I calculate the angular distance to be traversed and published in SwingWorker .

  • In the swing, the distances traveled are collected and summed and the pointer moves at the corresponding angular distance.

  • All of the above is within SwingWorker .

  • I've also made one or more code changes out of this, but not much.

Here's how it went:

import java.awt.BasicStroke;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.GradientPaint;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GridLayout;
import java.awt.Polygon;
import java.awt.RenderingHints;
import java.awt.geom.AffineTransform;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingWorker;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
import javax.swing.border.EmptyBorder;

public class SpinArrowTest extends JFrame {

    private static final long serialVersionUID = 1L;

    private final JButton rotateButton;
    private final Board board;

    public static void main(String[] args) throws UnsupportedLookAndFeelException {

        try {
            UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
        } catch (ClassNotFoundException | InstantiationException | IllegalAccessException
                | UnsupportedLookAndFeelException e) {
            e.printStackTrace();
        }

        EventQueue.invokeLater(SpinArrowTest::new);
    }

    public SpinArrowTest() {

        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 = 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));

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

        controlsPane.add(rotateButton);

        contentPane.add(controlsPane, BorderLayout.SOUTH);
        pack();
        setLocationRelativeTo(null);
        setVisible(true);
    }

    // Medidas em graus por segundo.
    private static final double VELOCIDADE_ANGULAR_INICIAL_MINIMA = 180;
    private static final double VELOCIDADE_ANGULAR_INICIAL_MAXIMA = 720;

    // Unidade de aceleração, medido em graus por segundo a cada segundo.
    private static final double ATRITO = -40;

    // Tempo entre ticks, em MICROsegundos.
    // Quanto menor for, mais preciso fica, porém mais custoso será.
    private static final int MICRO_DELTA_T = 10_000;

    // Tempo entre ticks, em segundos.
    private static final double DELTA_T = MICRO_DELTA_T / 1_000_000.0;

    private void girar() {
        rotateButton.setEnabled(false);

        SwingWorker<Void, Double> worker = new SwingWorker<Void, Double>() {
            @Override
            protected Void doInBackground() {

                ScheduledExecutorService ses = Executors.newScheduledThreadPool(1);

                // Sorteia a velocidade inicial da roleta em graus por segundo.
                double velocidadeInicial =
                        Math.random() * (VELOCIDADE_ANGULAR_INICIAL_MAXIMA - VELOCIDADE_ANGULAR_INICIAL_MINIMA)
                        + VELOCIDADE_ANGULAR_INICIAL_MINIMA;

                // Armazena a velocidade angular atual em graus por segundo.
                AtomicReference<Double> vref = new AtomicReference<>(velocidadeInicial);

                Runnable run = () -> {
                    // Obtém a velocidade angular atual.
                    double velocidadeAngular = vref.get();

                    // Publica no SwingWorker a distância angular percorrida.
                    // Obs: Velocidade * tempo = distância (deg/s * s = deg)
                    publish(velocidadeAngular * DELTA_T);

                    // Aplica o atrito para reduzir a velocidade.
                    // Obs: Aceleração * tempo = velocidade (deg/s² * s = deg/s)
                    double velocidadeAngularNova = velocidadeAngular + ATRITO * DELTA_T;
                    vref.set(velocidadeAngularNova);

                    // Se parou, termina.
                    if (isCancelled() || velocidadeAngularNova <= 0.0) ses.shutdown();
                };

                ses.scheduleAtFixedRate(run, 0, MICRO_DELTA_T, TimeUnit.MICROSECONDS);
                try {
                    ses.awaitTermination(99999, TimeUnit.DAYS);
                } catch (InterruptedException e) {
                    // Ignora a exceção e deixa o SwingWorker terminar graciosamente.
                }
                return null;
            }

            @Override
            protected void process(List<Double> doubles) {
                double distanciaAngular = doubles.stream().reduce(Double::sum).orElse(0.0);
                board.spin(distanciaAngular);
            }

            @Override
            protected void done() {
                rotateButton.setEnabled(true);
            }
        };
        worker.execute();
    }
}

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

class Board extends JPanel {

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

    public Board() {
        angleDegrees = 90;
    }

    public void spin(double degrees) {
        angleDegrees += degrees;
        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, 15);
        line.draw(g2);
    }
}

// Classe que representa o círculo.

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

// Classe que representa a seta que irá girar dentro do círculo.

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);
    }
}
    
11.10.2017 / 05:19