27.07.2013

# JavaFX - Lottery Wheel

This article shows how to use `TranslateTransition`s, `FadeTransition`s, `Timeline`s and Timeline chaining to implement a simple lottery wheel where names rotate around a point. The wheel reduces its speed continuously and at the end the winner name is highlighted. The names are loaded from a simple text file and positioned around the middle point. The winner is indicated by an arrow which is created with a `Path`. The `Font` size and the `Color` of the names depends on the angle where they are placed around the circle.

Hint: The source code is available at GitHub. The link is at the end of the text.

The following video shows the Lottery Wheel in action:

I divided the application into the following parts:

• Calculate the points around the center where the names should be positioned
• Create a bunch of timelines with increasing duration
• Use transitions to animate the movements of the names
• Format the names depending on angle where they are placed
• Draw an arrow and a `Rectangle` with round edges to highlight the winner
• A CSS file for the styling
• Load and shuffle the names (Didn't explain that in the article. Code at GitHub.)

## General hints

The following screenshot shows the lottery wheel and the angles I used to position the names. The angle with the degree 0 is on the left hand side where the arrow and the rectangle is placed. Keep in mind that normally this is the angle with 270 degrees. The angles are rotated by 270 degrees clockwise.

## Position the names around the center

The following code calculates the points where the names should be positioned around the center. Important is the `startAngleDegree` because the rotation should start at an angle of 270 degrees.

First you have to divide 360 degrees by the number of names to determine the rotation angle. E.g. if you have 36 names every name is rotated by 10 degrees. So it is possible that the distance between the names are equal.

If you want to use sine (`Math.sin()`) and cosine (`Math.cos()`) you have to calculate the radian value of the degree value. This done by multiplying the angle with `Math.PI / 180`.

All calculated points and degrees are stored in a `List` of `Point`s.

```	private List<Point> calculatePoints(int points, double centerX, double centerY, double radius) {
List<Point> pointList = new ArrayList<>();

double rotateAngleDegree = 360d / (double) names.size();

double startAngleDegree = 270;
for (int rotationStep = 0; rotationStep < points; rotationStep++) {
double degreeStart = rotationStep * rotateAngleDegree;

double angleAlpha = (degreeStart + startAngleDegree) * (Math.PI / 180);
Point p = new Point();
p.x = CENTER_X + ORBIT * Math.sin(angleAlpha);
p.y = CENTER_Y - ORBIT * Math.cos(angleAlpha);
p.position = rotationStep;
p.angle = degreeStart;
}

return pointList;
}
```

To store the points a simple data structure `Point` is used. Nothing special. No getters or setters.

```	class Point {
double x;
double y;
int position;
double angle;

public String toString() {
return position + " " + x + " " + y + " " + angle;
}
}
```

## Create the Timelines

One important part is the creation of the Timeslines:

```		// Number of rotations per timeline
int cyclesPerTimeline = 5;

// Create timeline with different durations to slow down the wheel
// Timeline chaining
Timeline nextTimeline = null;

for (int numberOfTimelines = 10; numberOfTimelines > 0; numberOfTimelines--) {
final KeyFrame duration = new KeyFrame(Duration.millis(100 * numberOfTimelines));
nextTimeline = createTimeline(root, duration, nextTimeline);
nextTimeline.setCycleCount(cyclesPerTimeline);
}
```

Every timeline animation is repeated for `cyclesPerTimeline` (e.g. `5`). In this example `10` timeslines are created. For every timeline a duration in milliseconds is defined. Because the lottery wheel should slow down, the duration each timeline needs increases.

In line 10 the method `createTimeline` is called. Important is the parameter `nextTimeline`. This is the timeline which will be started when the previous timeline has finished. This functionality is realized with the method `setOnFinished` in line 13 below (highlighted).

```private Timeline createTimeline(final Pane root, final KeyFrame duration, final Timeline nextTimeline) {
final Timeline timeline = new Timeline(new KeyFrame(Duration.ZERO, new EventHandler<ActionEvent>() {

@Override
public void handle(ActionEvent event) {

moveNames(duration);

}
}), duration);

// Start next timeline on finish
timeline.setOnFinished(new EventHandler<ActionEvent>() {
public void handle(ActionEvent event) {

handleNextTimelineAndWinner(root, nextTimeline);

};
});

return timeline;
}
```

## Using Transitions

The transitions to move the names from one position to the next are part of the method `moveNames`.

```private void moveNames(final KeyFrame duration) {
for (Text name : names) {
Point nextPoint = getNextPoint((Integer) name.getUserData());
TranslateTransition move = TranslateTransitionBuilder.create()
.node(name)
.fromX(name.translateXProperty().doubleValue())
.fromY(name.translateYProperty().doubleValue())
.toX(nextPoint.x)
.toY(nextPoint.y)
.duration(duration.getTime())
.build();

formatText(name, nextPoint);

ParallelTransition parallelTransition = new ParallelTransition();
if (nextPoint.position==0) {
// Make the text invisible
} else if(nextPoint.position==1) {
// Show the name at the arrow
arrowText.setText(name.getText());

// Make the text visible
}

parallelTransition.playFromStart();

name.setUserData(((Integer) name.getUserData()) + 1);

if (((Integer) name.getUserData()) > points.size() - 1) {
name.setUserData(0);
}
}
}
```

Important is that the position of the name is stored in the userData of the `Text` object. This value is used to deternine the next `Point`. For every `name` object a `TranslateTransition` is used to move the text from the actual point (including the translations used before) to the next point.

If the `name` is moving to the winning position a `FadeTransition` is used to make the text nearly invisible. If the `name` is moving from the winning position to the next position the text will be visible again. (I wanted to use the `FadeTransition` :-) )

Both transitions are executed parallel with a `ParallelTransition`. The method `getNextPoint` is nothing special:

```private Point getNextPoint(int positionRect) {
return positionRect + 1 >= points.size() ? points.get(0) : points.get(positionRect + 1);
}
```

## Timeline chaining

The part where I connect the timelines I called Timeline chaining. I'm not sure if this is the best way to slow down an animation, but I liked the solution. :-)

The method `handleNextTimelineAndWinner` calls the next timeline, if there is one. Otherwise it was the last timeline in the chain and the winner can be displayed.

We have to make the `winnerName` visible with a `FadeTransition`. Remove the `arrowText`, switch the styling and position the text.

```private void handleNextTimelineAndWinner(Pane root, Timeline nextTimeline) {
if (nextTimeline != null) {
// Start the next timeline
nextTimeline.play();
} else {
// Only executed if the last timeline is reached

// Make the text visible again

root.getChildren().remove(arrowText);

// If it was the last timeline in the chain
// highlight the name of the winner
winnerName.setId("winnertext");
winnerName.setTranslateY(0);
winnerName.setY(5);
}
}
```

## Formatting the Text

The method `formatText` is used to format the names which rotate around the center. Depending on the angle the `Text` changes its color and size. If you recall the screenshot with the angles above the `point.angle` used in the if-statements in the following method are more comprehensible.

Yes, the variable `angle` with the degree angle could be replace by radian degrees, but this solution is better to maintain.

```private void formatText(Text name, Point point) {
Font font;
if (point.angle == 0) {
font = Font.font("Verdana", FontWeight.BOLD, FONT_SIZE * 1.8);
name.setFill(Color.RED);
winnerName = name;
} else if (point.angle > 349 || point.angle < 11) {
font = Font.font("Verdana", FontWeight.BOLD, FONT_SIZE * 1.6);
name.setFill(Color.ORANGERED);
} else if (point.angle > 327 || point.angle < 22) {
font = Font.font("Verdana", FontWeight.BOLD, FONT_SIZE * 1.5);
name.setFill(Color.ORANGERED);
} else if (point.angle > 305 || point.angle < 45) {
font = Font.font("Verdana", FontWeight.BOLD, FONT_SIZE * 1.4);
name.setFill(Color.ORANGE);
} else if (point.angle > 292 || point.angle < 67) {
font = Font.font("Verdana", FontWeight.BOLD, FONT_SIZE * 1.3);
name.setFill(Color.YELLOW);
} else if (point.angle > 270 || point.angle < 90) {
font = Font.font("Verdana", FontWeight.BOLD, FONT_SIZE * 1.2);
name.setFill(Color.YELLOW);
} else if (point.angle > 247 || point.angle < 112) {
font = Font.font("Verdana", FontWeight.BOLD, FONT_SIZE * 1.1);
name.setFill(Color.YELLOW);
} else if (point.angle > 225 || point.angle < 135) {
font = Font.font("Verdana", FontWeight.BOLD, FONT_SIZE * 1);
name.setFill(Color.YELLOWGREEN);
} else if (point.angle > 202 || point.angle < 157) {
font = Font.font("Verdana", FontWeight.BOLD, FONT_SIZE * 0.9);
name.setFill(Color.YELLOWGREEN);
} else {
font = Font.font("Verdana", FontWeight.BOLD, FONT_SIZE * 0.8);
name.setFill(Color.YELLOWGREEN);
}
name.setFont(font);
return;
}
```

## Using Paths

The following method draws the arrow with a `Path`. First the top of the arrow is drawn and second the bottom of it. The colors are defined in the css files and the `styleClass` `arrowStyleUp` and `arrowStyleDown` is used.

Keep in mind that this solution is not very maintainable. A better way to draw such objects is using an external vector drawing program (e.g. InkScape) and export the coordinates. These coordinates can be used as input for the `Path`.

```private void paintArrow(Pane root) {
double rightSpace = WIDHT / 2 - ORBIT - 90;

Path path = PathBuilder.create()
.styleClass("arrowStyleUp")
.elements(
new MoveTo(rightSpace, CENTER_Y - 22),
new LineTo(rightSpace + 50, CENTER_Y - 22),
new LineTo(rightSpace + 50, CENTER_Y - 30),
new LineTo(rightSpace + 70, CENTER_Y - 12),
new LineTo(rightSpace, CENTER_Y - 12),
new LineTo(rightSpace, CENTER_Y - 22)
).build();

path = PathBuilder.create()
.styleClass("arrowStyleDown")
.elements(
new MoveTo(rightSpace, CENTER_Y - 12),
new LineTo(rightSpace + 70, CENTER_Y - 12),
new LineTo(rightSpace + 50, CENTER_Y + 6),
new LineTo(rightSpace + 50, CENTER_Y - 2),
new LineTo(rightSpace, CENTER_Y - 2),
new LineTo(rightSpace, CENTER_Y - 12)
).build();

}
```

## Putting all together

The `start` method puts all things together and draws the `Rectangle` with the rounded edges.

```@Override
public void start(Stage primaryStage) {
final Pane root = new Pane();

// Load all name from the file

shuffleNames();

// Calculate all points where the names should be positioned.
points = calculatePoints(names.size(), CENTER_X, CENTER_Y, ORBIT);

// Number of rotations per timeline
int cyclesPerTimeline = 5;

// Create recursive timeline to slow down the wheel
// Timeline chaining
Timeline nextTimeline = null;

for (int numberOfTimelines = 10; numberOfTimelines > 0; numberOfTimelines--) {
final KeyFrame duration = new KeyFrame(Duration.millis(100 * numberOfTimelines));
nextTimeline = createTimeline(root, duration, nextTimeline);
nextTimeline.setCycleCount(cyclesPerTimeline);
}

arrowText = new Text("Winner");
arrowText.setX(250);
arrowText.setY(CENTER_Y);

Rectangle rectangle = new Rectangle();
rectangle.setX(240);
rectangle.setY((HEIGHT-75)/2);
rectangle.setWidth(450);
rectangle.setHeight(60);

paintArrow(root);

Scene scene = new Scene(root, WIDHT, HEIGHT);

primaryStage.setTitle("JUG-DA Wheel of Fortune");
primaryStage.setScene(scene);
primaryStage.show();

nextTimeline.play();
}
```

## CSS files

If you want to change the colors of the background, the arrow or the rectangle you can do this in the css file:

```.root {
}

#winnertext {
-fx-font: 40px Tahoma;
-fx-fill: linear-gradient(from 0% 0% to 100% 200%, repeat, #ff00ff 0%, GREEN 50%);
-fx-stroke: YELLOW;
-fx-stroke-width: 1;
}

.arrowStyleUp {
-fx-stroke: #ff7e79;
-fx-fill: #ff7e79;
-fx-stroke-width: 1px;
}

.arrowStyleDown {
-fx-stroke: #ff2600;
-fx-fill: #ff2600;
-fx-stroke-width: 1px;
}

.arrow-text {
-fx-fill: RED;
-fx-font-family: Verdana;
-fx-font-weight: bold;
-fx-font-size: 22;
}

.rectangle {
-fx-stroke: RED;
-fx-fill: TRANSPARENT;
-fx-stroke-width: 5;
-fx-arc-width: 30;
-fx-arc-height: 30;
}
```

## Source code at GitHub

The source code can be found at GitHub: GitHub JavaFX-LotteryWheel