主流动画实现方式总结
文章已同步至个人Blog:Benjamin - 专注前端开发和用户体验
相关概念:绘制频率、屏幕刷新频率、硬件加速、60fps
绘制频率:
页面上每一帧变化都是系统绘制出来的(GPU或者CPU)【参考浏览器渲染原理】。但这种绘制又和PC游戏的绘制不同,它的最高绘制频率受限于显示器的刷新频率(而非显卡),所以大多数情况下最高的绘制频率只能是每秒60帧(frame per second,以下用fps简称),对应于显示器的60Hz。60fps是一个最理想的状态,在日常对页面性能的测试中,60fps也是一个重要的指标,the closer the better。
刷新频率: 图像在屏幕上更新的速度,也即屏幕上的图像每秒钟出现的次数,它的单位是赫兹(Hz)。刷新频率越高,屏幕上图像闪烁感就越小,稳定性也就越高,换言之对视力的保护也越好。一般人的眼睛、不容易察觉75Hz以上刷新频率带来的闪烁感,因此最好能将您显示卡刷新频率调到75Hz以上。要注意的是,并不是所有的显示卡都能够在最大分辨率下达到70Hz以上的刷新频率(这个性能取决于显示卡上RAMDAC的速度),而且显示器也可能因为带宽不够而不能达到要求。影响刷新率最主要的还是显示器的带宽。
显示器带宽是显示器视频放大器通频带宽度的简称,指电子枪每秒钟在屏幕上扫过的最大总像素数,以MHz(兆赫兹)为单位。 带宽的值越大,显示器性能越好。
硬件加速: 硬件有三个处理器,CPU、GPU和APU(不是加速处理器是声音处理器)。他们通过PCI/AGP/PCIE总线交换数据。今天,GPU已经不再局限于3D图形处理了,GPU通用计算技术发展已经引起业界不少的关注,事实也证明在浮点运算、并行计算等部分计算方面,GPU可以提供数十倍乃至于上百倍于CPU的性能。
60Hz和60fps是什么关系
没有任何关系。fps代表GPU渲染画面的频率,Hz代表显示器刷新屏幕的频率。一幅静态图片,你可以说这副图片的fps是0帧/秒,但绝对不能说此时屏幕的刷新率是0Hz,也就是说刷新率不随图像内容的变化而变化。游戏也好浏览器也好,我们谈到掉帧,是指GPU渲染画面频率降低。比如跌落到30fps甚至20fps,但因为视觉暂留原理,我们看到的画面仍然是运动和连贯的。
PS: 以下示例在Chrome环境中运行
一、CSS动画
1.1 Transitions动画
实例:
<style type="text/css"> .animate { width: 200px; height: 200px; margin: 0 auto; border-radius: 50%; background-color: #f00; line-height: 200px; border-radius: 50%; text-align: center; color: #fff; font-size: 20px; } .animate-transition { transition : transform 2s linear; -moz-transition : -moz-transform 2s linear; -webkit-transition : -webkit-transform 2s linear; -o-transition : -o-transform 2s linear; } .animate-transition:hover { cursor: pointer; transform : rotate(360deg); -moz-transform : rotate(360deg); -webkit-transform : rotate(360deg); -o-transform : rotate(360deg); } </style> <div class="animate animate-transition">Transition Animation</div>
1.2 Keyframes animation
Keyframes animation通过定义多个关键帧以及定义每个关键帧中的元素的属性值来实现更为复杂的动画效果。 实例:
<style type="text/css"> .animate-keyframes { -webkit-animation: frames 2s linear infinite; } .animate-keyframes:hover { cursor: pointer; -webkit-animation: none; } @-webkit-keyframes frames { 0% { background-color: #f00; -webkit-transform: rotate(0deg); } 100% { background-color: #f00; -webkit-transform: rotate(360deg); } } </style> <div class="animate animate-keyframes">keyframes animation</div>
1.3 CSS动画优缺点
优点: 简单、高效 声明式的 不依赖与主线程,采用硬件加速(GPU) 简单的控制keyframe animation 播放和暂停 缺点: 不能动态的修改或定义动画内容 不同的动画无法实现同步 多个动画彼此无法堆叠 另: 1) CSS3 transition强制硬件加速会加大GPU消耗,高负荷情形下将导致运行不流畅。这种情况在移动设备上尤为明显。(特殊情况下,比如当数据在浏览器主线程和排版线程之间传递产生的瓶颈也会导致不流畅)。某些CSS属性,比如transform和opacity,则不受这些瓶颈影响。Adobe在这里精心总结了这些问题。详细请戳 transition的兼容性问题是个诟病,IE10+及现代浏览器,使用起来会造成很多不便。 由于transition并不是由JavaScript原生控制(而仅仅是由JavaScript触发),浏览器无法获知如何与控制这些transition的JavaScript代码同步地优化他们。 2) keyframes animation 的动画曲线会应用到所有变化的属性上,而且手写比较复杂的动画,写起来就是噩梦。
二、SVG动画
2.1 实例
<div class="animate-svg"> <svg id="svgAnimation" ns="http://www.w3.org/2000/svg" version="1.1" width="200" height="200"> <g transform="translate(100,100)"> <g> <rect width="200" height="200" rx="100" ry="100" fill="red" transform="translate(-100,-100)"></rect> <text x="-60" y="-0" font-size="20" fill="white" >SVG Animation</text> <!-- Add ease-in-out and infinite iterations to this animation and the code --> <animateTransform attributeName="transform" attributeType="xml" type="rotate" from="0" to="360" dur="3s" repeatCount="indefinite">SVG Animation</animateTransform> </g> </g> </svg> </div>
2.2 SVG动画优缺点
优点:
1) 矢量图形,不受像素影响——SVG的这个特性使得它在不同的平台或者媒体下表现良好,无论屏幕分辨率如何
2) SVG对动画的支持较好,其DOM结构可以被其特定语法或者Javascript控制,从而轻松的实现动画
3) Javascript可以完全控制SVG Dom 元素
4) SVG的结构是XML,其可访问性(盲文、声音朗读等)、可操作性、可编程性、可被CSS样式化完胜Canvas。另外,其支持 ARIA 属性,使其如虎添翼。
缺点:
1) DOM比正常的图形慢,而且如果其结点多而杂,就更慢。
2) SVG 画点报表什么的,还行;在网页游戏前,就束手无策了;当然可以结合 Canvas + SVG实现。
3) 不能动态的修改动画内容
4) 不能与HTML内容集成
5) 整个SVG作为一个动画
6) 浏览器兼容性问题,IE8-以及Android 2.3默认浏览器是不支持SVG
三、Javascript动画
3.1 jQuery动画
jQuery动画使用setInterval实现:
// 用例 $("#div").animate({}); // 源码 jQuery.fx.timer = function( timer ) { if ( timer() && jQuery.timers.push( timer ) && !timerId ) { timerId = setInterval( jQuery.fx.tick, jQuery.fx.interval ); } }; jQuery.fx.interval = 13;
How to determine the best “framerate” (setInterval delay) to use in a JavaScript animation loop?
优点: 易用,低效,兼容好;
缺点:
1) setInterval多个间隔可能会被跳过
2) setInterval多个间隔可能比预期小
3) 不同浏览器的精度量级不同:
4) jQuery 无法解决频繁触发 Layout 导致的抽动。
3.2 requestAnimationFrame
实例:
<style type="text/css"> .animate-RAF { } .animate-input { margin-top: 10px; text-align: center; } </style> <div id="animate-RAF" class="animate animate-RAF">RAF Animation</div> <div class="animate-input"><input type="button" id="btn_start" value="Start" style="width:100px;height:30px"></div> <script type="text/javascript"> var animate_raf = document.getElementById("animate-RAF"), btn_start = document.getElementById("btn_start"), frameid = null; function frame(time) { animate_raf.style['-webkit-transform'] = 'rotate('+ Math.cos(time/1000)*360 +'deg)'; frameid = requestAnimationFrame(frame); } // bind Event btn_start.addEventListener("click", function(event) { var val = this.value; if(val === "Start") { frameid = requestAnimationFrame(frame); this.value = "Pause"; }else { this.value = "Start"; cancelAnimationFrame(frameid); } }, false); </script>
优点:
1) 在每次浏览器更新页面时,能获取通知并执行应用。 简单理解为,RAF能在每个16.7ms间执行一次咱们的函数,不多不少。
2) 最小化的消耗资源,RAF在页面被切换或浏览器最小化时,会暂停执行,等页面再次关注时,继续执行动画。
3) 相比 CSS 动画有更好的掌控,能合理降低CPU的使用。
缺点:
1) 无法控制执行时间,执行时间由系统根据屏幕刷新时间决定
2) 浏览器兼容性问题,IE10+及现代浏览器,低版本浏览器建议降级处理,使用setInterval或setTimeout
3.3 Canvas
优点:
1) 画2D图形时,页面渲染性能比较高
2) 页面渲染性能受图形复杂度影响小
3) 渲染性能只受图形的分辨率的影响
4) 画出来的图形可以直接保存为 .png 或者 .jpg的图形
5) 最适合于画光栅图像(如游戏和不规则几何图形等),编辑图片还有其他基于像素的图形操作。
缺点:
1) 整个就是一张图,无论你往上画什么东西——没有DOM 结点可供操作
2) 没有实现动画的API,你必须依靠定时器和其他事件来更新Canvas
3) 对文本的渲染支持是比较差
4) 对要求有高可访问性(盲文、声音朗读等)页面,比较困难
5) 对交互要求高的(比如TIBCO的很多产品)的界面,不建议使用Canvas
3.4 WebGL
WebGL是一种3D绘图标准,这种绘图技术标准允许把JavaScript和OpenGL ES 2.0结合在一起,通过增加OpenGL ES 2.0的一个JavaScript绑定,WebGL可以为HTML5 Canvas提供硬件3D加速渲染,这样Web开发人员就可以借助系统显卡来在浏览器里更流畅地展示3D场景和模型了,还能创建复杂的导航和数据视觉化。显然,WebGL技术标准免去了开发网页专用渲染插件的麻烦,可被用于创建具有复杂3D结构的网站页面,甚至可以用来设计3D网页游戏等等。
浏览器支持:
Internet Explorer 11+
Google Chrome 9+
Firefox 4+
Opera 12+
Safari 5.1+
3.5 Problems
1) 不同的api有不同的模型动画
2) 不必要的维护来支持多种方式实现同样的事情
3) Web开发人员需要学习多种实现技术
4) Javascript不容易设置声明式动画
四、Flash动画(过时)
五、Web Animations 1.0 ——A new general purpose animation model
2) JavaScript implementation of the Web Animations API
Web Animations API为CSS和SVG动画提供了单一接口。旨在通过提供更好的性能、更好的控制时间线和播放、灵活、统一的Javascript编程接口,使做一些事情更容易。
当前状态:
Specification at First Public Working Draft: www.w3.org/TR/web-animations
Chrome: CSS Transitions & Animations rewritten on top of the Web Animations model JavaScript API in development behind a flag
Firefox & Safari: Started implementation
IE: No public signals
实例一:A simple example
var web_animation_1 = document.getElementById("web_animation_1"); web_animation_1.addEventListener('click', function() { web_animation_1.animate([{ transform: 'rotate(0deg)' }, { transform: 'rotate(360deg)' }],{ duration: 2 }); }, false);
实例二:More complex timing
var web_animation_2 = document.getElementById("web_animation_2"); web_animation_2.addEventListener('click', function() { web_animation_2.animate([{ transform: 'rotate(0deg)' }, { transform: 'rotate(360deg)' }],{ direction: 'alternate', duration: 1, iterations: Infinity, easing: 'ease-in-out', playbackRate: 2 }); }, false);
实例三:Without the syntactic sugar
var web_animation_3 = document.getElementById("web_animation_3"); web_animation_3.addEventListener('click', function() { var obj = new Animation(web_animation_3,[{ transform: 'rotate(0deg)' }, { transform: 'rotate(360deg)' }],{ duration: 2 }); document.timeline.play(obj); }, false);
实例四:Parallel animation grouping
var web_animation_4 = document.getElementById("web_animation_4"), parItem1 = document.getElementById("parItem1"), parItem2 = document.getElementById("parItem2"), parItem3 = document.getElementById("parItem3"); web_animation_4.addEventListener('click', function() { var obj = new ParGroup([ new Animation(parItem1, [{width: '0px'}, {width: '500px'}], 1), new Animation(parItem2, [{width: '0px'}, {width: '700px'}], 1), new Animation(parItem3, [{width: '0px'}, {width: '200px'}], 1), ]) document.timeline.play(obj); }, false);
实例五:Sequential animation grouping
var web_animation_5 = document.getElementById("web_animation_5"), seqItem1 = document.getElementById("seqItem1"), seqItem2 = document.getElementById("seqItem2"), seqItem3 = document.getElementById("seqItem3"); web_animation_5.addEventListener('click', function() { var obj = new SeqGroup([ new Animation(seqItem1, [{width: '0px'}, {width: '200px'}], 1), new Animation(seqItem2, [{width: '0px'}, {width: '300px'}], 1), new Animation(seqItem3, [{width: '0px'}, {width: '200px'}], 1), ]) document.timeline.play(obj); }, false);
实例六:Nested grouped animations
var web_animation_6 = document.getElementById("web_animation_6"), outerSeqItem1 = document.getElementById("outerSeqItem1"), outerSeqItem2 = document.getElementById("outerSeqItem2"), innerParItem1 = document.getElementById("innerParItem1"), innerParItem2 = document.getElementById("innerParItem2"), innerParItem3 = document.getElementById("innerParItem3"); web_animation_6.addEventListener('click', function() { var parobj = new ParGroup([ new Animation(innerParItem1, [{width: '0px'}, {width: '300px'}], 1), new Animation(innerParItem2, [{width: '0px'}, {width: '300px'}], 1), new Animation(innerParItem3, [{width: '0px'}, {width: '300px'}], 1), ]); var seqobj = new SeqGroup([ new Animation(outerSeqItem1, [{width: '0px'}, {width: '200px'}], 1), parobj, new Animation(outerSeqItem2, [{width: '0px'}, {width: '200px'}], 1), ]); document.timeline.play(seqobj); }, false);
实例七:Path animations
var web_animation_7 = document.getElementById("web_animation_7"); web_animation_7.addEventListener('click', function() { var obj = new Animation(web_animation_7, new PathAnimationEffect( 'M 100 200 ' + 'C 200 100 300 0 400 100 ' + 'C 500 200 600 300 700 200 ' + 'C 800 100 900 100 900 100', 'auto-rotate'), { duration: 2, direction: 'alternate', easing: 'ease-in-out', iterations: Infinity, }); document.timeline.play(obj); }, false);
实例八:Custom animation effects
function customAnimationEffect(timeFraction, iteration, target) { web_animation_8.innerHTML = 'timeFraction: ' + timeFraction.toFixed(2) + '\n' + 'iteration: ' + iteration; } var web_animation_8 = document.getElementById("web_animation_8"); var obj = new Animation(null, {sample: customAnimationEffect}, { duration: 2, direction: 'alternate', easing: 'ease-in-out', iterations: Infinity, }); var customPlayer = document.timeline.play(obj); window.addEventListener('slideenter', function(event) { if (event.slide == customSlide) { customPlayer.currentTime = 0; } }, false);
实例九:综合实例
(function(document) { 'use strict'; var Animations = {}, player, controls = document.getElementById('animate-controls'); Animations.targets = { path: document.getElementById('path'), ballContainer: document.getElementById('animate-ball-container'), ball: document.getElementById('animate-ball') }; Animations.keyframeMove = new Animation(Animations.targets.ballContainer, [{ offset: 0, transform: 'translate(0,0)' }, { offset: 1, transform: 'translate(600,0)' }], { duration: 2000 }); Animations.keyframeSpinRoll = new Animation(Animations.targets.ball, [{ transform: 'rotate(950deg)' }], { duration: 2000 }); Animations.motionpathBounce = new Animation(Animations.targets.ballContainer, new MotionPathEffect("M25,25 " + "a150,100 0 0,1 300,0 " + "a75,50 0 0,1 150,0 " + "a35,20 0 0,1 70,0 " + "a2,1 0 0,1 35,0 " + "h45"), { duration: 2500 }); Animations.keyframeSpinBounce = new Animation(Animations.targets.ball, [{ transform: 'rotate(950deg)' }], { duration: 2500 }); Animations.animationGroupRoll = new AnimationGroup([Animations.keyframeMove, Animations.keyframeSpinRoll], { easing: 'ease-out' }); Animations.animationGroupBounce = new AnimationGroup([Animations.motionpathBounce, Animations.keyframeSpinBounce], { easing: 'ease-out' }); controls.addEventListener('click', function(event) { if (event.target) { var targetElement = event.target; switch (targetElement.id) { case 'keyframe-start': player = document.timeline.play(Animations.animationGroupRoll); break; case 'motionpath-start': player = document.timeline.play(Animations.animationGroupBounce); break; case 'pause': player.pause(); break; case 'cancel': player.cancel(); break; case 'play': player.play(); break; case 'reverse': player.reverse() } } }) })(document);
六、现行兼容性方案
1) 页面增强动画建议使用CSS动画
2) 复杂动画交互建议使用RAF及setInterval 或setTimeout优雅降级处理
3) 推荐动画库Velocity.js、GreenSock:
Velocity.js是一款动画切换的jQuery插件,它重新实现了jQuery的$.animate()方法从而加快动画切换的速度。Velocity.js只有7k的大小,它不仅包含了$.animate()的所有功能,并且还包含了颜色切换、转换(transform)、循环、缓动、CSS切换、Scroll功能,它是jQuery、 jQuery UI、CSS变换 在动画方面的最佳组合。
Velocity.js支持IE8+、Chrome、Firefox等浏览器,并支持Andriod以及IOS。
Velocity.js在内部实现中使用了jQuery的$.queue()方法,因此它比 jQuery的$.animate()、$.fade()、$.delay()方法更加流畅,其性能也高于CSS的animation属性。
GreenSock:GSAP v12平台
非常快的速度:性能是非常重要的,尤其是在移动设备上。GSAP不断优化,以保证互动项目的快速响应、高效率及平滑,你可以从这里查看动画效果测试。
异想天开的强劲:内置众多引擎的功能,如动画色彩、贝塞尔曲线、CSS样式属性、Flash滤镜、数组等等,定义不同的回调,可以通过帧或者秒定义运动。
兼容性:Flash,HTML5,jQuery,Canvas,CSS,新浏览器,旧浏览器,RequireJS,EaseIJS,移动设备等等-GSAP都可以很好的与他们兼容,你可以选择你熟悉的工具来使用。
Javascript,AS3/AS2:选择适合你的语言来完成动画。
轻量与可扩展性:模块化与插件式的结构保持了核心引擎的轻量,TweenLite包非常小(基本上低于7kb)。
没有依赖:GSAP没有基于第三方工具来构建(虽然它将jQuery作为选择器),因此能保证最短的加载时间与最大化性能。
高等序列:不用受限于线性序列,可以重叠动画序列,你可以通过精确时间控制,灵活地使用最少的代码实现动画。
良好的技术支持:可以通过论坛反馈,会有专家和资深活跃用户回答问题。
任何对象都可以实现动画:是的,任何,不用预定义的属性,任何对象的任意数字属性都可以实现动画,如果这些属性(如颜色,滤镜,非数值属性等)需要处理,插件可以实现。如果没有,我们可以实现一个。
重写管理:GSAP帮助防止动画引擎的冲突以及高级选项的设置。 易于学习:文档、教程、 示例、学习指南、论坛,还有很多学习资源,非常地丰富。
许可证:除商业用途意外,GSAP完全免费。
参考链接:
CSS Will Change Module Level 1
CSS Will Change Module Level 1 (中文版本)
CSS vs. JS Animation: Which is Faster?
CSS animations and transitions performance: looking inside the browser
SVG or Canvas? СHoosing Between the Two
Exploring the Web Animations API
Everything You Need to Know About the CSS will-change Property