在操作dom元素的时候为了让网站显得更有活力或者某些想让人注意到, 经常需要用到一些小动画, 但常用的 jquery 库只有一种ease(先加速后减速的动画)运动方式的动画, 虽然这是很常用的动画, 但有时也会用到其他的, 最近写了一个集成几种常用动画的库, move.js , 如果不是走动画队列的话, 通常的动画库在进行一个动画的时候, 在对元素施加另一个动画就会马上停止当前动画, 马上执行新添加的动画, move动画库稍微修改了一下, 在新动画添加之后, 老动画还会继续跑, 两个动画会进行叠加. 你可以在demo演示中疯狂的点击start, demo演示
四种常见动画:
- ease ---- 先加速后减速动画, 初速度较小开始加速, 过了中点就减速, 这也是jquery默认动画
- easeOut ---- 初速度较大, 一直做减速运动
- collision ---- 碰撞动画, 初速度较小或为0, 一直加速, 碰撞终点的时候反弹, 就像篮球落地
- elastic ---- 弹性动画, 初速度较大, 一直做加速度减小的加速运动, 到达终点位置加速度为0(因为设置了阻力,所以加速度为0的地方在终点之前), 来回摆动
动画曲线:
有了动画曲线, 这几种动画其实只是一道简单的高一物理题, 只需要特别注意一下终点位置像素偏差如何处理.
终点位置处理
因为使用的定时器做动画, 时间间隔是13ms, 因为jquery也是13ms, 我测试了13ms也可以得到比较平滑的效果, 但因为浏览器刷新时间间隔是16.7ms, 我的理解是需要3ms多进行dom渲染, 这点不太确定, 忘知道的朋友指点. 因为每隔13ms里目标值接近几个像素, 那么如果在接近终点的时候, 如果定时器上一次设置得值离目标1px, 那么下一次可能跳过目标, 因此会做位置判断, 如果速度>0, 并且当前位置已经>=目标值, 或者速度<0,当前位置<=目标值, 就让当前位置在目标位置.
代码如下:
var move = { css: function(obj, attr){ if( typeof attr === "string" ){ return obj.currentStyle ?obj.currentStyle[attr] : window.getComputedStyle(obj, false)[attr]; } else if( typeof attr === "object" ){ var a; for(a in attr){ switch(a){ case "width": case "height": case "left": case "top": case "right": case "padding": case "paddingLeft": case "paddingRight": case "paddingTop": case "paddingBottom": case "margin": case "marginLeft": case "marginRight": case "marginTop": case "marginBottom": case "borderRadius": case "borderWidth": if( typeof attr[a] === "number" ) obj.style[a] = attr[a] + ‘px‘; else obj.style[a] = attr[a]; break; case "opacity": if( +attr[a] < 0 ) attr[a] = 0; obj.style.filter = "alpha(opacity="+ attr[a]*100 +")"; obj.style.opacity = attr[a]; break; default: obj.style[a] = attr[a]; } } } }, init: function(obj, json, time){ if( !obj.ani ){ obj.ani = {}; //动画对象 obj.ani.s0 = {}, //当前值 obj.ani.st = {}, //目标值 obj.ani.dis = {}, //目标值和起始值距离 obj.ani.va = {}, //平均速度 obj.ani.v = {}, //初始速度,当前速度 obj.ani.a = {}, //加速度 obj.ani.d = {}, //t时间段内的位移 obj.ani.res = {}; //此刻的结果 } obj.aniOver = false; obj.ani.time = time || 500; obj.ani.interval = 13; obj.ani.total = Math.ceil( obj.ani.time/obj.ani.interval ); //定时器总次数 obj.ani.t = 0; //当前次数 //如果第一次动画还没结束第二次就开始了, 就将第二次的json属性传入obj.ani.st(第一次的还在) //并且上一次动画的目标值不受影响 var attr; for( attr in json) obj.ani.st[attr] = parseFloat(json[attr], 10); for( attr in obj.ani.st ){ obj.ani.s0[attr] = parseFloat(move.css(obj, attr), 10); // obj.ani.st[attr] = obj.ani.st[attr]; obj.ani.dis[attr] = obj.ani.st[attr] - obj.ani.s0[attr]; obj.ani.va[attr] = obj.ani.dis[attr]/obj.ani.total; obj.ani.d[attr] = 0; } }, ease: function(obj, json, time, fn){ if( obj.aniOver === false ) clearInterval(obj.ani.timer); this.init(obj, json, time); var attr, This = this; //因为每一种动画的初始速度, 最大速度, 加速度不同, 所以这三个单独设置 for( attr in obj.ani.st ){ obj.ani.v[attr] = 0.5*obj.ani.va[attr]; //假设最大速度是3倍平均速度,初速度是0.5倍, 因此是3-0.5 obj.ani.a[attr] = (3-0.5)*obj.ani.va[attr]/(0.5*obj.ani.total); } obj.ani.timer = setInterval(function(){ obj.ani.t++; for( attr in obj.ani.st ){ if( Math.abs(obj.ani.d[attr]) < Math.abs(obj.ani.dis[attr]/2) ){ obj.ani.v[attr] += obj.ani.a[attr]; obj.ani.d[attr] += obj.ani.v[attr]; } else if( Math.abs(obj.ani.d[attr])>=Math.abs(obj.ani.dis[attr]/2) && Math.abs(obj.ani.d[attr])<=Math.abs(obj.ani.dis[attr]) ){ obj.ani.v[attr] -= obj.ani.a[attr]; obj.ani.d[attr] += obj.ani.v[attr]; } obj.ani.res[attr] = obj.ani.s0[attr] + obj.ani.d[attr]; if( (obj.ani.v[attr] > 0 && obj.ani.res[attr] > obj.ani.st[attr]) || (obj.ani.v[attr] < 0 && obj.ani.res[attr] < obj.ani.st[attr]) ) obj.ani.res[attr] = obj.ani.st[attr]; if( obj.ani.t > obj.ani.total ){ clearInterval(obj.ani.timer); obj.aniOver = true; break; } } move.css(obj, obj.ani.res); if( obj.aniOver ){ obj.ani.st = {}; obj.ani.res = {}; if( typeof fn === "function" ) fn.call(obj); } }, obj.ani.interval); }, easeOut: function(obj, json, time, fn){ if( obj.aniOver === false ) clearInterval(obj.ani.timer); this.init(obj, json, time); var attr, This = this; //因为每一种动画的初始速度, 最大速度, 加速度不同, 所以这三个单独设置 for( attr in obj.ani.st ){ obj.ani.v[attr] = 5*obj.ani.va[attr]; obj.ani.a[attr] = -6*obj.ani.va[attr]/(0.5*obj.ani.total); } obj.ani.timer = setInterval(function(){ obj.ani.t++; for( attr in obj.ani.st ){ obj.ani.v[attr] += obj.ani.a[attr]; obj.ani.d[attr] += obj.ani.v[attr]; obj.ani.res[attr] = obj.ani.s0[attr] + obj.ani.d[attr]; if( (obj.ani.v[attr] > 0 && obj.ani.res[attr] > obj.ani.st[attr]) || (obj.ani.v[attr] < 0 && obj.ani.res[attr] < obj.ani.st[attr]) ){ for( attr in obj.ani.res ) obj.ani.res[attr] = obj.ani.st[attr]; clearInterval(obj.ani.timer); obj.aniOver = true; break; } } move.css(obj, obj.ani.res); if( obj.aniOver && typeof fn === "function" ) fn.call(obj); }, obj.ani.interval); }, collision: function(obj, json, time, fn){ if( obj.aniOver === false ) clearInterval(obj.ani.timer); this.init(obj, json, time); var attr, This = this, temp; //因为每一种动画的初始速度, 最大速度, 加速度不同, 所以这三个单独设置 for( attr in obj.ani.st ){ obj.ani.v[attr] = 2*obj.ani.va[attr]; obj.ani.a[attr] = 6*obj.ani.va[attr]/(0.5*obj.ani.total); } obj.ani.timer = setInterval(function(){ obj.ani.t++; for( attr in obj.ani.st ){ console.log(obj.ani.st) if( obj.ani.d[attr] === obj.ani.dis[attr] ) obj.ani.v[attr]*=-0.5; obj.ani.v[attr] += obj.ani.a[attr]; obj.ani.v[attr] *= 0.999; temp = obj.ani.dis[attr] - obj.ani.d[attr]; if( temp*obj.ani.v[attr] > 0 && Math.abs(temp) < Math.abs(obj.ani.v[attr]) ){ obj.ani.d[attr] += temp; } else{ obj.ani.d[attr] += obj.ani.v[attr]; } obj.ani.res[attr] = obj.ani.s0[attr] + obj.ani.d[attr]; if( obj.ani.t > obj.ani.total ){ for( attr in obj.ani.res ) obj.ani.res[attr] = obj.ani.st[attr]; clearInterval(obj.ani.timer); obj.aniOver = true; break; } } move.css(obj, obj.ani.res); if( obj.aniOver ){ obj.ani.st = {}; obj.ani.res = {}; if( typeof fn === "function" ) fn.call(obj); } }, obj.ani.interval); }, elastic: function(obj, json, fn){ if( obj.aniOver === false ) clearInterval(obj.ani.timer); this.init(obj, json); var attr, This = this, factor={}; //因为每一种动画的初始速度, 最大速度, 加速度不同, 所以这三个单独设置 for( attr in obj.ani.st ){ obj.ani.v[attr] = 0*obj.ani.va[attr]; factor[attr] = 0.06; } obj.ani.timer = setInterval(function(){ obj.ani.t++; for( attr in obj.ani.st ){ obj.ani.a[attr] = (obj.ani.dis[attr] - obj.ani.d[attr])*factor[attr]; obj.ani.v[attr] += obj.ani.a[attr]; obj.ani.v[attr] *= 0.8; // obj.ani.v[attr] = obj.ani.v[attr] > 0 ? Math.ceil(obj.ani.v[attr]) : Math.floor(obj.ani.v[attr]); obj.ani.d[attr] += obj.ani.v[attr]; obj.ani.res[attr] = obj.ani.s0[attr] + obj.ani.d[attr]; if( Math.abs(obj.ani.v[attr]) <= 2 && Math.abs(obj.ani.dis[attr] - obj.ani.d[attr]) <= 2 ){ factor[attr] = 0; obj.ani.v[attr]=0; obj.ani.res[attr] = obj.ani.st[attr]; } if( obj.ani.t > obj.ani.total ){ for( attr in obj.ani.res ) obj.ani.res[attr] = obj.ani.st[attr]; clearInterval(obj.ani.timer); obj.aniOver = true; break; } } move.css(obj, obj.ani.res); if( obj.aniOver ){ obj.ani.st = {}; obj.ani.res = {}; if( typeof fn === "function" ) fn.call(obj); } }, obj.ani.interval); } }
为了少传参数, 所以我用了命名空间来管理运动库, 如果想调用碰撞动画, 只需这样实现:
move.collision(obj, {left:500, top:300}, 1000, function(){ alert("动画完成"); });
其他几种动画调用方法同理, 但注意elastic不需要传入时间
为什么要在obj下声明一个ani对象?
由于不是走的动画队列, 所以动画正在进行时如果施加新动画, 之前动画就会停止, 比如设置left从100px跑到600px, 在右移动的过程中如果马上施加一个top从当前值跑到500px的动画, 如果所有参数(初速度, 目标值, 时间等)放到函数中作为局部变量, 每次都会设置新的, 之前的目标值则就消失了, 因此将这些属性放到dom节点对象下面的ani对象中, 方便管理.
这个动画库原理还是非常简单的, 有问题或不足的地方欢迎指正, 源码下载: move.js, demo演示