Creating Transitions and Timeline Animation in JavaFX
2 Tree Animation Example
This chapter provides details about the Tree Animation example. You will learn how all the elements on the scene were created and animated. Figure 2-1 shows the scene with a tree.
Project and Elements
The Tree Animation project consists of several files. Each element, like leaves, grass blades, and others are created in separate classes. TreeGenerator class creates a tree from all the elements. Animator class contains all animation except grass animation that resides in the GrassWindAnimation class.
The scene in the example contains the following elements:
-
Tree with branches, leaves, and flowers
-
Grass
Each element is animated in its own fashion. Some animations run in parallel, and others run sequentially. The tree-growing animation is run only once, whereas the season-change animation is set to run infinitely.
The season-change animation includes the following parts:
-
Leaves and flowers appear on the tree
-
Flower petals fall and disappear
-
Leaves and grass change color
-
Leaves fall to the ground and disappear
Grass
This section describes how the grass is created and animated.
Creating Grass
In the Tree Animation example, the grass, shown in Figure 2-3 consists of separate grass blades, each of which is created using Path
and added to the list. Each blade is then curved and colored. An algorithm is used to randomize the height, curve, and color of the blades, and to distribute the blades on the "ground." You can specify the number of blades and the size of the "ground" covered with grass.
Example 2-1 Creating a Grass Blade
public class Blade extends Path { public final Color SPRING_COLOR = Color.color(random() * 0.5, random() * 0.5
+ 0.5, 0.).darker(); public final Color AUTUMN_COLOR = Color.color(random() * 0.4 + 0.3, random()
* 0.1 + 0.4, random() * 0.2); private final static double width = 3; private double x = RandomUtil.getRandom(170); private double y = RandomUtil.getRandom(20) + 20; private double h = (50 * 1.5 - y / 2) * RandomUtil.getRandom(0.3); public SimpleDoubleProperty phase = new SimpleDoubleProperty(); public Blade() { getElements().add(new MoveTo(0, 0)); final QuadCurveTo curve1; final QuadCurveTo curve2; getElements().add(curve1 = new QuadCurveTo(-10, h, h / 4, h)); getElements().add(curve2 = new QuadCurveTo(-10, h, width, 0)); setFill(AUTUMN_COLOR); //autumn color of blade setStroke(null); getTransforms().addAll(Transform.translate(x, y)); curve1.yProperty().bind(new DoubleBinding() { { super.bind(curve1.xProperty()); } @Override protected double computeValue() { final double xx0 = curve1.xProperty().get(); return Math.sqrt(h * h - xx0 * xx0); } }); //path of top of blade is circle //code to bend blade curve1.controlYProperty().bind(curve1.yProperty().add(-h / 4)); curve2.controlYProperty().bind(curve1.yProperty().add(-h / 4)); curve1.xProperty().bind(new DoubleBinding() { final double rand = RandomUtil.getRandom(PI / 4); { super.bind(phase); } @Override protected double computeValue() { return (h / 4) + ((cos(phase.get() + (x + 400.) * PI / 1600 +
rand) + 1) / 2.) * (-3. / 4) * h; } }); } }
Creating Timeline Animation for Grass Movement
Timeline animation that changes the x-coordinate of the top of the blade is used to create grass movement.
Several algorithms are used to make the movement look natural. For example, the top of each blade is moved in a circle instead of a straight line, and side curve of the blade make the blade look as if it bends under the wind. Random numbers are added to separate each blade movement.
Example 2-2 Grass Animation
class GrassWindAnimation extends Transition { final private Duration animationTime = Duration.seconds(3); final private DoubleProperty phase = new SimpleDoubleProperty(0); final private Timeline tl = new Timeline(Animation.INDEFINITE); public GrassWindAnimation(List<Blade> blades) { setCycleCount(Animation.INDEFINITE); setInterpolator(Interpolator.LINEAR); setCycleDuration(animationTime); for (Blade blade : blades) { blade.phase.bind(phase); } } @Override protected void interpolate(double frac) { phase.set(frac * 2 * PI); } }
Tree
This section explains how the tree shown in Figure 2-4 is created and animated.
Branches
The tree consists of branches, leaves, and flowers. Leaves and flowers are drawn on the top branches of the tree. Each branch generation consists of three branches (one top and two side branches) drawn from a parent branch. You can specify the number of generations in the code using the NUMBER_OF_BRANCH_GENERATIONS
passed in the constructor of TreeGenerator in the Main class. Example 2-3 shows the code in the TreeGenerator class that creates the trunk of the tree (or the root branch) and adds three branches for the following generations.
Example 2-3 Root Branch
private List<Branch> generateBranches(Branch parentBranch, int depth) { List<Branch> branches = new ArrayList<>(); if (parentBranch == null) { // add root branch branches.add(new Branch()); } else { if (parentBranch.length < 10) { return Collections.emptyList(); } branches.add(new Branch(parentBranch, Type.LEFT, depth)); branches.add(new Branch(parentBranch, Type.RIGHT, depth)); branches.add(new Branch(parentBranch, Type.TOP, depth)); } return branches; }
To make the tree look more natural, each child generation branch is grown at an angle to the parent branch, and each child branch is smaller than its parent. The child angle is calculated using random values. Example 2-4 provides a code for creating child branches.
Example 2-4 Child Branches
public Branch(Branch parentBranch, Type type, int depth) { this(); SimpleDoubleProperty locAngle = new SimpleDoubleProperty(0); globalAngle.bind(locAngle.add(parentBranch.globalAngle.get())); double transY = 0; switch (type) { case TOP: transY = parentBranch.length; length = parentBranch.length * 0.8; locAngle.set(getRandom(10)); break; case LEFT: case RIGHT: transY = parentBranch.length - getGaussianRandom(0,
parentBranch.length, parentBranch.length / 10, parentBranch.length / 10); locAngle.set(getGaussianRandom(35, 10) * (Type.LEFT == type ? 1 :
-1)); if ((0 > globalAngle.get() || globalAngle.get() > 180) && depth <
4) { length = parentBranch.length * getGaussianRandom(0.3, 0.1); } else { length = parentBranch.length * 0.6; } break; } setTranslateY(transY); getTransforms().add(new Rotate(locAngle.get(), 0, 0)); globalH = getTranslateY() * cos(PI / 2 - parentBranch.globalAngle.get() *
PI / 180) + parentBranch.globalH; setBranchStyle(depth); addChildToParent(parentBranch, this); }
Leaves and Flowers
Leaves are created on top branches. Because the leaves are created at the same time as the branches of the tree, leaves are scaled to 0 by leaf.setScaleX(0)
and leaf.setScaleY(0)
to hide them before the tree is grown as shown in the Example 2-5. The same trick is used to hide the leaves when they fall. To create a more natural look, leaves have slightly different shades of green. Also, the leaf color changes depending on the location of the leaf; the darker shades are applied to the leaves located below the middle of the tree crown.
Example 2-5 Leaf Shape and Placement
public class Leaf extends Ellipse { public final Color AUTUMN_COLOR; private final int N = 5; private List<Ellipse> petals = new ArrayList<>(2 * N + 1); public Leaf(Branch parentBranch) { super(0, parentBranch.length / 2., 2, parentBranch.length / 2.); setScaleX(0); setScaleY(0); double rand = random() * 0.5 + 0.3; AUTUMN_COLOR = Color.color(random() * 0.1 + 0.8, rand, rand / 2); Color color = new Color(random() * 0.5, random() * 0.5 + 0.5, 0, 1); if (parentBranch.globalH < 400 && random() < 0.8) { //bottom leaf is darker color = color.darker(); } setFill(color); } }
Flowers are created in the Flower class and then added to the top branches of the tree in the TreeGenerator class. You can specify the number of petals in a flower. Petals are ellipses distributed in a circle with some overlapping. Similar to grass and leaves, the flower petals are colored in different shades of pink.
Animating Tree Elements
This section explains techniques employed in the Tree Animation example to animate the tree and season change. Parallel transition is used to start all the animations in the scene as shown in Example 2-6.
Example 2-6 Main Animation
final Transition all = new ParallelTransition(new GrassWindAnimation(grass), treeWindAnimation, new SequentialTransition(branchGrowingAnimation, seasonsAnimation(tree, grass))); all.play();
Growing a Tree
Tree growing animation is run only once, at the beginning of the Tree Animation example. The application starts a sequential transition animation to grow branches one generation after another as shown in Example 2-7. Initially length is set to 0. The root branch size and angle are specified in the TreeGenerator
class. Currently each generation is grown during two seconds.
Example 2-7 Sequential Transition to Start Branch Growing Animation
SequentialTransition branchGrowingAnimation = new SequentialTransition();
The code in Example 2-8 creates the Tree growing animation:
Example 2-8 Branch Growing Animation
private Animation animateBranchGrowing(List<Branch> branchGeneration, int depth, Duration duration) { ParallelTransition sameDepthBranchAnimation = new ParallelTransition(); for (final Branch branch : branchGeneration) { Timeline branchGrowingAnimation = new Timeline(new KeyFrame(duration, new KeyValue(branch.base.endYProperty(), branch.length))); sameDepthBranchAnimation.getChildren().add( new SequentialTransition( PauseTransitionBuilder.create().duration(Duration.ONE).onFinished(new EventHandler<ActionEvent>() { @Override public void handle(ActionEvent t) { branch.base.setStrokeWidth(branch.length / 25); } }).build(), branchGrowingAnimation)); } return sameDepthBranchAnimation; }
Because all the branch lines are calculated and created simultaneously, they could appear on the scene as dots. The code introduces a few tricks to hide the lines before they grow. In Example the code duration.one millisecond
pauses transition for an unnoticeable time. In the Example 2-9, the base.setStrokeWidth(0)
code sets branches width to 0 before the grow animation starts for each generation.
Creating Tree Crown Movement
In parallel with growing a tree, wind animation starts. Tree branches, leaves, and flowers are moving together.
Tree wind animation is similar to grass movement animation, but it is simpler because only the angle of the branches changes. To make the tree movement look natural, the bend angle is different for different branch generations. The higher the generation of the branch (that is the smaller the branch), the more it bends. Example 2-10 provides code for wind animation.
Example 2-10 Wind Animation
private Animation animateTreeWind(List<Branch> branchGeneration, int depth, Duration duration) { ParallelTransition wind = new ParallelTransition(); for (final Branch brunch : branchGeneration) { final Rotate rotation = new Rotate(0); brunch.getTransforms().add(rotation); wind.getChildren().add(TimelineBuilder.create().keyFrames(new KeyFrame(duration, new KeyValue(rotation.angleProperty(), depth * 2))).autoReverse(true).cycleCount(Animation.INDEFINITE).build()); } return wind; }
Animating Season Change
Season-change animation actually starts after the tree has grown, and runû infinitely. The code in Example 2-11 calls all the season animations:
Example 2-11 Starting Season Animation
private Transition seasonsAnimation(final Tree tree, final List<Blade> grass) { Transition spring = animateSpring(tree.leafage, grass); Transition flowers = animateFlowers(tree.flowers); Transition autumn = animateAutumn(tree.leafage, grass); return SequentialTransitionBuilder.create().children(spring, flowers, autumn).cycleCount(Animation.INDEFINITE).build(); } private Transition animateSpring(List<Leaf> leafage, List<Blade> grass) { ParallelTransition springAnimation = new ParallelTransition(); for (final Blade blade : grass) { springAnimation.getChildren().add(FillTransitionBuilder.create().shape(blade). toValue(blade.SPRING_COLOR).duration(GRASS_BECOME_GREEN_DURATION).build()); } for (Leaf leaf : leafage) { springAnimation.getChildren().add(ScaleTransitionBuilder.create().toX(1). toY(1).node(leaf).duration(LEAF_APPEARING_DURATION).build()); } return springAnimation; }
Once all the tree branches are grown, leaves start to appear as directed in Example 2-12.
Example 2-12 Parallel Transition to Start Spring Animation and Show Leaves
private Transition animateSpring(List<Leaf> leafage, List<Blade> grass) { ParallelTransition springAnimation = new ParallelTransition(); for (final Blade blade : grass) { springAnimation.getChildren().add(FillTransitionBuilder.create().shape(blade). toValue(blade.SPRING_COLOR).duration(GRASS_BECOME_GREEN_DURATION).build()); } for (Leaf leaf : leafage) { springAnimation.getChildren().add(ScaleTransitionBuilder.create().toX(1).toY(1). node(leaf).duration(LEAF_APPEARING_DURATION).build()); } return springAnimation; }
When all leaves are visible, flowers start to appear as shown in Example 2-13. The sequential transition is used to show flowers gradually. The delay in flower appearance is set in the sequential transition code of Example 2-13. Flowers appear only in the tree crown.
Example 2-13 Showing Flowers
private Transition animateFlowers(List<Flower> flowers) { ParallelTransition flowersAppearAndFallDown = new ParallelTransition(); for (int i = 0; i < flowers.size(); i++) { final Flower flower = flowers.get(i); for (Ellipse pental : flower.getPetals()) { flowersAppearAndFallDown.getChildren().add(new SequentialTransition( FadeTransitionBuilder.create().delay(FLOWER_APPEARING_ DURATION.divide(3).multiply(i + 1)).duration(FLOWER_APPEARING_ DURATION).node(pental).toValue(1).build(), fakeFallDownAnimation(pental))); } } return flowersAppearAndFallDown; }
Once all the flowers appear on the screen, their petals start to fall. In the code in Example 2-14 the flowers are duplicated and the first set of them is hidden to show it later.
Example 2-14 Duplicating Petals
private Ellipse copyEllipse(Ellipse petalOld, Color color) { Ellipse ellipse = new Ellipse(); ellipse.setRadiusX(petalOld.getRadiusX()); ellipse.setRadiusY(petalOld.getRadiusY()); if (color == null) { ellipse.setFill(petalOld.getFill()); } else { ellipse.setFill(color); } ellipse.setRotate(petalOld.getRotate()); ellipse.setOpacity(0); return ellipse; }
Copied flower petals start to fall to the ground one by one as shown in Example 2-15. The petals disappear after five seconds on the ground. The fall trajectory of a petal is not a straight line, but rather a calculated sine curve, so that petals seem to be whirling as they fall.
Example 2-15 Shedding Flowers
Animation fakeLeafageDown = fakeFallDownEllipseAnimation((Ellipse) leaf, leaf.AUTUMN_COLOR, new HideMethod() { @Override public void hide(Node node) { node.setScaleX(0); node.setScaleY(0); } });
The next season change starts when all the flowers disappear from the scene. The leaves and grass become yellow, and the leaves fall and disappear. The same algorithm used in Example 2-15 to make the flower petals fall is used to show falling leaves. The code in Example 2-16 enables autumn animation.
Example 2-16 Animating Autumn Changes
private Transition animateAutumn(List<Leaf> leafage, List<Blade> grass) { ParallelTransition autumn = new ParallelTransition(); ParallelTransition yellowLeafage = new ParallelTransition(); ParallelTransition dissappearLeafage = new ParallelTransition(); for (final Leaf leaf : leafage) { final FillTransition toYellow = FillTransitionBuilder.create().shape(leaf).toValue(leaf.AUTUMN _COLOR).duration(LEAF_BECOME_YELLOW_DURATION).build(); yellowLeafage.getChildren().add(toYellow); Animation fakeLeafageDown = fakeFallDownEllipseAnimation((Ellipse) leaf, leaf.AUTUMN_COLOR, new HideMethod() { @Override public void hide(Node node) { node.setScaleX(0); node.setScaleY(0); } }); dissappearLeafage.getChildren().add(new SequentialTransition( fakeLeafageDown, FillTransitionBuilder.create().shape(leaf).toValue((Color) leaf.getFill()).duration(Duration.ONE).build())); } ParallelTransition grassBecomeYellowAnimation = new ParallelTransition(); for (final Blade blade : grass) { final FillTransition toYellow = FillTransitionBuilder.create().shape(blade).toValue(blade.AUTUMN_ COLOR).delay(Duration.seconds(1 * random())).duration(GRASS_BECOME_YELLOW_ DURATION).build(); grassBecomeYellowAnimation.getChildren().add(toYellow); } autumn.getChildren().addAll(grassBecomeYellowAnimation, new SequentialTransition(yellowLeafage, dissappearLeafage)); return autumn; }
After all leaves disappear from the ground, spring animation starts by coloring grass in green and showing leaves.