素材:
期望效果:http://v.youku.com/v_show/id_XMjY2NTc1MzYw.html
一开始需要制作这个动画的时候,其实我是拒绝的,因为单凭这几个素材,即便我对动画了如指掌,也是无从下手的。后来有一天,我见到了 wow 军团再临的 CG 动画,脑海中突然灵光一闪,便出现了这样的构思:
“ 穿越重重的云层,伴随着白天到黑夜的转化,logo 从遥远的地方出现,随后往屏幕方向快速移动,在 logo 快要充满整个屏幕的时候星光一闪,又消逝在远方...”
于是我开始了开场动画的坎坷历程。
云层制作:
云层的制作耗时比较久,期间参阅了网上一些使用 CSS 模拟云层的博文及示例,下面列出两个比较好的示例:
移动的云:https://codepen.io/montanaflynn/pen/orxwK
3D 云:https://www.clicktorelease.com/blog/how-to-make-clouds-with-css-3d
实际上使用单图片来模拟云层会有一些体验问题,比如,当我们试图穿过一片图片生成的云时,会感觉一下就没了,对用户而言会产生一定的视觉落差,而真实的云层是一片区域,所以一片云我使用了 5 张图片转换方向来生成,这样在三维的场景下我们的云就有了充实感。
下面引用一段 Jaume Sanchez Elias 写的 3D 云的生成代码:
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
|
function createCloud() { vardiv = document.createElement('div'); div.className = 'cloudBase'; // 随机定位当前云团位置 var t = 'translateX( ' + random_x + 'px ) ' + 'translateY( ' + random_y + 'px ) ' + 'translateZ( ' + random_z + 'px )'; div.style.transform = t; world.appendChild(div); for (varj = 0; j < 5 + Math.round(Math.random() * 10); j++) { varcloud = document.createElement('div'); cloud.className = 'cloudLayer'; // 随机产生云层的'translateX/translateY/translateZ/rotateZ/scale // CSS值,这里是云团充实感的关键实现 cloud.data = { x: random_x, y: random_y, z: random_z, a: random_a, s: random_s }; var t = 'translateX( ' + random_x + 'px ) ' + 'translateY( ' + random_y + 'px ) ' + 'translateZ( ' + random_z + 'px ) ' + 'rotateZ( ' + random_a + 'deg ) ' + 'scale( ' + random_s + ' )'; cloud.style.transform = t; div.appendChild(cloud); layers.push(cloud); } return div; } |
通过上面的方法去输出云层,我们便拥有了质感较强的云。而为了让动画的加载更快一些,我将生成的云层以固定 DOM 的形式写在页面中,让前期的 JS 消耗尽量减小,并且图片的加载会在页面加载之初进行。在有了云层 DOM 后,便可以开始着手穿越云层效果的制作了,穿越过程里还夹带了从白天到黑夜的背景色变化效果,这个穿越的动作我使用 translateZ 属性来实现,让屏幕随着时间往前推进,完成穿越云层的效果,代码实现如下(这里仅展示 webkit 版本代码):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
@-webkit-keyframes angular { 0% { -webkit-transform: translateZ(300px); opacity: 1; } 100% { -webkit-transform: translateZ(570px); opacity: 1; } } @-webkit-keyframes dayToNight { 0% { background-color: #007fd5; opacity: 1; } 100% { background-color: #000; opacity: 1; } } |
星光及 logo:
logo 及星光的呈现过程是一个放大及放更大/缩小的过程,通过改变它的 scale 可以实现,实现如下:
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
|
/* 星光场景 */ @-webkit-keyframes starsense { 0% { -webkit-transform: scale(1); opacity: 0; } 30% { -webkit-transform: scale(25); opacity: 1; } 90% { -webkit-transform: scale(0); opacity: 1; } 100% { -webkit-transform: scale(0); opacity: 0; } } /* logo场景 */ @-webkit-keyframes logosense { 0% { -webkit-transform: scale(0); transform: scale(0); opacity: 1; } 20% { -webkit-transform: scale(1); transform: scale(1); } 85% { -webkit-transform: scale(1); transform: scale(1); opacity: 1; -webkit-filter: blur(0); } 100% { -webkit-transform: scale(100); transform: scale(100); opacity: 0; -webkit-filter: blur(50px); } } |
性能优化:
由于在测试中发现动画的性能是一个比较大的问题,在一些配置比较低的机器会有很多掉帧,卡顿的现象,因此需要进行性能方面的优化。
我使用 chrome 里面的工具 Timeline 进行了动画执行性能检查,从中发现,在动画执行周期内,渲染及重绘耗费的资源比较多,并且期间 JS 也占用了一些资源,于是我首先回头查看了我的动画加载函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
function loadOpenSenseAnimation() { isStart = true; openSense.className += ' begin'; world.className += ' begin'; logoSense.className += ' begin'; setTimeout(function() { viewPort.className = ' begin'; }, 4000); loadingAudio === 0 && Audio.play(); openSense.addEventListener("webkitAnimationEnd", function(){ // 动画结束时事件 console.log('动画执行结束啦!'); if (openSense) { // 执行完就抹除 openSense.parentNode.removeChild(openSense); } }, false); } |
于是它变成了这样纸:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
function loadOpenSenseAnimation() { Settings.isStart = true; $container.className += ' animated'; $mask.addEventListener("webkitAnimationStart", function() { // 动画开始时候播放 if (Settings.isVoice && Settings.loadingAudio === 0) { Settings.AudioPlayer.play(); } }, false); $container.addEventListener("webkitAnimationEnd", function(animation) { // 动画结束时事件 if (animation.animationName === Settings.EndingAnimatedName) { OpenSense.pass(); } }, false); } |
这里我把 setTimeout 函数移除掉,使用 animation-delay 来接替 setTimeout 的位置,多个 DOM 操作被合并成了一个,把 JS 的消耗影响尽量降到最低。动画的声音播放使用 webkitAnimationStart 事件来监听,使音乐与动画的进行同步(这里需要注意 webkitAnimationEnd/webkitAnimationStart 的使用,每一个子节点的动画开始结束都会触发这个事件,需要判断一下 animationName 确定是否是自己需要的动画事件),接下来我再次使用 Timeline 进行检查:
可以看到,在动画执行期间,从云层开始帧数就一直不高,结合此前的渲染重绘时间占用率过高,初步定为 CSS 属性使用不当。于是我查阅了 CSS 动画中所使用的属性:
根据 Timeline 掉帧的时间片段,结合这个时间段内产生作用的 CSS 属性,将一些可能影响性能的属性标记了出来,并进行了排除试验,我们发现,blur 是损耗性能的主要因素,于是我对 CSS 做了一次排除优化,将 blur,重复或不必要的属性进行剔除。
优化结果:
多次优化后,通过 Timeline 得到了下面的结果,我们可以看到,除了页面加载之初的一些掉帧,后面基本平稳在 60 帧,期间的无帧数是因为动画固定在 logo 处停了 3s 左右。
在线示例: http://huangxingbang.github.io/openSense/cloud.html
总结:
1. 动画尽量使用 opacity/translate/rotate /scale 这些可以让 GPU 分担工作的属性。
2. 摈弃 setTimeout 在动画中的控制,动画播放时机的控制使用 animation-delay 来实现,如果需要精准控制,使用 RequestAnimationFrame 对动画进行更新。
3. 使用 webkitAnimationStart/webkitAnimationEnd 对动画并行的任务进行开始/结束控制。
参考资料:
前端性能优化(CSS 动画篇):https://segmentfault.com/a/1190000000490328
高性能 CSS3 动画:https://www.qianduan.net/high-performance-css3-animations/
那个谁 2016 年 1 月 27 日
帅,学到了动画优化的方法
林贞恩 2016 年 1 月 25 日
顶也~~~
韩懿莹 2016 年 1 月 23 日
既然看了, 顶一个吧,好文章
xiaowie 2016 年 1 月 13 日
帅~[嘻嘻]
流氓兔ジ 2016 年 1 月 12 日
赞,太帅了
liping 2016 年 1 月 8 日
漂亮
实例CSS3开场动作的制作与优化 | 匠仓科技 2016 年 1 月 6 日
[…] 转自 AlloyTeam:http://www.alloyteam.com/2016/01/css3995527/ […]