Search code examples
javaswingtimergraphics2d

Why does the star rotation slows down automatically after some time


I have a program where I have to use internal Timer event to rotate a star in circular motion. There is a button in the frame that changes the direction of the star and a slider which changes the speed of the star rotation.

This is my Main Class

import javax.swing.JFrame;

public class Main {
    public static void main(String args[]) {
//      Making an instance of the class that makes the frame
        MainFrame frame = new MainFrame();
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setSize(500, 500);
        frame.setVisible(true);
    }
}

MainFrame Class that makes the frame.

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;

import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;


@SuppressWarnings("serial")
public class MainFrame extends JFrame{
    private StarGraphics frameStar;
    private JButton starToggle;
    public JSlider starSpeed;
    
    
    public MainFrame() {
//      Setting some properties of the frame
        setTitle("Star Moving with Internal Events");
        setLayout(new BorderLayout());
        
//      Initializing the panel which has rotating star
        frameStar = new StarPainter(this);
        
//      Adding the bottom toggle button and slider
        addToggler();   
    }
    
//  Getter for the slider value
    public int sliderSpeed() {
        return starSpeed.getValue();
    }
    
    
//  Adds Button to change the direction of the star
    private void addToggler() {
//      Adding another jpanel which has layout set to null so i can add button of my size
        JPanel panel = new JPanel();
        panel.setLayout(null);
        panel.setPreferredSize(new Dimension(20,80));
        panel.setBackground(Color.WHITE);
        
//      Initializing button and its action listener to change star direction
        starToggle = new JButton("Toggle");
        starToggle.setBounds(190, 0, 80, 20);
        starToggle.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                if(e.getSource() == starToggle) {
                    frameStar.ChangeDirections();
                }
            }
        });
        
//      Adding button to panel
        panel.add(starToggle);
        
//      Adding slider to the panel
        addSlider(panel);
        
//      Adding panel to the main Panel at the bottom
        add(panel, BorderLayout.SOUTH);
    }
    
    private void addSlider(JPanel panel) {
//      Adding Slider and it's properties
        starSpeed = new JSlider(JSlider.HORIZONTAL, 0, 20, 5);
        starSpeed.setMajorTickSpacing(10);
        starSpeed.setMinorTickSpacing(1);
        starSpeed.setPaintTicks(true);
        starSpeed.setPaintLabels(true);
        starSpeed.setBounds(70,30,400, 45);
        
//      Adding Slider-ChangeListener to change the rotation speed of the star
        starSpeed.addChangeListener(new ChangeListener() { // anonymous inner class  
            // handle change in slider value
            @Override
            public void stateChanged(ChangeEvent e) {
                frameStar.ChangeSpeed(starSpeed.getValue());
            }
         } 
      );
        
//      Adding label besides the slider
        JLabel label = new JLabel("Speed : ");
        label.setBounds(10 , 10, 80, 80);
        panel.add(label);
        panel.add(starSpeed);
    }
}

Class to create the panel that has star rotation

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.*;

@SuppressWarnings("serial")
public class StarGraphics extends JPanel{
    private boolean toggleDir = false;
    private int speed = 10;
    private Timer timer;
    protected double angleOfStarRotation = 0;
    
    public StarGraphics(JFrame frame) {
        
        setPreferredSize(new Dimension(500, 470));
        setBackground(Color.BLACK);
        setLayout(new BorderLayout());
        frame.add(this, BorderLayout.CENTER);
        
        startTimer();
    }
    
    public void startTimer() {
        timer = new Timer(speed, new ActionListener() {
            public void actionPerformed(ActionEvent e){
//              System.out.println(angleOfStarRotation);
                if(!toggleDir)  //rotates clockwise
                    angleOfStarRotation = angleOfStarRotation + 1;
                else        //rotates counterclockwise
                    angleOfStarRotation = angleOfStarRotation - 1;
                  
//                if (angleOfStarRotation == 360 || angleOfStarRotation == -360)  // If there is a full circle, it will reset the angle to zero
//                    angleOfStarRotation = 0;
                  
                repaint();
            }});
        timer.start();
    }
    
    public void ChangeSpeed(int newSpeed) { 
        this.speed = newSpeed;
        timer.setDelay(speed);
    }   
    public void ChangeDirections() {toggleDir = !toggleDir; }

}

And a class that paints the star into the panel

import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.geom.GeneralPath;
import java.security.SecureRandom;

import javax.swing.JFrame;

@SuppressWarnings("serial")
public class StarPainter extends StarGraphics{
    private int[] starXPoints = {55, 67, 109, 73, 83, 55, 27, 37, 1, 43};
    private int[] starYPoints = {0, 36, 36, 54, 96, 72, 96, 54, 36, 36};
    GeneralPath starDesign = new GeneralPath();
    
    public StarPainter(JFrame frame) {
        super(frame);
    }
    
    public void drawStar(GeneralPath path) {
        path.moveTo(starXPoints[0], starYPoints[0]);
        
        for(int i=0; i<10; i++)
           path.lineTo(starXPoints[i], starYPoints[i]);
        
        path.closePath();
    }
    
    public void starActions(Graphics2D g) {
        int startAngle = 360;
        // For Random Color
        SecureRandom random = new SecureRandom(); 
           
        // rotate around origin and draw stars in random colors
        for (int count = 1; count <= 1; count++) 
        {
            double angle = startAngle - 90;
            // rotate coordinate system
            g.rotate(angleOfStarRotation * Math.PI / angle); //rotates as per the rotated angle    // 
           // set random drawing color
            g.setColor(new Color(random.nextInt(256), random.nextInt(256), random.nextInt(256)));
          // draw filled star
            g.fill(starDesign);
          // dispose the star
            g.dispose();
        }
    }
    
    @Override
    public void paintComponent(Graphics g)
    {
       super.paintComponent(g);
       
       Graphics2D g2d = (Graphics2D) g;
      
       drawStar(starDesign);
       
       g2d.translate(250,150);
       
       starActions(g2d);
       
    }
}

After running the code, the output will show a frame with coloring star rotating around the panel in circular motion, but after 2 full rotation, the star rotation slow down automatically. Does anyone know why that happens?


Solution

  • Your "core" problem is right here...

    public void drawStar(GeneralPath path) {
        path.moveTo(starXPoints[0], starYPoints[0]);
        
        for(int i=0; i<10; i++)
           path.lineTo(starXPoints[i], starYPoints[i]);
        
        path.closePath();
    }
    

    This gets called each time you re-draw the component, which means, you're adding new points to the shape, making it infinitely more complex on each paint pass.

    Instead, just create the shape in the constructor...

    public StarPainter() {
        starDesign.moveTo(starXPoints[0], starYPoints[0]);
    
        for (int i = 0; i < 10; i++) {
            starDesign.lineTo(starXPoints[i], starYPoints[i]);
        }
    
        starDesign.closePath();
    }
    

    As has already been pointed out, you're disposing of a Graphics context which you did not create.

    If your going to change the transformation of the context, you should always create a copy of your own, for example...

    public void starActions(Graphics2D g) {
        int startAngle = 360;
        // For Random Color
        SecureRandom random = new SecureRandom();
    
        g = (Graphics2D) g.create();
        // rotate around origin and draw stars in random colors
        //for (int count = 1; count <= 1; count++) {
            double angle = startAngle - 90;
            // rotate coordinate system
            g.rotate(angleOfStarRotation * Math.PI / angle); //rotates as per the rotated angle    // 
            // set random drawing color
            g.setColor(new Color(random.nextInt(256), random.nextInt(256), random.nextInt(256)));
            // draw filled star
            g.fill(starDesign);
            // dispose the star
            g.dispose();
        //}
    }
    
    @Override
    public void paintComponent(Graphics g) {
        super.paintComponent(g);
    
        Graphics2D g2d = (Graphics2D) g.create();
        g2d.translate(250, 150);
        starActions(g2d);
        g2d.dispose();
    
    }
    

    Also, passing a reference of the JFrame to the component...

    public StarGraphics(JFrame frame) {
        
        setPreferredSize(new Dimension(500, 470));
        setBackground(Color.BLACK);
        setLayout(new BorderLayout());
        frame.add(this, BorderLayout.CENTER);
        
        startTimer();
    }
    

    is a bad idea. The component has no need for, nor is it its responsibility too, interact with the frame. You're just exposing implementation detail unnecessarily.

    Runnable example

    import java.awt.BorderLayout;
    import java.awt.Color;
    import java.awt.Dimension;
    import java.awt.EventQueue;
    import java.awt.Graphics;
    import java.awt.Graphics2D;
    import java.awt.event.ActionEvent;
    import java.awt.event.ActionListener;
    import java.awt.geom.GeneralPath;
    import java.security.SecureRandom;
    import javax.swing.JButton;
    import javax.swing.JFrame;
    import javax.swing.JLabel;
    import javax.swing.JPanel;
    import javax.swing.JSlider;
    import javax.swing.Timer;
    import javax.swing.event.ChangeEvent;
    import javax.swing.event.ChangeListener;
    
    public class Main {
    
        public static void main(String[] args) {
            new Main();
        }
    
        public Main() {
            EventQueue.invokeLater(new Runnable() {
                @Override
                public void run() {
                    MainFrame frame = new MainFrame();
                    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                    frame.setSize(500, 500);
                    frame.setVisible(true);
                }
            });
        }
    
        public class MainFrame extends JFrame {
    
            private StarGraphics frameStar;
            private JButton starToggle;
            public JSlider starSpeed;
    
            public MainFrame() {
                //      Setting some properties of the frame
                setTitle("Star Moving with Internal Events");
                setLayout(new BorderLayout());
    
                //      Initializing the panel which has rotating star
                frameStar = new StarPainter();
                add(frameStar);
    
                //      Adding the bottom toggle button and slider
                addToggler();
            }
    
            //  Getter for the slider value
            public int sliderSpeed() {
                return starSpeed.getValue();
            }
    
            //  Adds Button to change the direction of the star
            private void addToggler() {
                //      Adding another jpanel which has layout set to null so i can add button of my size
                JPanel panel = new JPanel();
                panel.setLayout(null);
                panel.setPreferredSize(new Dimension(20, 80));
                panel.setBackground(Color.WHITE);
    
                //      Initializing button and its action listener to change star direction
                starToggle = new JButton("Toggle");
                starToggle.setBounds(190, 0, 80, 20);
                starToggle.addActionListener(new ActionListener() {
                    @Override
                    public void actionPerformed(ActionEvent e) {
                        if (e.getSource() == starToggle) {
                            frameStar.ChangeDirections();
                        }
                    }
                });
    
                //      Adding button to panel
                panel.add(starToggle);
    
                //      Adding slider to the panel
                addSlider(panel);
    
                //      Adding panel to the main Panel at the bottom
                add(panel, BorderLayout.SOUTH);
            }
    
            private void addSlider(JPanel panel) {
                //      Adding Slider and it's properties
                starSpeed = new JSlider(JSlider.HORIZONTAL, 0, 20, 5);
                starSpeed.setMajorTickSpacing(10);
                starSpeed.setMinorTickSpacing(1);
                starSpeed.setPaintTicks(true);
                starSpeed.setPaintLabels(true);
                starSpeed.setBounds(70, 30, 400, 45);
    
                //      Adding Slider-ChangeListener to change the rotation speed of the star
                starSpeed.addChangeListener(new ChangeListener() { // anonymous inner class  
                    // handle change in slider value
                    @Override
                    public void stateChanged(ChangeEvent e) {
                        frameStar.ChangeSpeed(starSpeed.getValue());
                    }
                }
                );
    
                //      Adding label besides the slider
                JLabel label = new JLabel("Speed : ");
                label.setBounds(10, 10, 80, 80);
                panel.add(label);
                panel.add(starSpeed);
            }
        }
    
        public class StarGraphics extends JPanel {
    
            private boolean toggleDir = false;
            private int speed = 10;
            private Timer timer;
            protected double angleOfStarRotation = 0;
    
            public StarGraphics() {
                setPreferredSize(new Dimension(500, 470));
                setBackground(Color.BLACK);
                setLayout(new BorderLayout());
                startTimer();
            }
    
            public void startTimer() {
                timer = new Timer(speed, new ActionListener() {
                    public void actionPerformed(ActionEvent e) {
                        //              System.out.println(angleOfStarRotation);
                        if (!toggleDir) //rotates clockwise
                        {
                            angleOfStarRotation = angleOfStarRotation + 1;
                        } else //rotates counterclockwise
                        {
                            angleOfStarRotation = angleOfStarRotation - 1;
                        }
    
                        System.out.println("tick");
                        //                if (angleOfStarRotation == 360 || angleOfStarRotation == -360)  // If there is a full circle, it will reset the angle to zero
                        //                    angleOfStarRotation = 0;
                        repaint();
                    }
                });
                timer.start();
            }
    
            public void ChangeSpeed(int newSpeed) {
                this.speed = newSpeed;
                timer.setDelay(speed);
            }
    
            public void ChangeDirections() {
                toggleDir = !toggleDir;
            }
    
        }
    
        public class StarPainter extends StarGraphics {
    
            private int[] starXPoints = {55, 67, 109, 73, 83, 55, 27, 37, 1, 43};
            private int[] starYPoints = {0, 36, 36, 54, 96, 72, 96, 54, 36, 36};
            GeneralPath starDesign = new GeneralPath();
    
            public StarPainter() {
                starDesign.moveTo(starXPoints[0], starYPoints[0]);
    
                for (int i = 0; i < 10; i++) {
                    starDesign.lineTo(starXPoints[i], starYPoints[i]);
                }
    
                starDesign.closePath();
            }
    
            public void starActions(Graphics2D g) {
                int startAngle = 360;
                // For Random Color
                SecureRandom random = new SecureRandom();
    
                g = (Graphics2D) g.create();
                // rotate around origin and draw stars in random colors
                //for (int count = 1; count <= 1; count++) {
                double angle = startAngle - 90;
                // rotate coordinate system
                g.rotate(angleOfStarRotation * Math.PI / angle); //rotates as per the rotated angle    // 
                // set random drawing color
                g.setColor(new Color(random.nextInt(256), random.nextInt(256), random.nextInt(256)));
                // draw filled star
                g.fill(starDesign);
                // dispose the star
                g.dispose();
                //}
            }
    
            @Override
            public void paintComponent(Graphics g) {
                super.paintComponent(g);
    
                Graphics2D g2d = (Graphics2D) g.create();
                g2d.translate(250, 150);
                starActions(g2d);
                g2d.dispose();
    
            }
        }
    }