Bounce: Nested Timeline Animation

By Josh Marinacci, September 26, 2008

The JavaFX Timeline class enables you to animate variables, and therefore objects, over time. Though you might often animate only one object at a time, the Timeline actually supports nested timelines, which enable you to build more complex animations. This example shows a master timeline that contains three nested timelines:

  • Timeline one moves the ball back and forth.
  • Timeline two moves the ball up and down.
  • Timeline three "squishes" the ball when it contacts the floor.

Understanding the Code

The model for this sample is pretty simple, as Figure 1 shows. The ball is defined by four variables: x, y, sx, and sy. The x and y variables represent the ball's current location. The sx and sy variables are the scale x and scale y of the ball. These two variables give the ball the appearance of squishing when it contacts the base of the floor.

Source Code
//width and height of the screen
var w = 240;
var h = 320;
// model variables
def rad = 50; // radius of the ball
var x = 0.0;    // x location of the ball
var y = 0.0;    // y location of the ball
var sx = 1.0;   // scale x
var sy = 1.0;   // scale y
var color1 = Color.SILVER;
var color2 = Color.MAROON;

Figure 1: Ball Data Model

There individual timelines animate the ball in different directions, as Figure 2 shows. The ax timeline animates the ball from side to side over 10 seconds. The ay timeline moves the ball up and down every 4.5 seconds. It uses a SPLINE interpolator to create a smoother bouncing motion. A third timeline, sxy, performs the actual squishing when the ball nears the floor.

Source Code
// animate side to side
var ax = Timeline {
    keyFrames:[
        KeyFrame { time: 0s values:  x => rad },
        KeyFrame { time: 3s values: x => w-rad tween Interpolator.LINEAR },
    ]
    autoReverse: true
    repeatCount: Timeline.INDEFINITE
}

// animate up and down
var ay = Timeline {
    repeatCount: Timeline.INDEFINITE
    keyFrames: [
        KeyFrame { time: 0s values: y => 0.0 },
        KeyFrame { time: 2.2s values: y => h-rad tween Interpolator.SPLINE(0,0,.5,0) },
        KeyFrame { time: 2.25s values: y => h-rad },
        KeyFrame { time: 4.5s values: y => 0.0 tween Interpolator.SPLINE(0,0,0,0.5) },
    ]
}

// animate the squish when boucing
var sxy =  Timeline {
    repeatCount: Timeline.INDEFINITE
    keyFrames: [
        KeyFrame { time: 2s values: [sx => 1.0, sy => 1.0 ] },
        KeyFrame { time: 2.25s
            values: [
                sx => 1.2 tween Interpolator.LINEAR,
                sy => 0.7 tween Interpolator.LINEAR,
            ]
        },
        KeyFrame { time: 2.5s
            values: [
                sx => 1.0 tween Interpolator.LINEAR,
                sy => 1.0 tween Interpolator.LINEAR,
            ]
        },
        KeyFrame { time: 4.5s
            values: [
                sx => 1.0 tween Interpolator.LINEAR,
                sy => 1.0 tween Interpolator.LINEAR,
            ]
        },
    ]
}

Figure 2: Ball Movement Timelines

The three timelines are nested together in a master timeline called clip, which loops forever, as Figure 3 shows.

Source Code
// loop the other timelines forever
var clip = Timeline {
    repeatCount: Timeline.INDEFINITE
    keyFrames:
        KeyFrame {
            time: 0s
            timelines: [ax, ay, sxy]
        }
}

Figure 3: Master Timeline

Now that the data model is animated properly, you can bind a shaded circle to the model to create a ball, as shown in Figure 4. The ball itself is a Circle with a radius of 50 and a fill that uses a RadialGradient to create the shading effect. To move the ball, the circle's transforms variable is bound to a sequence of four transforms. The first transform moves the ball. The second transform translates to the center of the ball so that the third transform performs do the squishing properly. The fourth transform simply undoes the second one.

Source Code
Circle {
    centerX: 0  centerY: 0  radius: rad
    stroke: Color.BLACK
    fill: RadialGradient {
        proportional: false  radius: 70
        centerX: 0  centerY: 0
        focusX: 20  focusY: -50
        stops: [
            Stop { offset: 0.0  color: color1 },
            Stop { offset: 0.85 color: color2 },
            Stop { offset: 1.0  color: color2 }
        ]
    }
    transforms: bind [
        Transform.translate(x, y),        // move left and right and up and down
        Transform.translate(50.0, 50.0),  // translate to the center of the ball
        Transform.scale(sx, sy),          // do the squish
        Transform.translate(-50.0, -50.0) // un-translate
    ]
}

Figure 4: Displaying the Actual Ball