JavaFX - Rotating Circles

14.03.2013

JavaFX - Animation of Circles with a Timeline

The following article describes how to animate objects in JavaFX. In the example, a Scene is created which contains some objects (javafx.scene.shape.Circle-Objects) which are flying around an imaginary middlepoint. The animation is realized with a JavaFX javafx.animation.Timeline and javafx.animation.TranslateTransitions to move the objects from one point to the next position around the orbit. The objects are styled by a CSS file which defines a radial-gradient to change the style (color and texture) of the Circle.

The following screenshot shows the finished JavaFX application.

Here is the source code of the application:

package org.hameister.rotating;

import java.util.ArrayList;
import java.util.List;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.animation.TranslateTransition;
import javafx.animation.TranslateTransitionBuilder;
import javafx.application.Application;
import javafx.event.Event;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.shape.Circle;
import javafx.scene.shape.CircleBuilder;
import javafx.stage.Stage;
import javafx.util.Duration;

import static javafx.util.Duration.millis;

public class RotatingObjects extends Application {

    // Number of steps for a circle/ball during one orbit.
    private static final int MOVING_POINTS = 60;
    // Ball speed
    private static final int STEP_DURATION_IN_MILLISECONDS = 100;
    // Scene Size
    private static final int SCENE_SIZE = 800;

    @Override
    public void start(Stage primaryStage) {
        final int midPoint = SCENE_SIZE / 2;

        // Orbits of the balls (Radius)
        final int[] ballOrbits = {300, 200, 100, 50 ,20, 1};
        // Size of the circles
        final int[] ballRadius = {30, 20, 10, 5, 2 ,1 };

        final List<Circle> balls = new ArrayList<>();
        CircleBuilder builder = CircleBuilder.create().centerX(0).centerY(0).styleClass("ball-style");
        for (int i = 0; i < ballOrbits.length; i++) {
            balls.add(builder.radius(ballRadius[i]).build());
        }

        final Timeline timeline = new Timeline(new KeyFrame(Duration.ZERO, new EventHandler() {
            int movingStep = 0;

            @Override
            public void handle(Event event) {
                movingStep++;

                double angleAlpha = movingStep * ( Math.PI / 30 );

                for (int i = 0; i < balls.size(); i++) {
                    // p(x) = x(0) + r * sin(a)
                    // p(y) = y(y) - r * cos(a)
                    moveBall(balls.get(i), midPoint + ballOrbits[i] * Math.sin(angleAlpha), midPoint - ballOrbits[i] * Math.cos(angleAlpha));
                }
                
                // Reset after one orbit.
                if (movingStep == MOVING_POINTS) {
                    movingStep = 0;
                }
            }
        }), new KeyFrame(Duration.millis(STEP_DURATION_IN_MILLISECONDS)));

        timeline.setCycleCount(Timeline.INDEFINITE);

        Pane root = new Pane();
        root.getChildren().addAll(balls);

        Scene scene = new Scene(root, SCENE_SIZE, SCENE_SIZE);

        primaryStage.setTitle("Rotating Balls");
        primaryStage.setScene(scene);
        primaryStage.getScene().getStylesheets().add("rotatingObjects");
        primaryStage.show();

        timeline.play();
    }

    private void moveBall(Circle ball, double x, double y) {
        TranslateTransition move = TranslateTransitionBuilder.create()
                .node(ball)
                .toX(x)
                .toY(y)
                .duration(millis(STEP_DURATION_IN_MILLISECONDS))
                .build();

        move.playFromStart();
    }

    public static void main(String[] args) {
        launch(args);
    }
}

This example application has six Circle objects which are created with a CircleBuilder. Every Circle has an orbit (ballOrbits) to define the distance from the middlepoint and a radius (ballRadius) to define the size of the rotating circle.

The method handle(Event event) in the Timeline is invoked every 100 milliseconds (STEP_DURATION_IN_MILLISECONDS) and moves all Circle objects one step further with a TranslateTranssition in the method moveBall.

Every Circle object is moved 60 times (MOVING_POINTS) for every orbit. This means that the angle between the moves is 6°. (60*6°=360° for one orbit).

A new position of an object is calculated with the formular: p(x) = x(0) + radius * sin(a) and p(y) = y(0) - radius * cos(a).

Attention should be paid to the calculation of the angle a (angleAlpha). You have to convert the angle from degrees to radians. The application uses this formular to calculate the angle angleAlpha in Line 52 to move an object by 6°.

a(deg)   a(rad)
------ = ------ 
360°      2*PI

         a(deg) * 2 * PI
a(rad) = ----------------
                 360°
                 
With a(deg)=6° =>

6° * 2 * PI    PI
----------- = ----
   360°        30

Here is the CSS file for the styling of the Circle objects. In the example above only the ball-style is used. If you want to change the color of the object, try one of the other styles.

.root {
    -fx-background-color: radial-gradient(center 50% 50%, radius 60%, reflect, #ADFF2F, black );
}
.ball-style {
    -fx-fill: radial-gradient(center 50% 18%, radius 50%, reflect, red, #CD2626 80% );
}

.ball-style-blue {
    -fx-fill: radial-gradient(center 50% 18%, radius 50%, reflect, blue, #0000CD 80% );
}

.ball-style-orange {
    -fx-fill: radial-gradient(center 50% 18%, radius 50%, reflect, orange, #CD8500 80% );
}

.ball-style-yellow {
    -fx-fill: radial-gradient(center 50% 18%, radius 50%, reflect, yellow, #CDCD00 80% );
}

In Line 76 of the Application the CSS file is loaded. The CircleBuilder in Line 40 uses the style ball-style from the CSS file to set a radial-gradient for the Circle.