Building GUI Applications With JavaFX

A frame with an image as background

Lesson 7: Creating Animated Objects


This section introduces a step-by-step procedure that demonstrates how to add animation to a simple application. It will guide you through the creation of a cloud that travels across a sky of sunshine, and bounces off the window borders, as displayed in the following window.

As you learned in Presenting UI Objects in a Graphical Scene, UI components, shapes, text, and images are considered a hierarchy of objects in a graphical scene. Animation of these graphical objects also takes place in scene, so the first step is to create a scene.

  1. Add import statements for the Stage, Scene, and Color classes.

  2. Add the Stage object literal and define the title instance variable.

  3. Add the Scene object literal to the scene instance variable of the Stage class.

  4. Define the color of the scene using the fill variable of the Scene class.

    import javafx.stage.Stage;
    import javafx.scene.Scene;
    import javafx.scene.paint.Color;

    Stage{
    title: "Cloud"
    scene:
    Scene{
    fill: Color.WHITE
    }//Scene
    }//Stage

Refer to Using Declarative Syntax for more information about declarative syntax employed in the JavaFX Script programming language.

In the JavaFX SDK, images are created using the javafx.scene.image.Image class, where the image location is specified in the url instance variable. Note that only a Node object can be added to a scene's content, so you need to use an adapter class, called ImageView. More details about scene and nodes are in Presenting UI Objects in a Graphical Scene.

  1. Add two new imports for the Image and ImageView classes.

  2. Set an image that will serve as a background for the traveling cloud. Use the sun.jpg image. When specifying the image url, you can set the URI of the resource or use the relative codebase. In this example the image url is set using the __DIR__ variable that indicates the directory where the image is located. By default it points to the current directory, so make sure that the sun image is located in the same directory as application compiled classes. To run the application on the mobile emulator make sure that the image is packed into the application jar file along with the compiled classes.

These changes are reflected in the modified code shown below:

import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.paint.Color;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
Stage{
title: "Cloud"
scene:
Scene{
fill: Color.WHITE
content:[
ImageView{
image: Image{
url: "{__DIR__}sun.jpg"
}//Image
}//ImageView
]
}//Scene
}//Stage

When compiled and run on the mobile emulator, this modified code produces the following window.

A frame with an image as background
Figure 1: A scene with an image as background

Create the actual cloud by drawing five successive arcs, joining the last one to the first. The end point of the first arc is the start point of the second arc. This is illustrated in the following diagram.

End-points of arcs that form cloud
Figure 2: End-points of arcs that form cloud

To draw this cloud in your code you need to perform the following steps:

  1. Add import statements for the MoveTo, ArcTo, Path, LinearGradient, and Stop classes. Refer to Creating Graphical Objects for more information about shapes and filling methods.

  2. Use the MoveTo, ArcTo, and Path classes from the javafx.scene.shape package as shown in the following code fragment.

       Path {
    stroke: Color.DODGERBLUE
    fill: LinearGradient {
    startX:60,
    startY:10,
    endX:10
    endY:80 ,
    proportional: false
    stops: [
    Stop {offset: 0.0 color: Color.DODGERBLUE},
    Stop {offset: 0.5 color: Color.LIGHTSKYBLUE},
    Stop {offset: 1.0 color: Color.WHITE}
    ]
    }
    elements: [
    MoveTo {x: 15 y: 15 },
    ArcTo {x: 50 y: 10 radiusX: 20 radiusY: 20 sweepFlag: true},
    ArcTo {x: 70 y: 20 radiusX: 20 radiusY: 20 sweepFlag: true},
    ArcTo {x: 50 y: 60 radiusX: 20 radiusY: 20 sweepFlag: true},
    ArcTo {x: 20 y: 50 radiusX: 10 radiusY: 5 sweepFlag: true},
    ArcTo {x: 15 y: 15 radiusX: 10 radiusY: 10 sweepFlag: true},
    ]
    }//Path

    The MoveTo class indicates the start point for the shape, and the ArcTo class creates an arc segment. All segments are combined into a shape using the Path class which represents a simple shape, and enables basic construction of a geometric path. The Path class is helpful when you need to create your own shape that is different from the primitive graphic shapes available in the javafx.scene.shape package. The Path class extends the Node class and inherits all of its instance variables and functions.


    Note: The sweepFlag instance variable is set to true so that the arc be drawn clockwise, in a "positive" angle. If the arcs are drawn counterclockwise, they will not curve correctly.

The following modified code includes a graphical scene, an image, and a cloud:

import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.paint.Color;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.paint.LinearGradient;
import javafx.scene.paint.Stop;
import javafx.scene.shape.ArcTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.Path;

Stage{
title: "Cloud"
scene: Scene{
fill: Color.WHITE
content:[
ImageView{
image: Image{
url: "{__DIR__}sun.jpg"
}
},//ImageView
Path {
stroke: Color.DODGERBLUE
fill: LinearGradient {
startX:60,
startY:10,
endX:10
endY:80 ,
proportional: false
stops: [
Stop {offset: 0.0 color: Color.DODGERBLUE},
Stop {offset: 0.5 color: Color.LIGHTSKYBLUE},
Stop {offset: 1.0 color: Color.WHITE}
]
}//LinearGradient
elements: [
MoveTo {x: 15 y: 15 },
ArcTo {x: 50 y: 10 radiusX: 20 radiusY: 20 sweepFlag: true},
ArcTo {x: 70 y: 20 radiusX: 20 radiusY: 20 sweepFlag: true},
ArcTo {x: 50 y: 60 radiusX: 20 radiusY: 20 sweepFlag: true},
ArcTo {x: 20 y: 50 radiusX: 10 radiusY: 5 sweepFlag: true},
ArcTo {x: 15 y: 15 radiusX: 10 radiusY: 10 sweepFlag: true},
]
}//Path
]
}//Scene
}//Stage

When compiled and run in a desktop window, this modified code produces the following window.

A frame with an image and a cloud-looking shape
Figure 3: A window with an image and a cloud-looking shape

The next step is to animate the cloud. The JavaFX Script Language supports the Key Frame animation concept. This means that the animated state transitions of the graphical scene are declared by start and end snapshots (key frames) of the scene's state at certain points in time. Given these two states, the system can automatically perform the animation. It can stop, pause, resume, reverse or repeat movement when requested.

First, simplify the task by animating the cloud horizontally, with no vertical motion. Later you will add in the vertical motion. To animate the could horizontally, alter the translateX instance variable of the Path object, and leave the translateY instance variable constant. Perform the following steps:

  1. Set the translateY variable of the Path object to 100.

  2. Define key frames for the start point (0, 100) and the end point (158, 100). To calculate these values, take into consideration the image size, which is 241x332, and the shape size, which is 83x64. These measurements are illustrated in the following diagram.

    Key Frames
    Figure 4: Key Frames

    Animation occurs along a timeline, represented by a javafx.animation.Timeline object. Each timeline contains two or more key frames, represented by javafx.animation.KeyFrame objects.

  3. Create a timeline with two key frames to perform the cloud's horizontal movement and starts the movement when the application is launched. Positions between the start and the end points are calculated using linear interpolation.

    import javafx.animation.Timeline;
    import javafx.animation.KeyFrame;
    import javafx.animation.Interpolator;

    var x: Number;

    Timeline {
    keyFrames: [
    KeyFrame{
    time: 0s
    values: x => 0.0
    },

    KeyFrame{
    time: 4s
    values: x => 158.0 tween Interpolator.LINEAR
    }
    ]
    }.play();

    The time instance variable defines the elapsed time at which the associated values will be set within a single cycle of the Timeline object. The time is a variable of the javafx.lang.Duration class that takes a Number value followed by "s" or "ms" to indicate seconds or milliseconds. The => operator provides a literal constructor for a list of key values. The tween operator is a literal constructor for an interpolated value. Therefore the cloud begins at pixel 0 and moves to position 158 over the course of four seconds.

    Although KeyFrame animations are typical JavaFX objects, special syntax is provided to make it easier to express animation than is possible with the standard object-literal syntax. The trigger clause enables you to associate an arbitrary callback with the key frame. The time specified by at is relative to the start of the timeline. This capability simplifies the code as follows:

    import javafx.animation.Timeline;
    import javafx.animation.Interpolator;

    var x: Number;

    Timeline {
    keyFrames: [
    at (0s) {x => 0.0},
    at (4s) {x => 158.0 tween Interpolator.LINEAR}
    ]
    }.play();


  4. Bind the translateX instance variable of the Path object to the x variable as shown in the following code fragment:

    Path{
    ...
    translateX: bind x
    ...
    }

    When the x variable changes, the translateX variable of the Path object also changes. More details about the Data Binding mechanism are in Applying Data Binding to UI Objects.

You can use instance variables of the Timeline class to control the timeline cycle.

  1. Set the repeatCount instance variable to Timeline.INDEFINITE to loop the animation.

  2. Set the autoReverse instance variable to true to enable two-way movement.

The following code accomplishes these tasks:

import javafx.animation.Timeline;
import javafx.animation.Interpolator;

var x: Number;

Timeline {
repeatCount: Timeline.INDEFINITE
autoReverse: true
keyFrames: [
at (0s) {x => 0.0},
at (4s) {x => 158.0 tween Interpolator.LINEAR}
]
}.play();


The modified code of the application is shown below:

import javafx.animation.Interpolator;
import javafx.animation.Timeline;
import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.paint.Color;
import javafx.scene.paint.LinearGradient;
import javafx.scene.paint.Stop;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.shape.ArcTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.Path;

var x: Number;

Timeline {
repeatCount: Timeline.INDEFINITE
autoReverse: true
keyFrames: [
at (0s) {x => 0.0},
at (4s) {x => 158.0 tween Interpolator.LINEAR}
]
}.play();

Stage{
title: "Cloud"
scene: Scene{
fill: Color.WHITE
content:[
ImageView{
image: Image{
url: "{__DIR__}sun.jpg"
}},
Path {
translateX: bind x
translateY: 100
stroke: Color.DODGERBLUE
fill: LinearGradient {
startX:60,
startY:10,
endX:10
endY:80 ,
proportional: false
stops: [
Stop {offset: 0.0 color: Color.DODGERBLUE},
Stop {offset: 0.5 color: Color.LIGHTSKYBLUE},
Stop {offset: 1.0 color: Color.WHITE}
]
}
elements: [
MoveTo {x: 15 y: 15 },
ArcTo {x: 50 y: 10 radiusX: 20 radiusY: 20 sweepFlag: true},
ArcTo {x: 70 y: 20 radiusX: 20 radiusY: 20 sweepFlag: true},
ArcTo {x: 50 y: 60 radiusX: 20 radiusY: 20 sweepFlag: true},
ArcTo {x: 20 y: 50 radiusX: 10 radiusY: 5 sweepFlag: true},
ArcTo {x: 15 y: 15 radiusX: 10 radiusY: 10 sweepFlag: true},
]
}//Path
]
}//Scene
}//Stage

When compiled and run this code produces the following window:

This animation application uses linear interpolation which moves the object in even time increments. You can play with other forms of interpolation. For example, if you set the Interpolator.EASEBOTH type, the cloud will slightly slow down at the start and at the end of the timeline cycle.

You can enhance the application by creating the originally desired floating effect.

  1. Create another timeline for the y coordinate of the shape.

  2. Bind the translateY instance variable to the y value as shown on the following code fragment:

    var y: Number;

    Timeline {
    repeatCount: Timeline.INDEFINITE
    autoReverse: true
    keyFrames: [
    at (0s) {y => 0.0},
    at (7s) {y => 258.0 tween Interpolator.LINEAR},
    ]
    }.play();
    ...

    Path{
    ...
    translateY: bind y
    ...
    }//Path



    Note: The y variable attains its maximum position after seven seconds, which is faster than the x variable. Therefore, the translateY value changes faster than translateX. This produces a wandering effect.

    The following is the complete code of the example.

    import javafx.animation.Interpolator;
    import javafx.animation.Timeline;
    import javafx.stage.Stage;
    import javafx.scene.Scene;
    import javafx.scene.paint.Color;
    import javafx.scene.paint.LinearGradient;
    import javafx.scene.paint.Stop;
    import javafx.scene.image.Image;
    import javafx.scene.image.ImageView;
    import javafx.scene.shape.ArcTo;
    import javafx.scene.shape.MoveTo;
    import javafx.scene.shape.Path;

    var x: Number;

    Timeline {
    repeatCount: Timeline.INDEFINITE
    autoReverse: true
    keyFrames: [
    at (0s) {x => 0.0},
    at (4s) {x => 158.0 tween Interpolator.LINEAR}
    ]
    }.play();

    var y: Number;

    Timeline {
    repeatCount: Timeline.INDEFINITE
    autoReverse: true
    keyFrames: [
    at (0s) {y => 0.0},
    at (7s) {y => 258.0 tween Interpolator.LINEAR},
    ]
    }.play();

    Stage{
    title: "Cloud"
    scene: Scene{
    fill: Color.WHITE
    content:[
    ImageView{
    image: Image{
    url: "{__DIR__}sun.jpg"
    }},
    Path {
    translateX: bind x
    translateY: bind y
    stroke: Color.DODGERBLUE
    fill: LinearGradient {
    startX:60,
    startY:10,
    endX:10
    endY:80 ,
    proportional: false
    stops: [
    Stop {offset: 0.0 color: Color.DODGERBLUE},
    Stop {offset: 0.5 color: Color.LIGHTSKYBLUE},
    Stop {offset: 1.0 color: Color.WHITE}
    ]
    }
    elements: [
    MoveTo {x: 15 y: 15 },
    ArcTo {x: 50 y: 10 radiusX: 20 radiusY: 20 sweepFlag: true},
    ArcTo {x: 70 y: 20 radiusX: 20 radiusY: 20 sweepFlag: true},
    ArcTo {x: 50 y: 60 radiusX: 20 radiusY: 20 sweepFlag: true},
    ArcTo {x: 20 y: 50 radiusX: 10 radiusY: 5 sweepFlag: true},
    ArcTo {x: 15 y: 15 radiusX: 10 radiusY: 10 sweepFlag: true},
    ]
    }//Path
    ]
    }//Scene
    }//Stage


    When compiled and run, this code produces the following window.

    Cloud bouncing within window
    Figure 5: The application run on the mobile emulator

Desktop Profile


You can apply one of the effects available in the javafx.scene.effect and javafx.scene.effect.light packages to visually enhance the application. These packages exist only in the desktop profile. Perform the following steps to create the lighting effect and make the cloud seem embossed.

  1. Add import statements for the javafx.scene.effect.Lighting and javafx.scene.effect.light.DistantLight classes

  2. Apply the following code for the effect instance variable of the Path object.

    effect: Lighting{light: DistantLight{azimuth: 90}}

This effect simulates lighting up the object with a distant light source. The azimuth instance variable defines the angle of the light source.

The complete code of the desktop-specific application is located in the cloudDesktop.fx file. When compiled and run this code produces the window.

Cloud bouncing within window
Figure 6: Desktop-specific application

0 comments:

Post a Comment