JQuery动画可以实现非常多的效果,并且支持扩展动画效果,其中 http://easings.net/ 在基于JQuery上作了非常有用的动画扩展,尤其在一些曲线或抛物线上没有这些公式很难做出理想的动画来。

JQuery动画的底层实现核心思路是把整个区间分割成n个时间段,按时间动画关系函数来计算出当时所在移动(变化)的区间比率值。 这是一个easings自由掉落动画曲线,横向为时间,纵向为移动(变化)区间,在不同的时间里计算出对应的移动(变化)区间比例值即可实现对应的动画效果。 下面来看看底层部分代码:

动画入口代码:

jQuery.fn.extend({
	fadeTo: function( speed, to, easing, callback ) {

		// show any hidden elements after setting opacity to 0
		return this.filter( isHidden ).css( "opacity", 0 ).show()

			// animate to the value specified
			.end().animate({ opacity: to }, speed, easing, callback );
	},
	animate: function( prop, speed, easing, callback ) {
		var empty = jQuery.isEmptyObject( prop ),
			optall = jQuery.speed( speed, easing, callback ),
			doAnimation = function() {
				// Operate on a copy of prop so per-property easing won't be lost
				var anim = Animation( this, jQuery.extend( {}, prop ), optall );

				// Empty animations resolve immediately
				if ( empty ) {
					anim.stop( true );
				}
			};

		return empty || optall.queue === false ?
			this.each( doAnimation ) :
			this.queue( optall.queue, doAnimation );
	},
	stop: function( type, clearQueue, gotoEnd ) {
		var stopQueue = function( hooks ) {
			var stop = hooks.stop;
			delete hooks.stop;
			stop( gotoEnd );
		};

		if ( typeof type !== "string" ) {
			gotoEnd = clearQueue;
			clearQueue = type;
			type = undefined;
		}
		if ( clearQueue && type !== false ) {
			this.queue( type || "fx", [] );
		}

		return this.each(function() {
			var dequeue = true,
				index = type != null && type + "queueHooks",
				timers = jQuery.timers,
				data = jQuery._data( this );

			if ( index ) {
				if ( data[ index ] && data[ index ].stop ) {
					stopQueue( data[ index ] );
				}
			} else {
				for ( index in data ) {
					if ( data[ index ] && data[ index ].stop && rrun.test( index ) ) {
						stopQueue( data[ index ] );
					}
				}
			}

			for ( index = timers.length; index--; ) {
				if ( timers[ index ].elem === this && (type == null || timers[ index ].queue === type) ) {
					timers[ index ].anim.stop( gotoEnd );
					dequeue = false;
					timers.splice( index, 1 );
				}
			}

			// start the next in the queue if the last step wasn't forced
			// timers currently will call their complete callbacks, which will dequeue
			// but only if they were gotoEnd
			if ( dequeue || !gotoEnd ) {
				jQuery.dequeue( this, type );
			}
		});
	}
});

可以看到 animate 方法是基于 Animation 对象实现的,但在创建 Animation 对象时需要一个 jQuery.speed( speed, easing, callback ) 返回值,这个返回值(即是动画的时间与区间关系函数),在做动画扩展时也是扩展这个返回值的种类。下面再来看看 jQuery.speed做了什么:

jQuery.speed = function( speed, easing, fn ) {
	var opt = speed && typeof speed === "object" ? jQuery.extend( {}, speed ) : {
		complete: fn || !fn && easing ||
			jQuery.isFunction( speed ) && speed,
		duration: speed,
		easing: fn && easing || easing && !jQuery.isFunction( easing ) && easing
	};

	opt.duration = jQuery.fx.off ? 0 : typeof opt.duration === "number" ? opt.duration :
		opt.duration in jQuery.fx.speeds ? jQuery.fx.speeds[ opt.duration ] : jQuery.fx.speeds._default;

	// normalize opt.queue - true/undefined/null -> "fx"
	if ( opt.queue == null || opt.queue === true ) {
		opt.queue = "fx";
	}

	// Queueing
	opt.old = opt.complete;

	opt.complete = function() {
		if ( jQuery.isFunction( opt.old ) ) {
			opt.old.call( this );
		}

		if ( opt.queue ) {
			jQuery.dequeue( this, opt.queue );
		}
	};

	return opt;
};

jQuery.easing = {
	linear: function( p ) {
		return p;
	},
	swing: function( p ) {
		return 0.5 - Math.cos( p*Math.PI ) / 2;
	}
};

上面的代码中做了一些初始化处理,其中也包含了一些默认的数据,包括默认支持动画,以及默认动画时长,我们在调用动画时就可以在对应的参数上使用这些键名如:$('div').animate({top:'100px'}, 'slow'); 在 jQuery.speed 处理中会提取 opt.duration 区间值,这个值就是时长以毫秒为单位,常规默认值如下,如果不指定动画时长则默认为400毫秒。即在调用动画时可以写 $('div').animate({top:'100px'});

jQuery.fx.speeds = {
	slow: 600,
	fast: 200,
	// Default speed
	_default: 400
};

然后就是 Animation 对象,这个对象只是一个对jQuery.Tween对象的包装,以及动画定时器启停处理,所以就不贴代码了,直接看jQuery.Tween代码

function Tween( elem, options, prop, end, easing ) {
	return new Tween.prototype.init( elem, options, prop, end, easing );
}
jQuery.Tween = Tween;

Tween.prototype = {
	constructor: Tween,
	init: function( elem, options, prop, end, easing, unit ) {
		this.elem = elem;
		this.prop = prop;
		this.easing = easing || "swing";
		this.options = options;
		this.start = this.now = this.cur();
		this.end = end;
		this.unit = unit || ( jQuery.cssNumber[ prop ] ? "" : "px" );
	},
	cur: function() {
		var hooks = Tween.propHooks[ this.prop ];

		return hooks && hooks.get ?
			hooks.get( this ) :
			Tween.propHooks._default.get( this );
	},
	run: function( percent ) {
		var eased,
			hooks = Tween.propHooks[ this.prop ];

		if ( this.options.duration ) {
			// 动画函数调用
			this.pos = eased = jQuery.easing[ this.easing ](
				percent, this.options.duration * percent, 0, 1, this.options.duration
			);
		} else {
			this.pos = eased = percent;
		}
		this.now = ( this.end - this.start ) * eased + this.start;

		if ( this.options.step ) {
			this.options.step.call( this.elem, this.now, this );
		}

		if ( hooks && hooks.set ) {
			hooks.set( this );
		} else {
			Tween.propHooks._default.set( this );
		}
		return this;
	}
};

Tween.prototype.init.prototype = Tween.prototype;

在这段代码中是最终调用动画函数(代码中已经添加注释),这个动画调用函数也是我们扩展时需要遵循的,下面来说下扩展函数的参数:

jQuery.easing[ this.easing ](percent, this.options.duration * percent, 0, 1, this.options.duration)

从参数名上就可以定义一些意思: percent 当前动画完成时长占比占比值(0% ~ 100%)当然这里是以小数来表示如 0.6 this.options.duration * percent 当前动画完成时长 0 返回最小值 1 返回最大值 this.options.duration 动画总时长 需要说明下,这个函数不需要元素移动(变化)的样式值,只需要动画的时间进度,通过时间进度得出一个返回最大与最小范围内的值通过这个值乘以移动(变化的总差值)可以计算出本次的移动变化位置,从而实现动画。注意当 percent 超过 100% 时返回值会随着超过返回最大值。

那么在扩展动画时应该是怎么样处理?

jQuery.extend(jQuery.easing, {
     '动画名' : function (percent, present, minReturn, maxReturn, duration){
						 return  处理函数公式 ;
		 },
		 ...
})

调用方式:

 $('div').animate({top:'100px'}, 1000, '动画名');

到这里可以说如果我们不在基于JQuery上执行一些JQuery的扩展动画函数时就可以按照上面的参数说明来做,当前在做时一定要注意元素样式单位值(最好保证一致性),还有需要注意元素的style属性值与css文件的样式值获取方法不一样,这里就不一一说明。

下面给出一个自由掉落原生使用代码,动画函数取自easings,其它函数类似方式使用

    (function () {
        //动画函数
        function easeOutBounce(n, e, t, a, i) {
            return(e /= i) < 1 / 2.75 ? a * (7.5625 * e * e) + t : e < 2 / 2.75 ? a * (7.5625 * (e -= 1.5 / 2.75) * e + .75) + t : e < 2.5 / 2.75 ? a * (7.5625 * (e -= 2.25 / 2.75) * e + .9375) + t : a * (7.5625 * (e -= 2.625 / 2.75) * e + .984375) + t;
        }
        var percent = 0, duration = 1000, t, div = document.createElement('div'), diff = 600;
        div.style.width = '10px';
        div.style.height = '10px';
        div.style.position = 'absolute';
        div.style.top = '0px';
        div.style.left = '50%';
        div.style.backgroundColor = 'red';
        t = setInterval(function () {
            var _percent = percent / 100,
                    per = easeOutBounce(_percent, duration * _percent, 0, 1, duration);
            div.style.top = Math.round(diff * per) + 'px';
            percent += 1;
            if (percent > 100) {
                clearInterval(t);
            }
        }, 20);
        document.body.appendChild(div);
        console.info(document.body);
    })();