Documentation



JavaFX: Transformations, Animations, and Visual Effects

4 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 4-1 shows the scene with a tree.

Figure 4-1 Tree Animation

Description of Figure 4-1 follows
Description of "Figure 4-1 Tree Animation"

Project and Elements

The Tree Animation project consists of several files. Each element, like leaves, grass blades, and others are created in separate classes. The TreeGenerator class creates a tree from all the elements. The 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.

Figure 4-2 Animation Timeline

Description of Figure 4-2 follows
Description of "Figure 4-2 Animation Timeline"

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 4-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 4-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 4-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 4-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 4-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 4-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 4-4 provides a code for creating child branches.

Example 4-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 4-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 4-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 4-6.

Example 4-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 4-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 4-7 Sequential Transition to Start Branch Growing Animation

SequentialTransition branchGrowingAnimation = new SequentialTransition();

The code in Example 4-8 creates the Tree growing animation:

Example 4-8 Branch Growing Animation

private Animation animateBranchGrowing(List<Branch> branchGeneration) {
 
        ParallelTransition sameDepthBranchAnimation = new ParallelTransition();
        for (final Branch branch : branchGeneration) {
            Timeline branchGrowingAnimation = new Timeline(new KeyFrame(duration,
 new KeyValue(branch.base.endYProperty(), branch.length)));
            PauseTransition pauseTransition = new PauseTransition();
            pauseTransition.setOnFinished(t -> branch.base.setStrokeWidth(branch.length / 25));
            sameDepthBranchAnimation.getChildren().add(
                    new SequentialTransition(
                            pauseTransition,
                            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 4-9, the base.setStrokeWidth(0) code sets branches width to 0 before the grow animation starts for each generation.

Example 4-9 Tree Growing Animation Optimization

private void setBranchStyle(int depth) {
        base.setStroke(Color.color(0.4, 0.1, 0.1, 1));
 
        if (depth < 5) { 
            base.setStrokeLineJoin(StrokeLineJoin.ROUND);
            base.setStrokeLineCap(StrokeLineCap.ROUND);
        }
        base.setStrokeWidth(0); 
    }
}

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 4-10 provides code for wind animation.

Example 4-10 Wind Animation

private Animation animateTreeWind(List<Branch> branchGeneration, int depth) {
    ParallelTransition wind = new ParallelTransition();
    for (final Branch brunch : branchGeneration) {
        final Rotate rotation = new Rotate(0);
        brunch.getTransforms().add(rotation);

        Timeline windTimeline = new Timeline(new KeyFrame(WIND_CYCLE_DURATION, 
new KeyValue(rotation.angleProperty(), depth * 2)));
        windTimeline.setAutoReverse(true);
        windTimeline.setCycleCount(Animation.INDEFINITE);
        wind.getChildren().add(windTimeline);
    }
        return wind;
}

Animating Season Change

Season-change animation actually starts after the tree has grown, and runs infinitely. The code in Example 4-11 calls all the season animations:

Example 4-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);

        SequentialTransition sequentialTransition = new SequentialTransition(spring, flowers, autumn);
        return sequentialTransition;
    }
 
    private Transition animateSpring(List<Leaf> leafage, List<Blade> grass) {
        ParallelTransition springAnimation = new ParallelTransition();
        for (final Blade blade : grass) {
springAnimation.getChildren().add(new FillTransition(GRASS_BECOME_GREEN_DURATION, blade,
        (Color) blade.getFill(), blade.SPRING_COLOR));
        }
        for (Leaf leaf : leafage) {
            ScaleTransition leafageAppear = new ScaleTransition(LEAF_APPEARING_DURATION, leaf);
            leafageAppear.setToX(1);
            leafageAppear.setToY(1);
            springAnimation.getChildren().add(leafageAppear);
        }
        return springAnimation;
    }

Once all the tree branches are grown, leaves start to appear as directed in Example 4-12.

Example 4-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(new FillTransition(GRASS_BECOME_GREEN_DURATION, blade,
                (Color) blade.getFill(), blade.SPRING_COLOR));
    }
    for (Leaf leaf : leafage) {
        ScaleTransition leafageAppear = new ScaleTransition(LEAF_APPEARING_DURATION, leaf);
        leafageAppear.setToX(1);
        leafageAppear.setToY(1);
        springAnimation.getChildren().add(leafageAppear);
        }
    return springAnimation;
    }

When all leaves are visible, flowers start to appear as shown in Example 4-13. The sequential transition is used to show flowers gradually. The delay in flower appearance is set in the sequential transition code of Example 4-13. Flowers appear only in the tree crown.

Example 4-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()) {

                FadeTransition flowerAppear = new FadeTransition(FLOWER_APPEARING_DURATION, petal);
                flowerAppear.setToValue(1);
                flowerAppear.setDelay(FLOWER_APPEARING_DURATION.divide(3).multiply(i + 1));
                flowersAppearAndFallDown.getChildren().add(new SequentialTransition(new SequentialTransition(
                       flowerAppear,
                       fakeFallDownAnimation(petal))));
        }
    }
    return flowersAppearAndFallDown;
}

Once all the flowers appear on the screen, their petals start to fall. In the code in Example 4-14 the flowers are duplicated and the first set of them is hidden to show it later.

Example 4-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 4-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 4-15 Shedding Flowers

Animation fakeLeafageDown = fakeFallDownEllipseAnimation(leaf, leaf.AUTUMN_COLOR,
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 4-15 to make the flower petals fall is used to show falling leaves. The code in Example 4-16 enables autumn animation.

Example 4-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 =
new FillTransition(LEAF_BECOME_YELLOW_DURATION, leaf, null, leaf.AUTUMN_COLOR);

    Animation fakeLeafageDown = fakeFallDownEllipseAnimation(leaf, 
leaf.AUTUMN_COLOR,node -> {
        node.setScaleX(0);
        node.setScaleY(0);
    });
    dissappearLeafage.getChildren().add(fakeLeafageDown);
    }

    ParallelTransition grassBecomeYellowAnimation = new ParallelTransition();
    for (final Blade blade : grass) {
        final FillTransition toYellow =new
FillTransition(GRASS_BECOME_YELLOW_DURATION, blade, (Color) blade.getFill(),
blade.AUTUMN_COLOR);
        toYellow.setDelay(Duration.seconds(1 * random()));
        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.

Application Files

NetBeans Projects 

Close Window

Table of Contents

JavaFX: Transformations, Animations, and Visual Effects

Expand | Collapse