Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / Java

Wheel of Fortune in Java

4.83/5 (5 votes)
6 Jul 2020CPOL12 min read 28.6K   1.4K  
An animated random string selection wheel class written in Java AWT
A class that draws a fully animated rotatable wheel with sections containing strings. The wheel once spun, rotates for a certain period and stops at a random point, which gives the selected string. An example of animation in Java AWT

Introduction

The idea behind this project was to create a Java class that designs a simple „wheel-of-fortune“ type of control, which is basically a wheel divided into n sections of the same sizes, and has a tick in the middle of the right side (3 o'clock position) which shows the current selection. The idea was that the wheel could be turned by using the mouse, and also spun with an initial speed and deceleration if the mouse button was released during the movement. Once the wheel would stop spinning, the selected string could be read based on what the tick is pointing at.

Image 1

The wheel could be used for random selections from limited numbers of strings. The limit is due to the fact that the wheel is divided up to as many sections as the size of the string array, so visually it is only possible to fit in so many.

Since I am quite new to Java programming, this project was in a way intended for me to practice a little bit with Java animation using Java AWT.

The Main Class

The reusable classes needed for the Selection Wheel are: "SelectionWheel.java", "Wheel.java" and "Tick.java". I have written and attached to this project another source file, "MainWheel.java", which is just an example of usage of the SelectionWheel class.

Java
package SelectionWheel;

import javax.swing.*;

import java.io.File;
import java.io.FilenameFilter;
import java.util.*;

public class MainWheel {
    
    public static void main(String[] args) throws Exception {
        
        int width = 1000, height = 1000;
        
        JFrame frame = new JFrame();
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        
        ArrayList<String> list = new ArrayList<String>();
        list.add("Avatar");
        list.add("The Lord of the Rings: The Return of the King");
        list.add("Pirates of the Caribbean: Dead Man's Chest");
        list.add("The Dark Knight");
        list.add("Harry Potter and the Philosopher's Stone");
        list.add("Pirates of the Caribbean: At World's End");
        list.add("Harry Potter and the Order of the Phoenix");
        list.add("Harry Potter and the Half-Blood Prince");
        list.add("The Lord of the Rings: The Two Towers");
        list.add("Shrek 2");
        list.add("Harry Potter and the Goblet of Fire");
        list.add("Spider-Man 3");
        list.add("Ice Age: Dawn of the Dinosaurs");
        list.add("Harry Potter and the Chamber of Secrets");
        list.add("The Lord of the Rings: The Fellowship of the Ring");
        list.add("Finding Nemo");
        list.add("Star Wars: Episode III – Revenge of the Sith");
        list.add("Transformers: Revenge of the Fallen");
        list.add("Spider-Man");
        list.add("Shrek the Third");
        
        SelectionWheel wheel = new SelectionWheel(list);
        wheel.hasBorders(true);
        wheel.setBounds(10, 10, 700, 700);
        
        JLabel lbl1 = new JLabel("Selection: ");
        JLabel lbl2 = new JLabel("Angle: ");
        JLabel lbl3 = new JLabel("Speed: ");
        JLabel lblsel = new JLabel("(selection)");
        JLabel lblang = new JLabel("(angle)");
        JLabel lblsp = new JLabel("(speed)");
        lbl1.setBounds(720, 10, 100, 20);
        lblsel.setBounds(830, 10, 150, 20);
        lbl2.setBounds(720, 30, 100, 20);
        lblang.setBounds(830, 30, 150, 20);
        lbl3.setBounds(720, 50, 100, 20);
        lblsp.setBounds(830, 50, 150, 20);
        frame.add(wheel);
        frame.add(lbl1);
        frame.add(lblsel);
        frame.add(lbl2);
        frame.add(lblang);
        frame.add(lbl3);
        frame.add(lblsp);
        frame.setSize(width, height);
        frame.setLayout(null);
        frame.setVisible(true);
        
        lblsel.setText(wheel.getSelectedString());
        lblang.setText(Double.toString(wheel.getRotationAngle()));
        lblsp.setText(Double.toString(wheel.getSpinSpeed()));
        
        while(true) {
            // wait for action
            while(true)
            {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                if(wheel.isSpinning())
                    break;
            }
            // while spinning
            while(wheel.isSpinning())
            {
                lblsel.setText(wheel.getSelectedString());
                lblang.setText(Double.toString(wheel.getRotationAngle()));
                lblsp.setText(Double.toString(wheel.getSpinSpeed()));
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            lblsp.setText(Double.toString(wheel.getSpinSpeed()));
            // show selection
            JOptionPane.showMessageDialog(frame, "Selection: " + wheel.getSelectedString());
        }
    }
}

The SelectionWheel in the example is initialized with a String ArrayList containing 20 best grossing movies in 2000s. It is drawn on a JFrame. There are also several labels – lblsel shows the current selection, lblang shows the current rotation angle, and lblsp shows the current speed of rotation of the wheel.

After the initialization, the code is entering an infinite loop where first a loop is waiting for the wheel to begin spinning, then after the spinning has started, it is refreshing the labels by reading the attributes of the SelectionWheel, and finally once it has stopped, pops a MessageDialog showing the finally selected string (the string at which the tick is pointing).

The Classes

There are altogether three classes which are needed for the SelectionWheel to work.

The first is the SelectionWheel class, which is basically just a wrapper class which combines the other two classes – Wheel and Tick. The Wheel and Tick needed to be written separately because the wheel is constantly rotated, while the tick is stationary, so we needed two separate Graphics objects for drawing them.

All three classes are extensions of JPanel class.

The Wheel Class

The Wheel class is where all the animation and calculations happen. My original intention was to just write this class, but, as mentioned previously, the tick needed to be created separately because of the rotation – we need the wheel to rotate, but the tick remains stationary.

The Wheel class has only one constructor method – it is always initialized with an ArrayList of String objects.

Java
public Wheel(ArrayList<String> listOfStrings)

Mouse Listeners

The Wheel class constructor also creates a MouseListener – for mousePressed and mouseReleased events, and a MouseMotionListener – for the MouseDragged event.

The mouse events are tracked so that the animation of the rotation of the wheel can be triggered by using the mouse.

When the mouse is pressed, the rotation stops (if the wheel is moving at the moment). The time and the rotation angle are stored in order to calculate the initial speed and direction of rotation once it is released.

Java
@Override
public void mousePressed(MouseEvent e) {
    _mouseDragPosition = new Point2D.Double(e.getX(), e.getY());
    // to stop the spinning if the circle is clicked on
    double distance = Math.sqrt(Math.pow(_mouseDragPosition.getX() - 
    _center.getX(),2) + Math.pow(_mouseDragPosition.getY() - _center.getY(),2));
    if(distance <= _radius)
    {
        spinStop();
    }
    // to measure initial speed
    _timeStart = System.currentTimeMillis();
    _rotationAngleStart = _rotationAngle;
}

Once the mouse is released, the initial speed is calculated, and the spinning is started by calling the method spinStartAsync.

Java
@Override
public void mouseReleased(MouseEvent e) {
    setCursor(new Cursor(Cursor.DEFAULT_CURSOR));
    // to measure initial speed
    _timeEnd = System.currentTimeMillis();
    _rotationAngleEnd = _rotationAngle;
    double initialSpeed = 1000 * (_rotationAngleEnd - _rotationAngleStart) / 
                                 (_timeEnd - _timeStart);
    initialSpeed = (int)Math.signum(initialSpeed) * 
                   Math.min(Math.abs(initialSpeed), _maxSpinSpeed);
    try {
        spinStartAsync(Math.abs(initialSpeed), 
               (int)Math.signum(initialSpeed), _spinDeceleration);
    } catch (Exception e1) {
        e1.printStackTrace();
    }
}

The mouseDragged event listener is used to rotate the wheel while the mouse is pressed:

Java
addMouseMotionListener(new MouseAdapter() {
    @Override
    public void mouseDragged(MouseEvent e) {
        setCursor(new Cursor(Cursor.HAND_CURSOR));
        spinStop();
        /*
         * Use the equation for angle between two vectors:
         * vector 1 between last position of mouse and center of circle
         * vector 2 between current position of mouse and center of circle
         * ("k" is direction coefficient)
         */
        Point2D mousePos = new Point2D.Double(e.getX(), e.getY());
        double k1 = (_mouseDragPosition.getY() - _center.getY()) / 
                    (_mouseDragPosition.getX() - _center.getX());
        double k2 = (mousePos.getY() - _center.getY()) / (mousePos.getX() - _center.getX());
        double _delta = Math.toDegrees(Math.atan((k2-k1)/(1 + k2 * k1)));
        if(!Double.isNaN(_delta))
            setRotationAngle(getRotationAngle() + _delta);
        _mouseDragPosition = mousePos;
    }
});

The Animation

After the mouse button is released, the animation is accomplished by repeatedly repainting the JPanel – while a separate thread is calculating and updating the rotation angle over time; each time the JPanel is repainted, the wheel is drawn rotated by a new angle. A deceleration is defined in the calculation of the spinning speed, in order to make sure that the wheel eventually stops spinning.

Java
public void spinStartAsync(double speed, int direction, double deceleration)

(The spinning thread and the calculation process are explained in further text.)

However, the only thing handled so far is the calculation of the angle and calling the repaint method; in order to actually paint the wheel the way we want it, we need to override the paintComponent method.

Java
@Override
public void paintComponent(Graphics g)
{
    /*
     * Paintcomponent - if the image is null, create it and then draw it 
     * whilst keeping the current rotation.
     * The image can be larger than the displaying area, 
     * so after it is drawn it needs to be placed properly.
     */
    super.paintComponent(g);
    
    if(_image == null) {
        _image = drawImage();
        _rotationCenter = new Point2D.Double(
                this.getWidth() - 2 * BORDER - 2 * _radius + _center.getX(),
                this.getHeight() / 2
            );
        _imagePosition = new Point2D.Double(
                    (int)(this.getWidth() - 2 * BORDER - 2 * _radius),
                    (int)(this.getHeight() / 2 - _center.getY())
                );
    }
    
    Graphics2D gPanel = (Graphics2D) g;
    gPanel.setRenderingHint
        (RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
    gPanel.setRenderingHint
        (RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
    
    gPanel.rotate(Math.toRadians(_rotationAngle), 
                  _rotationCenter.getX(), _rotationCenter.getY());
    gPanel.drawImage(_image, (int)_imagePosition.getX(), (int)_imagePosition.getY(), null);
}

For the sake of performance, and since the wheel – once drawn – will not be changed, we are using a BufferedImage to store the wheel – so the wheel itself is actually drawn only once. The other approach would be to draw the entire wheel – section by section – each time the JPanel redraws, but this might cause poor performance, depending on the size of the wheel and the number of the sections.

The BufferedImage is drawn inside a separate method, which is thoroughly explained in further text.

Get / Set Methods

There are a lot of parameters of the wheel that can be affected / changed during runtime.

Java
public void setShape(Shape shape)

The wheel can have different shapes, and this set method is used to change the shape of the wheel. The currently available shapes are:

Java
public static enum Shape {
        CIRCLE,
        UMBRELLA
    }

This list can easily be extended by adding a method for drawing another shape of a section. The circle shape is drawn as arcs (method fillArc), and the umbrella shape is drawn as triangles (method fillTriangle).

Image 2Image 3

Java
public double getRotationAngle()
public void setRotationAngle(double rotationAngle)

The rotation angle can be set directly. The repaint method is called.

Java
public ArrayList<Color> getColorScheme()
public void setColorScheme(ArrayList<Color> colors)

The Wheel class contains an ArrayList of Color objects that are used to draw (fill) the wheel sections. The colors are used in the order of appearance in the ArrayList. If the ArrayList was never set, a default list will be used (method getDefaultColorList).

A color can also be added to the existing list by using method addColor.

Java
public int getRadius()

Gets the current radius of the wheel. The radius cannot be changed, it is calculated based on the size of the BufferedImage.

Java
public ArrayList<String> getListOfStrings()
public void setListOfStrings(ArrayList<String> list)

The list of strings can be changed during runtime. In that case, the existing BufferedImage is disposed and a new one is drawn.

The number of String objects in ArrayList is limited by a variable private final int LIMIT = 100. If the size of the ArrayList is above this limit, an Exception will be thrown.

The reason for limiting the number of strings is limiting the number of sections – because for a large number of sections, the delta angle becomes very very tiny, which is difficult to draw and may cause distortion and miscalculations.

Java
public Font getFont()
public void setFont(Font font)

Gets/sets the font of the displayed strings.

Java
public double getSpinSpeed()

This is the actual spinning speed of the wheel – it cannot be set, it starts with the initial speed and is decreasing by deceleration until a full stop.

The current speed is calculated in a separate thread, a special TimerTask object which runs the entire time, and based on the change of the rotation angle over a period of time, it is calculating the current speed of rotation. This class is explained in further text.

Java
public double getMaxSpinSpeed()
public void setMaxSpinSpeed(double speed)

The maximum speed limits the initial speed of the rotation. This parameter was introduced in order to avoid too long spinnings. This can be set differently; by allowing greater maximum speeds, we achieve a larger degree of randomness – however, in that case, it would be advisable to also set the deceleration accordingly.

Java
public double getSpinDeceleration()
public void setSpinDeceleration(double deceleration)

Gets/sets the spin deceleration. Note that deceleration must be <= 0, otherwise the result is an Exception.

Java
public String getSelectedString() {
    return _stringList.get((int)Math.floor(_noElem + (_rotationAngle % 360) / _delta) % _noElem);
}

Get the currently selected string (the string from the section to which the tick is pointing).

The idea is to get the number of deltas (section angle) in the current rotationAngle. This number is added to the size of the string arraylist, and then MODed by the size of the string arraylist, in order to avoid negative indices.

The Tick Class

The Tick class is very simple and short.

Tick can have width and height – these are the attributes. It can have a random polygon shape, which can be set with the method setPolygon. If the custom polygon is not set, then the triangle is used, which is calculated in method getTriangle().

Image 4

Java
private Polygon getTriangle() {
    /*
     * Get triangle polygon - default shape of the tick.
     */
    Polygon polygon = new Polygon();
    polygon.addPoint(0, this.getHeight() / 2);
    polygon.addPoint(this.getWidth(), 
       (int)(this.getHeight() / 2 - this.getWidth() * Math.tan(Math.toRadians(30))));
    polygon.addPoint(this.getWidth(), 
       (int)(this.getHeight() / 2 + this.getWidth() * Math.tan(Math.toRadians(30))));
    return polygon;
}

If a custom polygon is used, its size needs to be adjusted to fit the JPanel, and it needs to be positioned properly, so this method is called:

Java
private void adjustPolygon()
{
    /*
     * Adjust the size and position of the custom polygon shape of the tick.
     */
    int i;
    // calculate width/height of the polygon
    int xmax = Integer.MIN_VALUE, xmin = Integer.MAX_VALUE;
    int ymax = xmax, ymin = xmin;
    for(i = 0; i < _polygon.xpoints.length; i++)
    {
        if(_polygon.xpoints[i]>xmax) xmax = _polygon.xpoints[i];
        if(_polygon.xpoints[i]<xmin) xmin = _polygon.xpoints[i];
    }
    for(i = 0; i < _polygon.ypoints.length; i++)
    {
        if(_polygon.ypoints[i]>ymax) ymax = _polygon.ypoints[i];
        if(_polygon.ypoints[i]<ymin) ymin = _polygon.ypoints[i];
    }
    int width = xmax - xmin;
    // scale polygon
    double factor = (double)this.getWidth() / width;
    for(i = 0; i < _polygon.xpoints.length; i++)
    {
        _polygon.xpoints[i] *= factor;
        _polygon.ypoints[i] *= factor;
    }
    // calculate center of polygon
    int centerX = 0, centerY = 0;
    for(i = 0; i < _polygon.xpoints.length; i++)
    {
        centerX += _polygon.xpoints[i];
    }
    centerX /= _polygon.xpoints.length;
    for(i = 0; i < _polygon.ypoints.length; i++)
    {
        centerY += _polygon.ypoints[i];
    }
    centerY /= _polygon.ypoints.length;
    // translate polygon to center of the panel
    _polygon.translate(this.getWidth() / 2 - centerX, this.getHeight() / 2 - centerY);
}

The overriden paintComponent method will create the triangle if the custom polygon is not set, and then call fillPolygon to draw it.

The SelectionWheel Class

The SelectionWheel class is a wrapper for other two classes – it joins them together and makes sure that the wheel and tick are positioned properly in relation to one another. The positions are adjusted in the overriden setBounds method.

Java
@Override
public void setBounds(int x, int y, int width, int height) {
    /*
     * Adjust the bounds of the wheel and tick based on tick width.
     */
    super.setBounds(x, y, width, height);
    _wheel.setBounds(0, 0, width - _tick.getTickWidth(), height);
    _tick.setBounds(width - _tick.getTickWidth(), 0, _tick.getTickWidth(), height);
}

The class initializes a Wheel and a Tick object, and contains get and set methods that are passing attributes to and from these objects.

Points of Interest

Drawing the Wheel

The Wheel class contains several attributes that are crucial to the process of drawing, the most important being:

  • Radius – the radius of the wheel (circle); it is set in drawImage() method and will always be as big as the Image size allows it, minus the specified BORDER
    Java
    _radius = Math.min(img.getWidth(), img.getHeight()) / 2 - BORDER;
  • Center – the center of the wheel (circle) is always in the middle of the BufferedImage
    Java
    _center = new Point2D.Double((double)img.getWidth() / 2, (double)img.getHeight() / 2);
  • stringDistanceFromEdge – required in positioning the drawn string – how far from the edge of the wheel we want it to be; this is hardcoded to 5% of the radius
    Java
    double stringDistanceFromEdge = 0.05 * _radius;
  • fontSize – the optimal size of the font of drawn strings; this is calculated in a separate method calcFontSize

Calculating Font Size

Calculating the optimal font size was one of the trickiest parts. The idea was to calculate the largest possible font that would still enable all of the strings to fit into their respective sections, but with alignment to the edge of the wheel, stringDistanceFromEdge away from it.

The algorithm is as follows:

  1. First, we find the longest string in the ArrayList.
  2. Then, we set it to maximum allowed font size (final int MAXFONTSIZE).
  3. Then we adjust the font size to the maximum possible height of the String. For this, we use java.awt.FontMetrics and a couple of basic trigonometric rules, such as Pythagoras theorem and triangle similarity.
  4. Then, the width is adjusted by trial-and-error; if the string is wider than the section, the font size is reduced, and if the string is narrower than the section, the font size is increased – point by point – and then width is measured using FontMetrics in each iteration – until the string fits the section.

I tried to solve the font size problem in many different ways (i.e., linear proportionality of font size and string height, but unfortunately, this rule did not provide the best results). In the end, I used this convenient solution.

Java
private int calcFontSize(Graphics g, double stringDistanceFromEdge, int maxStringWidth) {
    /*
     * Calculates the optimal font size for the strings inside the sections.
     * The strings need to be positioned next to the broader end of the section.
     * The optimal size will depend on the longest string length 
     * and maximum height of the section
     * in the left border of the rectangle surrounding the string.
     */
    
    // Find the longest string
    String tmpString = "";
    for(int i = _noElem - 1; i >= 0; i--) {
        if(_stringList.get(i).length() > tmpString.length())
            tmpString = _stringList.get(i);
    }
    
    // Set it to max font size and calculate rectangle
    int fontSize = MAXFONTSIZE;
    g.setFont(new Font(_font.getFamily(), _font.getStyle(), fontSize));
    FontMetrics fontMetrics = g.getFontMetrics();
    Rectangle2D stringBounds = fontMetrics.getStringBounds(tmpString, g);
    
    // Adjust string height / font size
    int maxHeight = (int)Math.floor
                    (2 * stringDistanceFromEdge * Math.sin(Math.toRadians(_delta / 2)));
    if(stringBounds.getHeight() > maxHeight) {
        fontSize = (int)Math.floor(fontSize * maxHeight / stringBounds.getHeight());
        g.setFont(new Font(_font.getFamily(), _font.getStyle(), fontSize));
        fontMetrics = g.getFontMetrics();
        stringBounds = fontMetrics.getStringBounds(tmpString, g);
    }
    
    // Adjust string width
    // If the string is too narrow, increase font until it fits
    double K = stringBounds.getWidth() / stringBounds.getHeight();
    maxHeight = (int)Math.floor(2 * (_radius - stringDistanceFromEdge) * 
    Math.tan(Math.toRadians(_delta / 2)) / (1 + 2 * K * Math.tan(Math.toRadians(_delta / 2))));
    while(stringBounds.getWidth() < maxStringWidth) {
            g.setFont(new Font(_font.getFamily(), _font.getStyle(), ++fontSize));
            fontMetrics = g.getFontMetrics();
            stringBounds = fontMetrics.getStringBounds(tmpString, g);
    }
    // If the string is too wide, decrease font until it fits
    while(stringBounds.getWidth() > maxStringWidth) {
        g.setFont(new Font(_font.getFamily(), _font.getStyle(), --fontSize));
        fontMetrics = g.getFontMetrics();
        stringBounds = fontMetrics.getStringBounds(tmpString, g);
    }
    
    return Math.min(fontSize, MAXFONTSIZE);
}

"Zooming"

There is, of course, the eventuality that a string is so long that the resulting font size would be unreadable. So I introduced a variable MINFONTSIZE which is limiting the font size to a minimum value. The first idea was to just not draw the strings if the font size is too small, but then I thought of a better solution – to "zoom" in the wheel, so that the font can be larger. So, if the font size is too small, the wheel is proportionally enlarged in order that the minimum font size is satisfied. This is happening inside the drawImage method:

Java
// Adjust the parameters (for "zoom in") - if the font size is too small
if(fontSize < MINFONTSIZE) {
    _zoomFactor = (double)MINFONTSIZE / fontSize;
    width += (int) 2 * ((_zoomFactor * _radius) - _radius);
    height += (int) 2 * ((_zoomFactor * _radius) - _radius);
    _radius = (int)(_zoomFactor * _radius);
    img = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
    g2d = (Graphics2D) img.getGraphics();
    maxStringWidth = (int)(_radius - 2 * stringDistanceFromEdge);
    fontSize = calcFontSize(g2d, stringDistanceFromEdge, maxStringWidth);
}

Now, the wheel image is larger than the surrounding container (JPanel), so it needs to be properly placed. Also, a new rotation center needs to be calculated. This is a part of the paintComponent overriden method, and is only calculated once after the wheel image is drawn:

Java
if(_image == null) {
    _image = drawImage();
    _rotationCenter = new Point2D.Double(
            this.getWidth() - _image.getWidth(null) + _center.getX(),
            this.getHeight() / 2
        );
    _imagePosition = new Point2D.Double(
                (int)(this.getWidth() - _image.getWidth(null)),
                (int)(this.getHeight() / 2 - _center.getY())
            );
}

This is how it looks like:

Image 5

Drawing in a Loop

Each section of the wheel is drawn in a separate iteration of the loop. The simplest way of drawing the wheel was to just turn it around bit by bit and draw the necessary elements.

The loop is going through as many iterations as there are strings in the ArrayList (which is equal to the number of sections of the wheel).

First, the line (section border) from the centre of the wheel to the 3 o'clock mark is drawn.

Java
// Draw section border
if(hasBorders) {
    g2d.setColor(Color.BLACK);
    g2d.drawLine((int)_center.getX(), (int)_center.getY(), 
    (int)_center.getX() + _radius, (int)_center.getY());
}

(The border can be switched on and of with hasBorders variable.)

Then, the section above it is filled with color. The angle of the section is 360 / (number of sections). The color is chosen from the _colors ArrayList variable.

Java
// Fill section depending on the chosen shape
g2d.setColor(_colors.get(_colorCounter++ % _colors.size()));
if(_shape == Shape.UMBRELLA)
    fillTriangle(g2d);
else //if(_shape == Shape.CIRCLE)
    fillArc(g2d);

Depending on the chosen Shape of the wheel, the section is either drawn as an arc or as a triangle.

Java
private void fillArc(Graphics g2d) {
    g2d.fillArc((int)_center.getX() - _radius, (int)_center.getY() - 
    _radius, 2 * _radius, 2 * _radius, 0, (int)- Math.ceil(_delta)); // use ceil 
                                         // because of decimal part (would be left empty)
    if(hasBorders) {
        g2d.setColor(Color.black);
        g2d.drawArc((int)_center.getX() - _radius, (int)_center.getY() - 
                   _radius, 2 * _radius, 2 * _radius, 0, (int)- Math.ceil(_delta));
    }
}

private void fillTriangle(Graphics2D g2d) {
    /*
     * Method that draws section as a triangle (in case Shape=UMBRELLA was chosen)
     */
    int[] xpoints = new int[3];
    xpoints[0] = (int)_center.getX();
    xpoints[1] = (int)_center.getX() + _radius;
    int dx = (int) (2 * _radius * Math.pow(Math.sin(Math.toRadians(_delta / 2)), 2));
    xpoints[2] = xpoints[1] - dx;
    int[] ypoints = new int[3];
    ypoints[0] = (int)_center.getY();
    ypoints[1] = (int)_center.getY();
    int dy = (int) (2 * _radius * Math.sin(Math.toRadians
             (_delta / 2)) * Math.cos(Math.toRadians(_delta / 2)));
    ypoints[2] = ypoints[1] + dy;
    g2d.fillPolygon(xpoints, ypoints, 3);
    if(hasBorders) {
        g2d.setColor(Color.black);
        g2d.drawLine(xpoints[1], ypoints[1], xpoints[2], ypoints[2]);
    }
}

Next, the string is drawn. The wheel is rotated first for half a section angle so that the string would be drawn across the middle, and after drawing the string, it is rotated for another half of the section angle.

Java
// Draw string - rotate half delta, then draw then rotate the other half 
// (to have the string in the middle)
g2d.rotate(Math.toRadians(_delta / 2), _center.getX(), _center.getY());
g2d.setColor(Color.BLACK);
fontMetrics = g2d.getFontMetrics();
stringWidth = fontMetrics.stringWidth(_stringList.get(i));
g2d.drawString(_stringList.get(i), (int)(_center.getX() + 
               maxStringWidth - stringWidth + stringDistanceFromEdge), 
               (int)(_center.getY() + (double)fontMetrics.getHeight() / 2 - 
               fontMetrics.getMaxDescent()));
g2d.rotate(Math.toRadians(_delta / 2), _center.getX(), _center.getY());

Rendering Hints

Always be sure to use rendering hints when drawing in Java, otherwise the image can become very grainy. These are the hints I used:

Java
// Set rendering hints
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);

Wheel Rotation

The rotation itself is happening in a special class SpinRunnable which implements the Runnable interface:

Java
private class SpinRunnable implements Runnable {
    /*
     * Runnable class that handles the spinning of the wheel.
     * It sets the rotation angle by calculating the speed through time based on deceleration.
     * Each setRotationAngle call will cause the wheel to be redrawn.
     */
    private double spinSpeed;
    private int spinDirection;
    private double spinDeceleration;

    public SpinRunnable(double speed, int direction, double deceleration) {
        this.spinSpeed = speed;
        this.spinDirection = direction;
        this.spinDeceleration = deceleration;
    }

    public void run()
    {
        _spinOnOff = true;
        int sleepTime = 1000 / _refreshRate;
        double delta;
        while(_spinOnOff && spinSpeed > 0)
        {
            delta = spinDirection * (spinSpeed / _refreshRate);
            try {
                Thread.sleep(sleepTime);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            setRotationAngle(getRotationAngle() + delta);
            spinSpeed += spinDeceleration / _refreshRate;
        }
        _spinOnOff = false;
    }
}

The spinning is accomplished quite simply – by decreasing the speed by the deceleration factor over time, and calculating and setting rotation angle until the speed is 0 or until the spinning is turned off (_spinOnOff == false).

An instance of this class is initiated and its run method started by spinStartAsync method. The method is starting a new Thread, so that the spinning wouldn't block the rest of the code.

Java
public void spinStartAsync(double speed, int direction, double deceleration) throws Exception
{
    /*
     * Method that starts the spinning thread.
     * Parameters:
     * speed => degrees per second
     * direction => "< 0" = clockwise , "> 0" = counter-clockwise, "=0" = stand still
     * deceleration => "< 0" = degrees per second per second reducing speed, 
     * "= 0" = perpetual spin, "> 0" = throw exception
     */
    
    if(deceleration > 0)
        throw new Exception("Illegal parameter value: acceleration must be < 0");
    SpinRunnable spinRunnable = new SpinRunnable(speed, direction, deceleration);
    Thread t = new Thread(spinRunnable);
    t.start();
}

Tracking the Speed of Rotation

To track the speed of rotation, there is also a separate class which extends the TimerTask class.

Java
private class speedTimerTask extends TimerTask {
    /*
     * TimerTask class that monitors and refreshes the _spinSpeed
     * The speed is calculated as a difference of two rotation angles over a period of time.
     * We add the 360 to the "now" angle and then MOD it by 360 
     * to avoid miscalculation when passing the full circle.
     */
    @Override
    public void run() {
        double prevAngle, nowAngle;
        long sleepTime = 100;
        while(true) {
            prevAngle = getRotationAngle();
            try {
                Thread.sleep(sleepTime);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            nowAngle = getRotationAngle();
            nowAngle = (nowAngle + Math.signum(nowAngle) * 360) % 360;
            _spinSpeed = Math.abs(nowAngle - prevAngle) * (1000 / sleepTime);
        }
    }
}

This class contains the run method which is also run in a separate thread – it runs in an infinite loop where, in each iteration, it calculates the angle change over time, and based on that, sets the _spinSpeed variable.

History

  • 4th July, 2020: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)