个人账单和砸金蛋前端动画分享

我们在前端开发过程中,有时候实现一些页面效果需要用到CSS3动画,比如活动页面、抽奖页面、个人账单等。我们可以照着设计师给的效果实现,但是我们追求的不止是这样,我们需要实现精致的动画,流畅的体验,还有新颖的交互。下面分享我在做个人账单、砸蛋抽奖过程中的心得。

开发之前需要和产品与设计进行相关内容的确认

为了提高效率、减少开发难度、避免重复劳动,我们需要提前做好准备:

  • 产品和设计并提出的需求也许和实现冲突,作为开发人员需要与其确认可行的方案。
  • 向设计确认设计规范、适配等,或者提出自己认为更好的效果,按照规范进行设计有利于效果和动画实现。
  • 确认效果图是否符合规范,设计师切图是否有明确的标注,图片是否压缩。

Transitions, Transforms和Animation简介

CSS3动画相关的几个属性是:transition, transform, animation ,分别可以理解为过渡属性,变换属性,动画属性。

Transiton

transiton 属性是一个简写属性,用于设置四个过渡属性:

  • transition-property 指定过渡的CSS属性值,比如 transition-property:opacity 就是只指定 opacity 属性参与这个过渡, 还可以使用 all 指定所有的属性值。

  • transition-duration 指定过渡的持续时间

  • transition-delay 延迟过渡时间

  • transition-timing-function 指定过渡动画缓动类型,有ease | linear | ease-in | ease-out | ease-in-out | cubic-bezier()其中,linear线性过度,ease-in由慢到快,ease-out由快到慢,ease-in-out由慢到快在到慢。
    我们通常使用简写 transition: property duration timing-function delay; , 如:

1
.global-wrap .bg {transition: all 1s .6s ease-out;}
Transform

transform 指变换,实现元素的缩放,旋转,移动,斜切,分别对应 scale() rotate() translate() skew() 。需要注意的是,除了斜切,这些变换都可以使用3D的方式,比如移动变换还有以下的方式:

  • translate3d(x,y,z) 定义 3D 转换。
  • translateX(x) 定义转换,只是用 X 轴的值。
  • translateY(y) 定义转换,只是用 Y 轴的值。
  • translateZ(z) 定义 3D 转换,只是用 Z 轴的值。

transform 的使用栗子:

1
2
3
4
.skew { transform: skew(35deg); } // 斜切
.planet0 { transform: scale(1.1); } // 个人账单中用来对星球进行缩放
.sprite_text_2 { transform:rotate3d(0,0,1,-235deg); } // 个人账单中对文字进行旋转
.lotto-waiting { left: 50%; transform: translate3d(-50%,0,0); } // 砸蛋活动中使元素左右居中

如果要定义变换的开始坐标可以使用 transform-origin: x-axis y-axis z-axis; ,对于x轴和y轴都可以使用 left center right 或者%`。

Animation和keyframe

个人账单和砸金蛋页面主要使用的就是 animationkeyframe 制作动画。

animation 属性也是是一个简写属性,用于设置以下动画属性:

  • animation-name 规定需要绑定到选择器的 keyframe 名称。

  • animation-duration 规定完成动画所花费的时间,以秒或毫秒计。

  • animation-timing-function 规定动画的速度曲线。

  • animation-delay 规定在动画开始之前的延迟。

  • animation-iteration-count 规定动画应该播放的次数。

  • animation-direction 规定是否应该轮流反向播放动画。

  • animation-fill-mode none | forwards | backwards | both 在播放之前或之后是否可见。

    animation-duration 属性为 0时不会动画播。

    我们通常使用简写: animation: name duration timing-function delay iteration-count direction;

@keyframes 规则可以用来创建关键帧动画,每一个关键帧就是一个CSS样式,以百分比来定义关键帧,或者通过fromto,等价于 0%100% ,分别对应开始和 结束。

语法: @keyframes animationname {keyframes-selector {css-styles;}}

  • animationname 必需。动画的名称
  • keyframes-selector 必需。定义关键帧节点的位置
  • css-styles 必需。一个或多个合法的 CSS 样式属性。
两者的使用

栗子1:

1
2
3
4
5
6
7
8
9
10
11
.egg.troll { // 砸金蛋里面金蛋摇动的动画
-webkit-transform-origin: bottom center; // 让动画以底部中心为参考开始运动
-webkit-animation: troll 1s ease;
}

@-webkit-keyframes troll { // 让金蛋左右摇摆,使用旋转动画
0%{-webkit-transform:rotate3d(0,0,1,0deg);}
25%{-webkit-transform:rotate3d(0,0,1,-2deg);}
75%{-webkit-transform:rotate3d(0,0,1,2deg);}
100%{-webkit-transform:rotate3d(0,0,1,0deg);}
}

栗子2:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 为了实现更好的砸蛋效果,页面使用了逐帧动画。
/* 首先定义了breakShine动画,动画中对使用的雪碧图背景位置进行变更。
* 只关注背景图开始和结束的位置,并通过动画逐一显示中间位置来实现画面过渡
* 在break-shine动画样式中使用了steps(), 表示两个帧之间变化完成需要的步数,这样一来,如果需要完成一个15帧的动画过渡,我们就总共需要14步 steps(14),
* 以下例子-webkit-animation 使用的forwards已经在前面介绍过,它是animation-fill-mode的一个模式,表示动画结束停留在最后一个帧, 这样就能够保留金蛋破裂之后的状态了。
**/
.break-shine {
-webkit-animation: breakShine 1.8s steps(14) .2s forwards;
}
@-webkit-keyframes breakShine {
0%{ background-position: fn-pxtorem(-10) fn-pxtorem(-921);}
100%{ background-position: fn-pxtorem(-4210) fn-pxtorem(-921);}
}

栗子3:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/*流星动画*/
@-webkit-keyframes meteor {
0%{opacity:.0;-webkit-transform:translate3d(0, 0, 0);}
10%{opacity:.4;}
50%{opacity:.7;-webkit-transform:translate3d(150%, -150%, 0);}
90%{opacity:.4;}
100%{opacity:.0;-webkit-transform:translate3d(300%, -300%, 0);}
}

/*为每一颗流星设置不同的持续时间,延迟时间,并且循环产生,这样在页面中就会有不间断的流星雨了*/
.global-wrap.play .meteor1 {-webkit-animation: meteor 1.8s 0s linear both infinite;}
.global-wrap.play .meteor2 {-webkit-animation: meteor 2s .9s linear both infinite;}
.global-wrap.play .meteor3 {-webkit-animation: meteor 3.8s .5s linear both infinite;}
.global-wrap.play .meteor4 {-webkit-animation: meteor 1.6s .7s linear both infinite;}
.global-wrap.play .meteor5 {-webkit-animation: meteor 3.2s 0s linear both infinite;}
.global-wrap.play .meteor6 {-webkit-animation: meteor 1.2s 0s linear both infinite;}

动画设计和实现思路

动画分组

为了方便控制动画,采用元素嵌套来限制动画,让同一类型的元素能产生较多的效果。如:

1
2
3
4
5
6
7
8
9
10
11
12
<!-- 
把提示文字和砸蛋的破裂动画都放在金蛋元素内部,由金蛋状态控制是否显示
-->
<div class="lotto-sprite egg egg1"
ng-class="{
'troll':eggIndex == 1,
'breaking':breaking == 1,
'broken':breakIndex == 1,
'active':activeIndex == 1}" >
<div class="lotto-sprite hint">里面没东西<br />真的不要砸</div>
<div class="lotto-sprite break-shine"></div>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 用伪元素实现金蛋的阴影和敲金蛋的锤子,而敲金蛋的锤子只在金蛋被激活的状态下才显示,
// 这样一来通过控制金蛋状态,我们可以获得丰富的动画效果
//阴影
.egg::after {
background: fn-bg-url('lottery/','egg_sprite@2x.png') no-repeat;
background-size: fn-pxtorem(4520) fn-pxtorem(1322);
content: "";
display: block;
width: fn-pxtorem(254);
height: fn-pxtorem(330);
background-position: fn-pxtorem(-1098) fn-pxtorem(-262);
position: absolute;
z-index: -1;
bottom: fn-pxtorem(-40);
left: fn-pxtorem(-50);
pointer-events: none;
}
//锤子
.egg.active::before {
background: fn-bg-url('lottery/','egg_sprite@2x.png') no-repeat;
background-size: fn-pxtorem(4520) fn-pxtorem(1322);
content: "";
display: block;
width: fn-pxtorem(130);
height: fn-pxtorem(130);
background-position: fn-pxtorem(-1530) fn-pxtorem(-10);
position: absolute;
right: fn-pxtorem(-90);
top: fn-pxtorem(-72);
opacity: 0;
-webkit-transform-origin: bottom right;
-webkit-animation: hammer .5s ease;
z-index: 101;
}
动画控制

为了更好的展示数据,在动画设计方面由数据逻辑决定页面状态,页面的状态对应页面效果,动画负责串联各个效果。

  • 砸金蛋:

    通过不同的数据状态,控制外层的结构样式,从而影响内部样式。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <div class="lotto-sprite egg egg1"
    ng-class="{
    'troll':eggIndex == 1,
    'breaking':breaking == 1,
    'broken':breakIndex == 1,
    'active':activeIndex == 1}" >
    <div class="lotto-sprite hint">里面没东西<br />真的不要砸</div>
    <div class="lotto-sprite break-shine"></div>
    </div>

    对应的CSS样式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    // 通常状态下的样式
    .egg {
    width: fn-pxtorem(154);
    height: fn-pxtorem(210);
    background-position: fn-pxtorem(-770) fn-pxtorem(-262);
    position: absolute;
    .hint {
    @include mix-px2px(font-size, $fs-smaller);
    color: $white;
    width: fn-pxtorem(158);
    height: fn-pxtorem(118);
    background-position: fn-pxtorem(-1362) fn-pxtorem(-262);
    position: absolute;
    top: fn-pxtorem(-36);
    padding-top: fn-pxtorem(30);
    text-align: center;
    line-height: fn-pxtorem(24);
    opacity: 0;
    }

    .break-shine {
    background-position: fn-pxtorem(-10) fn-pxtorem(-921);
    }
    }

    // 在显示提示文字时,使用css动画显示出提示文字
    .egg.troll .hint {
    -webkit-transform-origin: bottom center;
    -webkit-animation: scale-bounce 3.5s .5s ease;
    }
    // 显示提示文字的同时,为了视觉效果,让金蛋本身也动起来
    .egg.troll {
    -webkit-transform-origin: bottom center;
    -webkit-animation: troll 1s ease;
    }
    // 金蛋破坏时,显示破坏的动画了。
    .egg.broken {
    width: fn-pxtorem(154);
    height: fn-pxtorem(210);
    background-position: fn-pxtorem(-934) fn-pxtorem(-262);
    position: absolute;
    .break-shine {
    width: fn-pxtorem(300);
    height: fn-pxtorem(400);
    position: absolute;
    left: fn-pxtorem(-74);
    bottom: fn-pxtorem(16);
    -webkit-animation: breakShine 1.8s steps(14) .2s forwards;
    }
    }
    .egg.break {
    -webkit-animation: break .2s steps(1) forwards;
    }
    .egg.breaking {
    -webkit-animation: breaking .3s steps(1) forwards;
    }
    // 除了破坏的瞬间,都是不需要显示砸蛋锤的
    .egg.broken::after, .egg.breaking::after, .egg.troll::after {
    display: none;
    }
  • 个人账单:

    个人账单采用滑动显示每一页的统计结果的设计,首先针对每一页都有统一的结构,当滑动到对应页的时候,就会在这个结构的外层元素上添加 play 样式,再由这个样式控制内部动画。

    以其中一个页面作为例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    <!-- 
    每一个页面都有一个item的样式,当滑动到这一页面时,就会给这个样式追加play样式,样式的变更可以产生过渡的动画,因此,我们要在play状态下实现想要的效果。
    -->
    <div class="item item-2" data-page="2">
    <div class="box">
    <!-- mod_container 通过 transform 属性将它水平垂直居中,内部的元素都会以中心作为参考进行动画,为了层次更加清晰,我们还分出了text-stage 和 actor-stage两个部分,分别用来展示文字动画和配图动画
    -->
    <div class="mod_container center">
    <div class="text-stage">
    <p class="text text_1 text_big title">你的努力,我们看得见</p>
    <p class="text text_2"></p>
    <p class="text text_2">但还没获得学分呢</p>
    <p class="text text_2">还要继续加油啊!</p>
    </div>
    <div class="actor-stage">
    <div class="actor actor1"></div>
    <div class="sprite_text_1 actor"></div>
    <div class="sprite_text_2 actor"></div>
    <div class="actor actor2"></div>
    <div class="sprite_text_3 actor"></div>
    <div class="sprite_text_4 actor"></div>
    <div class="sprite_text_5 actor"></div>
    </div>
    </div>
    </div>
    </div>

    对应的样式:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    // 我们会对mod_container 进行居中处理
    .mod_container {
    position: absolute;
    left: 50%;
    top: 50%;
    margin-top: -36px;
    -webkit-transform: translate3d(-50%, -50%, 0);
    -webkit-backface-visibility:hidden;z-index: 2;
    }

    // 然后统一为文字和配图添加动画,不同类型的文字和配图使用不同的延迟时间
    .item .text, .item .actor {
    background-repeat: no-repeat;
    -webkit-transform: translate3d(0, 200px, 0);opacity: 0;
    }
    .item.play .text, .item.play .actor {opacity: 1;-webkit-transform: translate3d(0, 0, 0);}
    .item.play .text_1 {-webkit-transition: all 1s ease-out;white-space: nowrap;}
    .item.play .text_2 {-webkit-transition: all 1s .3s ease-out;white-space: nowrap;}
    .item.play .text_3 {-webkit-transition: all 1s .8s ease-out;}
    .item.play .actor {-webkit-transition: all 1s .6s ease-out;}
    // 以上的样式已经使得元素在当前页时会产生过渡动画

    // 而下面的样式则是针对每一页的元素设置变化的样式,在没有paly时只负责做布局,在play状态下则修改了需要变化的属性,以下示例主要针对 opacity 和 transform 进行了变化。
    .item-2 .sprite_text_1 {position: absolute;right: 0;top: 0;width: 56px;height: 60px;background-position: -635px -240px;opacity: 0;}
    .item-2 .sprite_text_2 {position: absolute;right: 0;top: 0;width: 265px;height: 244px;background-position: -562px -300px;opacity: 0;}
    .item-2 .sprite_text_3 {position: absolute;left: 0;bottom: 0;width: 56px;height: 60px;background-position: -635px -240px;opacity: 0;}
    .item-2 .sprite_text_4 {position: absolute;right: 80px; bottom: -50px;width: 56px;height: 60px;background-position: -635px -240px;opacity: 0;}
    .item-2 .sprite_text_5 {position: absolute;right: 150px; bottom: -90px;width: 56px;height: 60px;background-position: -635px -240px;opacity: 0;}
    .item-2.play .sprite_text_1 {opacity: 1;}
    .item-2.play .sprite_text_2 {opacity: 1;}
    .item-2.play .sprite_text_3 {opacity: 1;}
    .item-2.play .sprite_text_4 {opacity: 1;transform: rotate(45deg);}
    .item-2.play .sprite_text_5 {opacity: 1; transform: scale(.5) rotate(80deg);}

    另外需要提到的是背景动画,由于背景和内容是区分开来的,所以在设计动画时将背景独立出来,让它置于内容的下层,为了控制不同页有不同的背景,我们从滚动页获取到了当前页数,并为背景添加了对应的样式:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    <!-- 这是加载页的背景样式,可以看到背景的最外层元素添加了一个play0样式,我们通过修改这个样式,就可以实现背景的不同展示。 -->
    <div class="wrap global-wrap play play0">
    <audio class="opacity" loop="loop" id="music" src="https://res.exexm.com/bill/common/bill_bg_music.mp3"></audio>
    <div class="bg bg2"></div>
    <div class="bg bg3"></div>

    <div class="sprite star star1"></div>
    <div class="sprite star star2"></div>
    <div class="sprite star star3"></div>
    <div class="sprite star star4"></div>
    <div class="sprite planet planet1"></div>
    <div class="sprite planet planet2"></div>
    <div class="sprite planet planet3"></div>
    <div class="sprite planet planet4"></div>
    <div class="sprite planet planet0"></div>
    <div class="sprite meteor meteor1"></div>
    <div class="sprite meteor meteor2"></div>
    <div class="sprite meteor meteor3"></div>
    <div class="sprite meteor meteor4"></div>
    <div class="sprite meteor meteor5"></div>
    <div class="sprite meteor meteor6"></div>
    </div>
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // 我们会预先写好每一页的背景页样式,然后通过transition实现背景元素的移动。

    /*背景动画*/
    .global-wrap .sprite {-webkit-transition: all 2s ease;}
    .global-wrap .bg {-webkit-transition: all 1s .6s ease-out;}

    .global-wrap.play0 .planet1 {bottom:800px;}
    .global-wrap.play0 .planet0 {bottom: 1200px;}
    .global-wrap.play0 .planet2 {bottom: 800px;}
    .global-wrap.play0 .planet3 {bottom: -200px;}
    .global-wrap.play0 .planet4 {bottom: -300px;}

    .global-wrap.play2 .planet0 {bottom: -400px;}
    .global-wrap.play2 .planet1 {bottom: 800px;}
    .global-wrap.play2 .planet2 {bottom: 650px;}
    .global-wrap.play2 .planet3 {bottom: 300px;}
    .global-wrap.play2 .planet4 {bottom: 50px;}

    ……

一些技巧
  • 通过缩放和位置变换使背景元素产生滚动视差。
  • 同一个动画如果设置的持续时间不一样,可以产生不同的效果:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // 这个动画只是对元素位置进行了微小的变换
    @-webkit-keyframes float {
    0%{opacity:1;-webkit-transform:translate3d(0, 0, 0);}
    20%{opacity:1;-webkit-transform:translate3d(-2%, -2%, 0);}
    40%{opacity:1;-webkit-transform:translate3d(1%, 1%, 0);}
    60%{opacity:1;-webkit-transform:translate3d(-1%, .5%, 0);}
    80%{opacity:1;-webkit-transform:translate3d(1%, -2%, 0);}
    100%{opacity:1;-webkit-transform:translate3d(0, 0, 0);}
    }

    // 设置了30秒的时长,循环动画,用来体现星球漂浮的状态。
    .global-wrap.play .planet0 {-webkit-animation: float 30s 0s linear both infinite;}

    // 把时间改为了0.2秒,用来体现火箭发射时的震动。
    .item-0.launch .actor2 {-webkit-animation: float .2s 0s linear both infinite;}
  • 通过设置 animation-timing-function 来提升动画的质感,另外,动画时间不宜过长。

  • 通过设置不同 animation-delay 动画延迟时间来达成连贯效果,相同的元素之间采取相同的动画时,设置不同的延迟和持续时间会让效果更自然。

提高性能

浏览器渲染页面时,会将解析到的DOM分为多个Layout,然后对每个Layout生成图像和位置,再讲这些图层作为纹理传到GPU,最后复合多个图层到屏幕上显示。

为了提高动画性能,我们能够从以下方面着手:

  • 不改变元素位置和尺寸等属性,这些属性会影响到Layout,触发浏览器的重布局。

  • 创建Layout,设置 opacity 或者 3D透视变换 perspective transform 等CSS属性 。

  • 减少使用能够触发里游览器重新绘制Layout的样式。

  • 使用3D动画,让手机能够启动GPU加速,使动画更加流畅:

    使用 opacity translate rotate scale 会使浏览器更加容易处理动画,相应的,采取translate3d(x,y,z) translateZ(z) 这样的形式来处理效果更佳。

关于提升动画性能的介绍:high-performance-animations

加速浏览器渲染的介绍:Accelerated Rendering in Chrome