JavaScript 运动框架(介绍及原理)
1,运动原理
Js运动,本质来说,就是让 web 上 DOM 元素动起来。而想要 DOM 动起来,改变其自身的位置属性,比如高宽,左边距,上边距,透明度等。动画的原理就是把不同状态的物体,串成连续的样子,就像一本书,画了几个小人,然后一翻书,就看见小人在动。js动画也一样。不同状态的DOM,用定时器控制,就能得到动画效果。
-
1 window.onload = function(){ 2 var oBtn = document.getElementById('btn'); 3 oBtn.onclick = function(){ 4 var oDiv = document.getElementById('div1'); 5 //设置定时器 6 setInterval(function(){ 7 //改变物体位置 8 oDiv.style.left = oDiv.offsetLeft + 10 + 'px'; 9 },30) 10 11 } 12 }
上述代码,点击btn之后,就能是物体向左运动。可是会一直向右动,不会停止。因此需要创立一个停止的条件。在条件符合的情况下,清楚定时器。其中对于目标点的判断,尤为重要。
-
1 window.onload = function(){ 2 var oBtn = document.getElementById('btn'); 3 oBtn.onclick = function(){ 4 var oDiv = document.getElementById('div1'); 5 //设置定时器 6 var timer = setInterval(function(){ 7 //判断停止条件 8 if(oDiv.offsetLeft > 300){ 9 clearInterval(timer); 10 }else{ 11 //改变物体位置 12 oDiv.style.left = oDiv.offsetLeft + 10 + 'px'; 13 document.title = oDiv.offsetLeft; 14 } 15 },30); 16 17 } 18 }
上述代码中,但物体的位置大于300的时候,将停止运动。但是上述代码还有个问题,就是连续点击按钮,物体会运动越来越快。因为每点击一次,就开了一个定时器,累加的定时器。造成运动混乱。
2,运动框架 (滑入滑出,淡入淡出)
为了解决上述问题,则必须在开启定时器之前,先清除定时器,因此需要一个全局变量 timer保存定时器。如下面代码。
-
1 window.onload = function(){ 2 var oBtn = document.getElementById('btn'); 3 oBtn.onclick = function(){ 4 startMove(); 5 } 6 } 7 var timer = null; 8 function startMove(){ 9 var oDiv = document.getElementById('div1'); 10 clearInterval(timer); 11 //设置定时器 12 timer = setInterval(function(){ 13 //判断停止条件 14 if(oDiv.offsetLeft > 300){ 15 clearInterval(timer); 16 }else{ 17 //改变物体位置 18 oDiv.style.left = oDiv.offsetLeft + 10 + 'px'; 19 document.title = oDiv.offsetLeft; 20 } 21 },30); 22 }
此外,在改变物体位置的时候,那个 “10”则是更改的数量,其实也就是速度。如果更改速度,运动的快慢就能确定。因此,运动框架的原理,基本步骤为。
-
先清除定时器
-
开启定时器,计算速度
-
判断停止条件,执行运动
-
1 var timer = null; 2 function startMove(){ 3 var oDiv = document.getElementById('div1'); 4 clearInterval(timer); 5 //计算速度 6 var iSpeed = 10; 7 //设置定时器 8 timer = setInterval(function(){ 9 //判断停止条件 10 if(oDiv.offsetLeft > 300){ 11 clearInterval(timer); 12 }else{ 13 //改变物体位置 14 oDiv.style.left = oDiv.offsetLeft + iSpeed + 'px'; 15 document.title = oDiv.offsetLeft; 16 } 17 },30); 18 }
对于停止条件,写死在里面了,所以需分离出参数。下面是一个分享到的例子。主要是根据目标判断速度的正负。从而在鼠标滑入画出时候进行运动/恢复的效果。
-
1 window.onload = function(){ 2 var oDiv = document.getElementById('div1'); 3 oDiv.onmouseover = function(){ 4 startMove(0); 5 } 6 oDiv.onmouseout = function(){ 7 startMove(-100); 8 } 9 } 10 var timer = null; 11 var iSpeed; 12 function startMove(iTatget){ 13 var oDiv = document.getElementById('div1'); 14 clearInterval(timer); 15 timer = setInterval(function(){ 16 //计算速度 17 if(iTatget -oDiv.offsetLeft > 0){ 18 iSpeed = 10; 19 }else{ 20 iSpeed = -10; 21 } 22 23 if(oDiv.offsetLeft == iTatget){ 24 clearInterval(timer); 25 }else{ 26 oDiv.style.left = oDiv.offsetLeft + iSpeed + 'px'; 27 } 28 document.title = oDiv.offsetLeft; 29 },30) 30 }
另外一个小例子,淡入淡出,即改变物体的透明度,由于没有像原生的位置属性那样的offsetLset. 需要一个变量来保存透明度的值,用来和速度加减,最后付给元素的透明度样式。从而实现淡入淡出效果。
-
1 window.onload = function(){ 2 var oImg = document.getElementById('img1'); 3 oImg.onmouseover = function(){ 4 startMove(100); 5 } 6 oImg.onmouseout = function(){ 7 startMove(30); 8 } 9 } 10 var timer = null; 11 //保存透明度的数字值 12 var alpha = 30; 13 function startMove(iTarget){ 14 var oDiv = document.getElementById('img1'); 15 clearInterval(timer); 16 timer = setInterval(function(){ 17 var iSpeed = 0; 18 if(alpha > iTarget){ 19 iSpeed = -1; 20 }else{ 21 iSpeed = 1; 22 } 23 if(alpha == iTarget){ 24 clearInterval(timer); 25 }else{ 26 //改变透明度速度值 27 alpha += iSpeed; 28 oDiv.style.filter = 'alpha(opacity:'+ alpha+')'; 29 oDiv.style.opacity = alpha/100; 30 document.title = alpha; 31 } 32 33 },30) 34 }
3,缓冲运动
缓冲运动原理就是,改变速度的值。每次累加的速度值变小,就是会是整个物体看起来越来越慢,以至于最后停掉。相当于改变使物体具有一个加速度。这个加速度,可以由物体当前位置和目标位置之间的距离得到,因为两者之间的距离一直在变小,所以速度也一直在变小。如下:
-
1 window.onload = function(){ 2 var btn = document.getElementsByTagName('input')[0]; 3 btn.onclick = function(){ 4 startMove(300); 5 } 6 } 7 var timer = null; 8 function startMove(iTarget){ 9 var oDiv = document.getElementById('div1'); 10 clearInterval(timer); 11 timer = setInterval(function(){ 12 //求出带有变化的速度 13 var iSpeed = (iTarget - oDiv.offsetLeft) / 8; 14 if(oDiv.offsetLeft == iTarget){ 15 clearInterval(timer); 16 }else{ 17 oDiv.style.left = oDiv.offsetLeft + iSpeed + 'px'; 18 } 19 document.title = oDiv.offsetLeft + '...' + iSpeed; 20 },30); 21 }
上述方法可以得到缓冲运动,但是实际运行效果,物体并没有在 300的位置停掉,而是在 293的位置就停掉了。究其原因。因为当物体的速度小于1 的时候。物体位置为293。此时计算的速度是 0.875.通过 oDiv.style.left = oDiv.offsetLeft + iSpeed + 'px'; 物体的位置为 293.875.可是计算机不能识别小数,将小数省略了。此时的 位置offsetLeft仍然是 293.再计算一次,还是同样的结果。定时器没有关掉,但是物体的位置却再也无法改变,故停留在了 293的位置。解决方案,就是将速度进行向上取整。但是,像上述运动,速度是正的,可是,当速度是负的时候,就同样会有相同的结果,因此需要在速度为负的时候,向下取整。
-
1 function startMove(iTarget){ 2 var oDiv = document.getElementById('div1'); 3 clearInterval(timer); 4 timer = setInterval(function(){ 5 var iSpeed = (iTarget - oDiv.offsetLeft) / 8; 6 //对正的速度向上取整,负的速度向下取整 7 iSpeed = iSpeed > 0 ? Math.ceil(iSpeed) : Math.floor(iSpeed); 8 if(oDiv.offsetLeft == iTarget){ 9 clearInterval(timer); 10 }else{ 11 oDiv.style.left = oDiv.offsetLeft + iSpeed + 'px'; 12 } 13 document.title = oDiv.offsetLeft + '...' + iSpeed; 14 },30); 15 }
4.多物体运动
下一步,就是处理多物体运动,运动函数里面每次都要选取一个元素加事件。如果需要对多个物体进行同样的运动, 需要将运动对象作为参数传进来。
-
1 window.onload = function(){ 2 var aDiv = document.getElementsByTagName('div'); 3 for(var i=0;i<aDiv.length;i++){ 4 aDiv[i].onmouseover = function(){ 5 startMove(this,300); 6 } 7 aDiv[i].onmouseout = function(){ 8 startMove(this,100); 9 } 10 } 11 } 12 var timer = null; 13 function startMove(obj,iTarget){ 14 clearInterval(timer); 15 timer = setInterval(function(){ 16 var iSpeed = (iTarget - obj.offsetWidth) / 8; 17 iSpeed = iSpeed > 0 ? Math.ceil(iSpeed) : Math.floor(iSpeed); 18 if(obj.offsetWidth == iTarget){ 19 clearInterval(timer); 20 }else{ 21 obj.style.width = obj.offsetWidth + iSpeed + 'px'; 22 } 23 24 },30) 25 }
通过循环物体,将物体的 this传给运动函数,使得多物体可以运动。但是这样有一个弊端,即当滑入第一个运动的时候,开启了定时器。如果此时,滑入另外一个物体,将会清理上一个定时器。这就造成了,上一次运动,很有可能还没完成结束,定时器就没关闭了。解决的方法,每个运动的物体,都能开了一个属于自己的定时器。因此,把定时器当成物体的属性。清理的时候也就是清理自己的定时器。
-
window.onload = function(){ var aDiv = document.getElementsByTagName('div'); for(var i=0;i<aDiv.length;i++){ aDiv[i].onmouseover = function(){ startMove(this,300); } aDiv[i].onmouseout = function(){ startMove(this,100); } } } function startMove(obj,iTarget){ //将定时器,变成物体的属性,类似给物体添加索引 clearInterval(obj.timer); obj.timer = setInterval(function(){ var iSpeed = (iTarget - obj.offsetWidth) / 8; iSpeed = iSpeed > 0 ? Math.ceil(iSpeed) : Math.floor(iSpeed); if(obj.offsetWidth == iTarget){ clearInterval(obj.timer); }else{ obj.style.width = obj.offsetWidth + iSpeed + 'px'; } },30) }
多物体的淡入淡出的时候,也有类似的问题。因为修改透明度的时候,是先用一个变量保存透明度,必须针对每个物体设立透明度值属性。
-
1 window.onload = function(){ 2 var aDiv = document.getElementsByTagName('div'); 3 for(var i=0;i<aDiv.length;i++){ 4 //将透明度值当初属性 5 aDiv[i].alpha = 30; 6 aDiv[i].onmouseover = function(){ 7 startMove(this,100); 8 } 9 aDiv[i].onmouseout = function(){ 10 startMove(this,30); 11 } 12 } 13 } 14 function startMove(obj,iTarget){ 15 clearInterval(obj.timer); 16 obj.timer = setInterval(function(){ 17 var iSpeed = (iTarget - obj.alpha) / 8; 18 iSpeed = iSpeed > 0 ? Math.ceil(iSpeed) : Math.floor(iSpeed); 19 if(obj.alpha == iTarget){ 20 clearInterval(obj.timer); 21 }else{ 22 obj.alpha += iSpeed; 23 obj.style.filter = 'alpha(opacity:'+obj.alpha+')'; 24 obj.style.opacity = obj.alpha / 100; 25 } 26 document.title = obj.alpha; 27 },30); 28 }
4.1 位置属性的bug
offsetWidth 或者 offsetHeight 等位置属性,一旦给他们加上 border。则会有诡异的现象出现。
-
window.onload = function(){ var oDiv = document.getElementById('div1'); setInterval(function(){ oDiv.style.width = oDiv.offsetWidth - 1 + "px"; },30) }
例如 oDiv.style.width = oDiv.offsetWidth - 1 + 'px'; 如果给 oDiv 的width 为一百,border 为 1.则这个物体的 width是100px;offsetWidth 为102px;带入公式之后,即减一之后。100 = 102 - 1 ,反而等于101;即 物体本来要减小,事实却增大了。解决的方案就是,加减的时候,必须使用物体的内联样式。但是 火狐 和 IE 又有兼容模式。解决方案如下:
-
1 window.onload = function(){ 2 var oDiv = document.getElementById('div1'); 3 setInterval(function(){ 4 5 oDiv.style.width = parseInt(getStyle(oDiv,'width')) - 1 + 'px'; 6 7 },30) 8 } 9 function getStyle(obj,attr){ 10 if(obj.currentStyle){ 11 return obj.currentStyle[attr]; 12 }else{ 13 return getComputedStyle(obj,false)[attr]; 14 } 15 }
其中,getStyle函数,传入一个元素对象,和其 css 属性,获取的是元素的样式,即 witdh 100px;因此需要parseInt转换
5.任意值运动
通过 getStyle 函数,可以获取元素的样式,还可也通过 attr 制定需要修改的 css属性。这样就能是物体有不同的运动形式。
-
1 window.onload = function(){ 2 var aDiv = document.getElementsByTagName('div'); 3 aDiv[0].onmouseover = function(){ 4 startMove(this,'width',300); 5 } 6 aDiv[0].onmouseout = function(){ 7 startMove(this,'width',100); 8 } 9 aDiv[1].onmouseover = function(){ 10 startMove(this,'height',100); 11 } 12 aDiv[1].onmouseout = function(){ 13 startMove(this,'height',50); 14 } 15 } 16 function getStyle(obj,attr){ 17 if(obj.currentStyle){ 18 return obj.currentStyle(attr); 19 }else{ 20 return getComputedStyle(obj,false)[attr]; 21 } 22 } 23 function startMove(obj,attr,iTarget){ 24 clearInterval(obj.timer); 25 obj.timer = setInterval(function(){ 26 var iCur = parseInt(getStyle(obj,attr)); 27 var iSpeed = (iTarget - iCur) / 8; 28 iSpeed = iSpeed > 0 ? Math.ceil(iSpeed) : Math.floor(iSpeed); 29 if(iCur == iTarget){ 30 clearInterval(obj.timer); 31 }else{ 32 obj.style[attr] = iCur + iSpeed + 'px'; 33 } 34 35 },30) 36 }
5.1 任意值完美版
上述版本,还不能处理透明度的任意值,因此需要增加额外的兼容hack。
-
1 window.onload = function(){ 2 var aDiv = document.getElementsByTagName('div'); 3 aDiv[0].onmouseover = function(){ 4 startMove(this,'opacity',100); 5 } 6 aDiv[0].onmouseout = function(){ 7 startMove(this,'opacity',30); 8 } 9 } 10 function getStyle(obj,attr){ 11 if(obj.currentStyle){ 12 return obj.currentStyleattr[attr]; 13 }else{ 14 return getComputedStyle(obj, false)[attr]; 15 } 16 } 17 function getStyle(obj, attr){ 18 if(obj.currentStyle) { 19 return obj.currentStyle[attr]; 20 }else{ 21 return getComputedStyle(obj, false)[attr]; 22 } 23 } 24 function startMove(obj,attr,iTarget){ 25 clearInterval(obj.timer); 26 obj.timer = setInterval(function(){ 27 var iCur = 0; 28 if(attr == 'opacity'){ 29 iCur = parseInt(parseFloat(getStyle(obj, attr))*100); 30 }else{ 31 iCur = parseInt(getStyle(obj,attr)); 32 } 33 var iSpeed = (iTarget - iCur) / 8; 34 iSpeed = iSpeed > 0 ? Math.ceil(iSpeed) : Math.floor(iSpeed); 35 if(iCur == iTarget){ 36 clearInterval(obj.timer); 37 }else{ 38 if(attr=='opacity'){ 39 iCur += iSpeed 40 obj.style.filter='alpha(opacity:' + iCur + ')'; 41 obj.style.opacity=iCur / 100; 42 } 43 else{ 44 obj.style[attr]=iCur+iSpeed+'px'; 45 } 46 document.title = obj.style[attr]; 47 } 48 49 },30) 50 }
6.链式运动
我们的运动框架到目前为止,基本功能都能实现了。现在拓展。所谓链式运动,即运动接着运动。当运动停止的时候,如果回调一个函数。回调一个运动函数,就能出现这样的效果。因此传入一个函数作为回调函数。
-
1 window.onload = function(){ 2 var oDiv = document.getElementById('div1'); 3 oDiv.onclick = function(){ 4 startMove(this,'width',300,function(){ 5 startMove(oDiv,'height',300,function(){ 6 startMove(oDiv,'opacity',100) 7 }) 8 }) 9 } 10 } 11 function getStyle(obj,attr){ 12 if(obj.currentStyle){ 13 return obj.currentStyleattr[attr]; 14 }else{ 15 return getComputedStyle(obj, false)[attr]; 16 } 17 } 18 19 function startMove(obj,attr,iTarget,fn){ 20 clearInterval(obj.timer); 21 obj.timer = setInterval(function(){ 22 var iCur = 0; 23 if(attr == 'opacity'){ 24 iCur = parseInt(parseFloat(getStyle(obj, attr))*100); 25 }else{ 26 iCur = parseInt(getStyle(obj,attr)); 27 } 28 var iSpeed = (iTarget - iCur) / 8; 29 iSpeed = iSpeed > 0 ? Math.ceil(iSpeed) : Math.floor(iSpeed); 30 if(iCur == iTarget){ 31 clearInterval(obj.timer); 32 //回调函数 33 if(fn) fn(); 34 }else{ 35 if(attr=='opacity'){ 36 iCur += iSpeed 37 obj.style.filter='alpha(opacity:' + iCur + ')'; 38 obj.style.opacity=iCur / 100; 39 } 40 else{ 41 obj.style[attr]=iCur+iSpeed+'px'; 42 } 43 document.title = obj.style[attr]; 44 } 45 46 },30) 47 }
7.同时运动
目前为止,我们的运动框架还有个小缺点,就是不能同时该两个属性进行运动,比如同时更改宽和高。这个要求传入的属性是不同的几个值。则考虑传入一个 json用来保存需要更改的属性。
-
1 window.onload = function(){ 2 var oDiv = document.getElementById('div1'); 3 oDiv.onclick = function(){ 4 startMove(this,{'width':300,'height':400}); 5 } 6 } 7 function getStyle(obj, attr){ 8 if(obj.currentStyle) { 9 return obj.currentStyle[attr]; 10 }else{ 11 return getComputedStyle(obj, false)[attr]; 12 } 13 } 14 function startMove(obj,json,fn){ 15 clearInterval(obj.timer); 16 obj.timer = setInterval(function(){ 17 // 循环json 18 for(var attr in json){ 19 var iCur = 0; 20 if(attr == 'opacity'){ 21 iCur = parseInt(parseFloat(getStyle(obj, attr))*100); 22 }else{ 23 iCur = parseInt(getStyle(obj,attr)); 24 } 25 var iSpeed = (json[attr] - iCur) / 8; 26 iSpeed = iSpeed > 0 ? Math.ceil(iSpeed) : Math.floor(iSpeed); 27 if(iCur == json[attr]){ 28 clearInterval(obj.timer); 29 if(fn) fn(); 30 }else{ 31 if(attr=='opacity'){ 32 iCur += iSpeed 33 obj.style.filter='alpha(opacity:' + iCur + ')'; 34 obj.style.opacity=iCur / 100; 35 } 36 else{ 37 obj.style[attr]=iCur+iSpeed+'px'; 38 } 39 document.title = obj.style[attr]; 40 } 41 } 42 },30)
上述代码,可以解决了同时运动的问题。但是还是有一个bug。比如,同时运动的某个属性,如果变化很小,马上就停止了,即关掉了定时器。那么会造成其他属性的变化也停止。因为这些属性都共用了一个定时器。因此需要判断,假设有三个人要来,然后一起去爬山。三个人有的先来,有的后来,只要三个人都到齐了,才出发。也就是只有三个属性都到了目标值,才关定时器。一开始,设立一个检查量,为真。假设所有人都到了,然后循环,只有有一个人没有到,检查就为假。直到所有的都到了,检测为真。则停止定时器。
-
1 window.onload = function(){ 2 var oDiv = document.getElementById('div1'); 3 oDiv.onclick = function(){ 4 startMove(this,{'width':102,'height':400,'opacity':100}); 5 } 6 } 7 function getStyle(obj, attr){ 8 if(obj.currentStyle) { 9 return obj.currentStyle[attr]; 10 }else{ 11 return getComputedStyle(obj, false)[attr]; 12 } 13 } 14 function startMove(obj,json,fn){ 15 clearInterval(obj.timer); 16 obj.timer = setInterval(function(){ 17 var bStop = true; 18 for(var attr in json){ 19 //取当前值 20 var iCur = 0; 21 if(attr == 'opacity'){ 22 iCur = parseInt(parseFloat(getStyle(obj, attr))*100); 23 }else{ 24 iCur = parseInt(getStyle(obj,attr)); 25 } 26 //计算速度 27 var iSpeed = (json[attr] - iCur) / 8; 28 iSpeed = iSpeed > 0 ? Math.ceil(iSpeed) : Math.floor(iSpeed); 29 //检测停止 30 if(iCur != json[attr]){ 31 bStop = false; 32 } 33 if(attr=='opacity'){ 34 iCur += iSpeed 35 obj.style.filter='alpha(opacity:' + iCur + ')'; 36 obj.style.opacity=iCur / 100; 37 } 38 else{ 39 obj.style[attr]=iCur+iSpeed+'px'; 40 } 41 } 42 if(bStop){ 43 clearInterval(obj.timer); 44 if(fn) fn(); 45 } 46 },30) 47 }
再循环外定义一个 标志变量 bStop = true。用来表示所有属性到达目标值。等循环结束了,如果这个值是真的,则停止定时器。因为,每次运行定时器,都会初始化这个值。循环的过程中,只要有一个没有到,bStop就被设定为 false。如果某个到了,此时 iCur != json[attr],表示速度为0 后面执行的结果,也不会有变化。只有所有的都达到目标值。循环则不再改变 bStop的值。此时,只要下一次运行定时器。就是初始化 bStop为真。而循环因为都到了,所以速度为0 也就再也没有变化。循环结束,sBstop还是真,表示所有都到了。因此此时结束定时器。
最后附上完美运动框架,封装成 move.js 就可以调用了。
1 /** 2 * @author rsj217 3 * getStyle 获取样式 4 * startMove 运动主程序 5 */ 6 7 function getStyle(obj, attr){ 8 if(obj.currentStyle) { 9 return obj.currentStyle[attr]; //for ie 10 }else{ 11 return getComputedStyle(obj, false)[attr]; // for ff 12 } 13 } 14 function Move(obj,json,fn){ 15 //停止上一次定时器 16 clearInterval(obj.timer); //关闭前一个定时器,解决对同个对象同时调用多个Move()时,定时器叠加问题。使用obj.timer给每个调用Move()的对象赋予各自的定时器,防止多个对象同时调用Move()时,同用一个定时器,而导致相关干扰问题。 17 //保存每一个物体运动的定时器 18 obj.timer = setInterval(function(){ 19 //判断同时运动标志 20 var bStop = true; 21 for(var attr in json){ 22 //取当前值 23 var iCur = 0; //创建一个变量,用于存储 attr属性每时每刻的值 24 if(attr == 'opacity'){ 25 //针对在FF中opacity属性值为浮点数值问题,将属性值 四舍五入、转换成浮点型。乘以100,使opacity属性值与IE统一为百分数 26 iCur = Math.round(parseFloat(getStyle(obj, attr))*100); 27 }else{ 28 iCur = parseInt(getStyle(obj,attr)); //将除opacity外的属性(width/fontSize/MarginLeft等)的初始值 转换为整型 29 } 30 //计算速度 31 var iSpeed = (json[attr] - iCur) / 8; //创建 递减的速度speed变量。实现属性值的变速改变 32 iSpeed = iSpeed > 0 ? Math.ceil(iSpeed) : Math.floor(iSpeed); //取整,解决浏览器忽略小于1px的数值 导致运动结束时,离目标值Itarget少几个像素的问题 33 //检测同时到达标志 34 if(iCur != json[attr]){ 35 bStop = false; 36 } 37 //更改属性,获取动画效果 38 if(attr=='opacity'){ 39 iCur += iSpeed 40 obj.style.filter='alpha(opacity:' + iCur + ')'; 41 obj.style.opacity=iCur / 100; 42 } 43 else{ 44 obj.style[attr]=iCur+iSpeed+'px'; 45 } 46 } 47 //检测停止 48 if(bStop){ 49 clearInterval(obj.timer); 50 if(fn) fn(); 51 } 52 },30) 53 }