4 树动画示例
本章介绍了树动画示例的详细信息。您将学习如何创建和动画化场景上的所有元素。
图4-1显示了带有树的场景。
项目和元素
树动画项目由多个文件组成。每个元素,如叶子、草叶等都在单独的类中创建。TreeGenerator
类从所有元素中创建一棵树。Animator
类包含了除了草动画之外的所有动画,草动画位于GrassWindAnimation
类中。
示例中的场景包含以下元素:
-
带有树枝、叶子和花朵的树
-
草
每个元素都以自己的方式进行动画。一些动画并行运行,而其他动画按顺序运行。树生长动画只运行一次,而季节变化动画设置为无限运行。
季节变化动画包括以下部分:
-
树上出现叶子和花朵
-
花瓣飘落并消失
-
叶子和草改变颜色
-
叶子落到地面并消失
草地
本节描述了如何创建和动画化草地。
创建草地
在树动画示例中,草地如图4-3所示,由单独的草叶组成,每个草叶都是使用Path
创建并添加到列表中。然后对每个草叶进行弯曲和着色。使用算法随机化草叶的高度、曲线和颜色,并将草叶分布在“地面”上。您可以指定草叶的数量和覆盖草地的“地面”的大小。
示例4-1 创建草叶
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); //草叶的秋天颜色 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); } }); //草叶顶部的路径是圆形 //弯曲草叶的代码 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; } }); } }
创建草地移动的时间轴动画
使用改变草叶顶部x坐标的时间轴动画来创建草地移动。
使用多种算法使移动看起来更自然。例如,每片草叶的顶部移动的是一个圆而不是直线,草叶的侧曲线使其看起来像是在风中弯曲。随机数被添加以分隔每片草叶的移动。
示例 4-2 草地动画
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); } }
树
本节将解释如何创建和动画显示在图4-4中展示的树。
分支
树由分支、叶子和花朵组成。叶子和花朵绘制在树的顶部分支上。每一代分支由三个分支(一个顶部分支和两个侧面分支)从父分支绘制而来。您可以在代码中使用传递给TreeGenerator构造函数的NUMBER_OF_BRANCH_GENERATIONS来指定代数的数量。示例4-3显示了TreeGenerator类中创建树干(或根分支)并为后续代添加三个分支的代码。
示例4-3 根分支
private List<Branch> generateBranches(Branch parentBranch, int depth) { List<Branch> branches = new ArrayList<>(); if (parentBranch == null) { // 添加根分支 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; }
为了使树看起来更自然,每个子代分支都以与父分支成角度生长,并且每个子分支都比其父分支小。子角度使用随机值计算。示例4-4提供了创建子分支的代码。
示例4-4 子分支
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); }
叶子和花朵
叶子是在顶部的分支上创建的。因为叶子是在树的分支同时创建的,所以在树长大之前,通过leaf.setScaleX(0)
和leaf.setScaleY(0)
将叶子的缩放设置为0,以隐藏它们,如示例4-5所示。当叶子掉落时,也使用同样的技巧来隐藏叶子。为了营造更自然的外观,叶子具有稍微不同的绿色阴影。另外,叶子的颜色会根据叶子的位置而改变;较暗的阴影应用于位于树冠中部以下的叶子。
示例4-5 叶子的形状和位置
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) { //底部的叶子较暗 color = color.darker(); } setFill(color); } }
花朵是在Flower类中创建的,然后添加到TreeGenerator类中的顶部分支上。您可以指定花朵中花瓣的数量。花瓣是分布在一个圆圈中并有一些重叠的椭圆形。与草和叶子类似,花瓣的颜色是不同的粉色阴影。
动画树元素
本节介绍了在Tree Animation示例中用于动画树和季节变化的技术。并行转换被用来启动场景中的所有动画,如示例4-6所示。
示例4-6 主要动画
final Transition all = new ParallelTransition(new GrassWindAnimation(grass), treeWindAnimation, new SequentialTransition(branchGrowingAnimation, seasonsAnimation(tree, grass))); all.play();
树的生长
树的生长动画只在Tree Animation示例的开始时运行一次。应用程序启动一个顺序过渡动画,逐代逐代地生长分支,如示例4-7所示。初始长度设置为0。根分支的大小和角度在TreeGenerator
类中指定。目前每一代的生长时间为两秒。
示例4-8中的代码创建了树的生长动画:
示例4-8 分支生长动画
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; }
因为所有的分支线都是同时计算和创建的,它们可能会以点的形式出现在场景中。代码引入了一些技巧,在它们生长之前隐藏这些线条。在示例中,代码duration.one millisecond
暂停过渡,时间不可察觉。在示例4-9中,base.setStrokeWidth(0)
代码在每一代的生长动画开始之前将分支的宽度设置为0。
创建树冠运动
在树木生长的同时,开始进行风动画。树枝、叶子和花朵一起移动。
树木的风动画类似于草的运动动画,但更简单,因为只有树枝的角度会改变。为了使树木的运动看起来自然,不同的分支代数有不同的弯曲角度。分支的代数越高(即分支越小),弯曲角度越大。示例4-10提供了风动画的代码。
示例4-10 风动画
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; }
动画化季节变化
实际上,季节变化的动画是在树木生长后开始的,并且无限循环运行。在示例4-11中的代码调用了所有季节动画:
示例4-11 开始季节动画
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; }
一旦所有树枝都长出来,叶子开始按照示例4-12中的指示出现。
示例4-12 并行转换开始春季动画并显示叶子
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; }
当所有叶子都可见时,花朵开始出现,如示例4-13所示。顺序转换用于逐渐显示花朵。花朵出现的延迟在示例4-13的顺序转换代码中设置。花朵只出现在树冠上。
示例4-13 显示花朵
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; }
一旦所有花朵出现在屏幕上,它们的花瓣开始掉落。在示例4-14中的代码中,花朵被复制并隐藏,以便稍后显示。
示例4-14 复制花瓣
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; }
复制的花瓣开始一个接一个地落到地面上,如示例4-15所示。花瓣在地面上停留五秒后消失。花瓣的下落轨迹不是直线,而是一个计算出的正弦曲线,使得花瓣在下落时看起来像是旋转。
示例4-15 落花
Animation fakeLeafageDown = fakeFallDownEllipseAnimation(leaf, leaf.AUTUMN_COLOR, node -> { node.setScaleX(0); node.setScaleY(0); });
当所有花朵从场景中消失时,下一个季节的变化开始。树叶和草变黄,树叶落下并消失。在示例4-15中使用的算法用于显示落叶。在示例4-16中的代码启用了秋季动画。
示例 4-16 动画化秋季变化
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; }
当所有叶子从地面上消失后,春季动画开始,将草地变为绿色并显示叶子。