其实Web动画的实现原理跟早期的运动影片很类似,都是通过将一张张的赛璐珞片以较快速度播放,从而模拟出连贯的物体运动。而这一张张的赛璐珞片就类似于投影运动媒体的帧的概念,而几乎所有投影运动媒体都是通过帧来实现的。
几乎所有的程序动画都会表现为某种形式的循环,我们会创建一个展现一系列图像的流程图以实现逐帧动画,其中每一帧只需要绘制出来即可。
为了实现动画,需要为每一帧执行以下操作:
1 . 执行该帧所要调用到的代码;
2 . 将所有对象绘制到出来;
3 . 重复这一过程渲染下一帧。
在H5时代,实现Web动画的方法有很多:
可以使用CSS3中的animation + keyframes或者transition,可以通过SVG中的SMIL动画接口,也可以借助jQuery动画相关的API。还可以使用JavaScript中最原始window.setTimout()和window.setInterval()通过不断更新元素的状态位置等来实现动画。
在HTML5中又提出了一种新的基于浏览器优化动画实现的方案 —— window.requestAnimationFrame()方法。
下面主要讨论一下JavaScript中动画循环函数 —— setTimeout()、setInterval()和requestAnimationFrame(),他们的对应取消循环函数分别是clearTimeout()、clearInterval()和cancelAnimationFrame()。
setTimeout实现循环动画的原理:
(function drawFrame() { var timer = null; var delayTime = 1000 / 60; // 帧渲染和帧绘制 ... timer = setTimeout(drawFrame, delayTime); // 停止循环 if( /* 停止条件成立 */ ) { clearTimeout(timer); } })();
setInterval实现循环动画的原理:
var timer = null; var delayTime = 1000/ 60; timer = setInterval(drawFrame, delayTime); function drawFrame() { // 帧渲染和帧绘制 ... // 停止循环 if( /* 动画停止条件成立 */ ) { clearInterval(timer); } }
setInterval却没有被所调用的函数所束缚,它只是简单地每隔一定时间就重复执行一次所调用的函数。而setTimeout受所调用函数的影响,只有执行完成该次的函数调用,才能继续执行下一次的函数调用。
如果要求在每隔一个固定的时间间隔后就精确地执行某动作,那么最好使用setInterval。
如果不想由于连续调用产生互相干扰的问题,尤其是每次函数的调用需要繁重的计算以及很长的处理时间,那么最好使用setTimeout。
requestAnimationFrame()的原理其实与setTimeout和setInterval类似,通过递归调用同一方法来不断更新画面以达到动画效果,但它优于setTimeout和setInterval的地方在于它是由浏览器专门为动画提供优化实现的API,并且充分利用显示器的刷新机制,比较节省系统资源。显示器有固定的刷新频率(60Hz或75Hz),也就是说,每秒最多只能重绘60次或75次,requestAnimationFrame的基本思想就是与这个刷新频率保持同步,利用这个刷新频率进行页面重绘。此外,使用这个API,一旦页面不处于浏览器的当前标签,就会自动停止刷新。这就节省了CPU、GPU和电力。
不过有一点需要注意,requestAnimationFrame是在主线程上完成。这意味着,如果主线程非常繁忙,requestAnimationFrame的动画效果会大打折扣。
requestAnimationFrame的语法如下:
requestAnimationFrame(callback) //callback为回调函数
requestAnimationFrame动画的实现原理与setTimeout类似,都是使用一个回调函数作为参数,且这个回调函数会在浏览器重绘之前调用。具体如下:
(function drawFrame() { var timer = null; // 帧渲染和帧绘制 ... timer = requestAnimationFrame(drawFrame); // 停止循环 if( /* 停止条件成立 */ ) { cancelAnimationFrame(timer); } })();
由于requestAnimationFrame是HTML5新定义的API,旧版本的浏览器并不兼容,而且浏览器的实现方式不一,此时就需要考虑到兼容性问题了。常用的兼容性写法如下:
window.requestAnimFrame = (function(){ return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || function( callback ){ window.setTimeout(callback, 1000 / 60); }; })(); window.cancelAnimationFrame = (function () { return window.cancelAnimationFrame || window.webkitCancelAnimationFrame || window.mozCancelAnimationFrame || window.oCancelAnimationFrame || function (timer) { window.clearTimeout(timer); }; })();
上面兼容性代码作用有两个,一是把各浏览器前缀进行统一,二是在浏览器没有requestAnimationFrame方法时将其指向setTimeout方法。
更具体的兼容性请看这里~http://caniuse.com/#feat=requestanimationframe
截个图,如下:
在动画中我们也许常用到用户交互效果,而用户交互是基于用户事件的,这些事件通常是鼠标事件、触摸事件以及键盘事件。
事件监听器是监听事件的对象。最原始的时间监听是使用"on + 事件"的方法监听事件,如"onclick"等。在现在标准Web浏览器中还可以通过调用DOM元素的addEventListener()方法指定它作为某个特定时间的监听器,在IE6~8中不兼容addEventListener(),但可以使用attachEvent()来实现类似的效果。
常用的兼容性写法如下:
// eventType为不含"on"的事件类型 var bind = (function(ele, eventType, callback) { if(ele.addEventListener) { // W3C标准写法 return ele.addEventListener(eventType, callback, false); }else if(ele.attachEvent) { // 兼容IE6~8 return ele.attachEvent(eventType, callback); }else { // 兼容IE5- return ele["on" + eventType] = callback; } })(); var unbind = (function(ele, eventType, callback) { if(ele.removeEventListener) { // W3C标准写法 return ele.removeEventListener(eventType, callback, false); }else if(ele.detachEvent) { // 兼容IE6~8 return ele.detachEvent(eventType, callback); }else { // 兼容IE5- return ele["on" + eventType] = null; } })();
onmousedown, onmouseup, onclick, ondbclick, onmousewheel, onmousemove, onmouseover, onmouseout;
ontouchstart, ontouchend, ontouchmove;
onkeydown, onkeyup, onkeypress;
onabort(图片在下载时被用户中断), onbeforeunload(当前页面的内容将要被改变时触发), onerror(出现错误时触发), onload(内容加载完成时触发), onmove(浏览器窗口被移动时触发), onresize(浏览器的窗口大小被改变时触发), onscroll(滚动条位置发生变化时触发), onstop(浏览器的停止按钮被按下时触发此事件或者正在下载的文件被中断时触发), onunload(当前页面将被改变时触发);
onblur(元素失去焦点时触发), onchange(元素失去焦点且元素内容发生改变时触发), onfocus(元素获得焦点时触发), onreset(表单中reset属性被激活时触发), onsubmit(表单被提交时触发);oninput(在input元素内容修改后立即被触发,兼容IE9+)
onbeforecopy:当页面当前的被选择内容将要复制到浏览者系统的剪贴板前触发此事件;
onbeforecut:当页面中的一部分或者全部的内容将被移离当前页面[剪贴]并移动到浏览者的系统剪贴板时触发此事件;
onbeforeeditfocus:当前元素将要进入编辑状态;
onbeforepaste:内容将要从浏览者的系统剪贴板传送[粘贴]到页面中时触发此事件;
onbeforeupdate:当浏览者粘贴系统剪贴板中的内容时通知目标对象;
oncontextmenu:当浏览者按下鼠标右键出现菜单时或者通过键盘的按键触发页面菜单时触发的事件;
oncopy:当页面当前的被选择内容被复制后触发此事件;
oncut:当页面当前的被选择内容被剪切时触发此事件;
onlosecapture:当元素失去鼠标移动所形成的选择焦点时触发此事件;
onpaste:当内容被粘贴时触发此事件;
onselect:当文本内容被选择时的事件;
onselectstart:当文本内容选择将开始发生时触发的事件;
ondrag:当某个对象被拖动时触发此事件 [活动事件];
ondragdrop:一个外部对象被鼠标拖进当前窗口时触发;
ondragend:当鼠标拖动结束时触发此事件;
ondragenter:当对象被鼠标拖动的对象进入其容器范围内时触发此事件;
ondragleave:当对象被鼠标拖动的对象离开其容器范围内时触发此事件;
ondragover:当某被拖动的对象在另一对象容器范围内拖动时触发此事件;
ondragstart:当某对象将被拖动时触发此事件;
ondrop:在一个拖动过程中,释放鼠标键时触发此事件;
每个鼠标事件都有两个属性用于确定鼠标当前位置:pageX和pageY。但是IE6~8不知持这两个属性,需要用到clientX和clientY。
其中,pageX和pageY的鼠标位置是相对于document文档的,而clientX和clientY的鼠标位置是相对于浏览器屏幕的。为了实现各平台统一,兼容性写法可以如下:
// 初始化鼠标位置,这里的鼠标位置默认是相对于document文档的 var mouse = { x: 0, y: 0 }; function getMouse(event) { var event = event || window.event; if(event.pageX || event.pageY) { x = event.x; y = event.y; }else { var scrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft; var scrollTop = document.documentElement.scrollTop || document.body.scrollTop; x = event.clientX + scrollLeft; y = event.clientY + scrollTop; } mouse.x = x; mouse.y = y; return mouse; }
一个触摸点可以被想象成鼠标光标,不过鼠标光标会一直停留在屏幕上,而手指却会从设备上按下、移动以及释放,所以某些时刻光标会从屏幕上消失。另外,触摸屏上不存在mouseover等效的触摸事件。同一时间可能发生多点触摸,某个触摸点的信息会保存在触摸事件的一个数组中。
获取触摸位置的方法见下:
// 触摸位置声明 var touch = { x: null, y: null, isPress: false } function getTouch (event) { var x, y, touchEvent = event.touches[0]; //获取触摸位置的第一个触摸点 var event = event || window.event; if(touchEvent.pageX || touchEvent.pageY) { touchEvent.pageX; y = touchEvent.pageY; }else { var scrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft; var scrollTop = document.documentElement.scrollTop || document.body.scrollTop; x = touchEvent.clientX + scrollLeft; y = touchEvent.clientY + scrollTop; } touch.x = x; touch.y = y; return touch; }
常用的方法是,如果不存在有效的触摸点是,x和y的值应设置为null。
element.addEventListener("touchstart", function(event) { touch.isPressed = true; }, false); element.addEventListener("touchsend", function(event) { touch.isPressed = false; touch.x = null; touch.y = null; }, false); element.addEventListener("touchsend", function(event) { if(touch.isPressed) { getTouch (event); } }, false);
获得键盘码可以使用event.keyCode。具体实现如下:
var keyCode; function getKeyCode(event) { var event = event || window.event; keyCode = event.keyCode; return keyCode; }
见我写的另外一篇文章《JavaScript滚轮事件兼容性写法》
多数Web动画都是由一帧帧的状态通过较快速度的播放模拟出来的,所以循环计时函数在这里就起到了连接连贯的帧状态的作用。而动画更多时候需要用户交互,所以事件和事件监听尤显重要。最后我列出了几个经常用到几个与事件相关的封装应用,方便自己查阅和调用。