第10章JavaScript的动画.docx
第10章JavaScript的动画.前娉示在Web中,“动画”这个词一向都属于网页动画王者Flasho但并不是说JaVaSCriPt就不能做出动画效果。虽然不借助于VML或者SVG,JaVaSCriPt就没有处理矢量图形的能力,但是对页面元素的控制力却是Flash不可比拟的。通过对页面元素的控制,使用JaVaSCriPt同样可以创造出接近于Flash的动画效果。本章就与读者一起,使用JaVaSCriPt来制作漂亮的动画效果。知识要点 动画基础 定时器 动画的实现 通用接口1.1.1 动画基础简单地说,动画可以理解为能动的图画。这确实很简单,简单到可以使用CSS技术轻松地来实现,在本章的例子中,将介绍一个使用正滤镜效果实现的动画。动画效果的原理就是利用人的视觉暂留,通过快速地播放连续的画面而产生场景中物体的运动效果。当然,连续的画面需要有连贯性一一比如一个完整的走路动作,按照规律一次变化一点点,这样才能产生平滑的运动效果。目前最流行的动画制作工具Flash就是利用这个方式来进行动画制作的。这里涉及到两个关键的概念一一帧和秒。帧可以理解为一幅画,或者电影胶片中的一张,每一帧都记录着一个瞬间的行为状态。比如第一帧,人物准备抬起左手;第二帧,人物左手抬起到了半空;第三帧,人物左手伸了出去。这三帧连续播放的效果就是一个完整的抬手伸出的动作。当然影响最终表现结果的还有一个重要因素,就是秒。通常,电影播放时,在一秒内会播放24幅画面。也就是说,人眼在一秒内看24帧就会觉得很正常,如果在一秒内播放的帧数过少,或者过多,就会造成画面过快或者过慢的效果了。当然这并不是绝对的,这与每帧之间的“间距”有关系,每帧移动的距离不同,那么效果也不同。帧和秒决定了最后动画播出的效果,这个由帧/秒决定的单位被称为FPS,也就是帧每秒(FramePerSecond)o虽然对于看电影来说,24FPS就够了,但是,目前在游戏中经常以高FPS为追求的目标,这是因为高FPS可以使整个过程表现得更细腻。1.1.2 时器说到JaVaSCriPt动画,在本书第6章中就介绍过如何通过改变元素样式来制作动画效果。因为在JaVaSCriPt中只能控制元素进行改变,而不能创造矢量图形,当然这不包括VML或者SVG。所以只能实现“元素动画”。提示在HTML5中,JaVaSCriPt可以实现矢量绘图,但HTML4.x中的元素动画同样重要。而实现元素动画只有两个要素,元素属性改变量以及完成改变量的时间,如图10.1所示。以元素移动来说,图10.1描述了在指定时间内完成指定移动量的过程。现在有了时间,也有了总量,问题是帧在哪里?这需要我们假定一个常量:FPS,如果使用24FPS,那么完成这个动画的总帧数为:总帧数(FS)=FPSX总时间(三)新的问题是:如何在总时间(三)内完成FS帧?对于JaVaSCriPt来说,这才是重点。答案是:我们需要一个定时器来周期性地驱动动画的改变,也就是帧的更新。(0.0)(200.200)图101元素的移动10.2.1 JavaScript中的定时器在本书第5章中,介绍了两种JavaScript支持的定时器函数:SetTimeout()允许延时执行函数setinterval()/允许在指定的间隔时间后重豆执行函数从函数定义上来看,SeUmerVaI更适合作为定时器来周期性的改变元素属性,看下面的例子:<style>Iblock(width:100px;height:100px;background:#aaa;position:absolute;top:10Opx;left:10Opx;)<style><divid=nblockn><div><script>每秒帧数varFPS=24;每帧间隔varSPF=1000/FPS;总时间3秒varperiod=3;/移动IoO像素vardistance=100;总帧数varframes=parselnt(FPS*period);每帧移动的距离varstep=distance/frames;varblock=ementByld(nblockn);定时器引用,用于结束动画vartimer;/动画处理函数functioncallback()(if(frames<=0)当总帧数到0时,停止动画clearinterval(timer);显示完成动画后的坐标alert(tLeft+tTop);)/变更移动量.left=tLeft+step+,px,;.top=tTop+step+,px,;frames;)timer=setinterval(callback,SPF);<script>定时器启动之后,灰色的方块bl。Ck就会沿着对角线的方向往右下角移动。虽然它很简单地只进行了位置的变动,但它确实是一个动画。除了使用SetInterVal来实现周期定时器外,还可以使用SemmeOUt来实现周期调度,看下面的例子:<script>每秒帧数varFPS=24;每帧间隔varSPF=1000/FPS;总时间3秒varperiod=3;移动IOo像素vardistance=100;总帧数varframes=parselnt(FPS*period);每帧移动的距离varstep=distance/frames;varblock=ementByld(blockn);/定时器引用,用于结束动画vartimer;/动画处理函数functioncallback()(if(frames<=0)当总帧数到0时,停止动画ClearTimeout(timer);显示完成动画后的坐标alert(tLeft+tTop);)变更移动量.left=tLeft+step+,px,;.top=tTop+step+,px,;frames-;SetTimeout(callback,SPF);)timer=SetTimeout(callback,SPF);<script>这是一个递归调用,看起来比Seumeival要复杂一些,但在实际的使用中,基本都是使用这种方式来处理动画的。为什么呢?答案稍后揭晓。提示在HTML5中,有了新的定时器接口requestAnimationFrame,该接口不需要使用者指定SPF,因为浏览器内部会为60帧的FPS而自动管理。该接口可以获得更低的CPU消耗、更少的电量消耗和更平滑的效果。10.2.2 帧和时间虽然这不是一本关于游戏开发的书籍,但我们仍然想把游戏开发中的一些经验介绍给读者。在本章的开头介绍了帧的概念,帧表示了1秒内定时器的循环次数,而我们使用帧进行动画的更新:在上面的例子中,动画在每帧更新一次block的位置,而在总帧数为0时结束动画。为什么没有说到时间呢?一般情况下,我们都会讲:我要看一个IO秒的广告,或者长达2个小时的动画,从来没人说:我想看一个216000帧(60FPS持续1小时)的动画。这就是我们想说的问题,时间。考虑这样一个场景:我们想在用户进入这个页面的时候显示一个欢迎动画,长度3秒(类似很多门户网站首页的强行广告)。如果动画刚开始播出1秒的时候,页面因为其他计算而导致性能下降,也许是卡住了10秒,那么10秒后动画是否应该消失?还是继续播完剩下的2秒?答案是消失。当然这还取决于程序的目的,是否需要一个与时间同步的动画。与时间同步的好处在于多人联网时,可以得到最小误差的相同体验:比如在一个抢购商品页面开放抢购之前播放了一些动画特效,之后开始抢购商品。如果动画长5秒,而某人从开始就卡了4秒,那他是该继续播完那剩下的1秒就开始抢购商品呢,还是继续等待5秒直到动画结束后才开始抢购商品?你一定已经知道答案了。下面来看看如何在代码中实现根据时间来进行的动画:<script>每秒帧数varFPS=24;每帧间隔varSPF=1000/FPS;/总时间3秒varperiod=3;/移动100像素vardistance=100;/总帧数varframes=parselnt(FPS*period);每帧移动的距离varstep=distance/frames;varblock=ementByld(,block");定时器引用,用于结束动画vartimer;动画处理函数functioncallback()(当前时间动画开始时间/如果因为某些原因,只执行了1帧时间就超过了3秒,那么动画同样会停止,因为不能影响后续的处理duration=+newDate()-StartTime;/+相当于.valuef();if(duration>=3000)(当时间超过3秒时,停止动画ClearTimeout(timer);显示完成动画后的坐标alert(tLeft+”:”+tTop);)/变更移动量.left=tLeft+step+,px,;.top=tTop+step+,px,;这句很重要SetTimeout(callback,SPF);)开启时间varStartTime=+newDate();/+相当于.valuef();执行时长varduration=0;timer=SetTimeout(callback,SPF);<script>使用时间来控制动画并不复杂,但是它可以保证你的整个程序流程不会因为图像显示的卡顿而影响后续的逻辑。提示一般来说,在画面需要频繁更新的游戏或者动画中,60FPS可以保证画面过渡看起来更平滑,但这不是必需的,有些类型只需要大于24的FPS就够了。高FPS会提高CPU和电量的消耗。当然在HTML5中,这种情况得到了改善。10.3动起来还不够只移动位置能算得上动画么?虽然从技术上来说,它算是动画,但从视觉上来说,它太简单了,简单到人们认为它不是动画。那现在我们就来完成更多的改变吧。10.3.1线性处理在开始编码之前,首先来构思一个动画场景,比如一个100*100像素的div在5秒的时间内渐变为透明、变小,随之消失。创建上述程序的操作步骤如下。(1)定义元素样式:<style>Iblock(width:100px;height:100px;background:#aaa;position:absolute;top:10Opx;left:100px;overflow:hidden;*初始化透明属性*/opacity:1;*CSS标准方式,1E7以上支持*/filter:Alpha(Opacity='100,);/*滤镜透明方式,工E6支持*/)<style><divid=,block,><div>为元素增加了跨平台的透明属性。(2)定义动画参数:帧每秒varFPS=24;每帧间隔varSPF=1000/FPS;/总时间3秒varperiod=3;移动IOo像素vardistance=100;修改尺寸,从100到0varsize=100;/透明度,从不透明100到透明0varopacity=100;/总帧数varframes=parselnt(FPS*period);/每帧移动距离vardisStep=distance/frames;/每帧缩小尺寸varsizestep=size/frames;每帧增加透明度varopaStep=opacity/frames;varblock=ementByld(,*block,*);获取运行时样式varOcurStyle=mputedStyle?mputedStyle(block,null):ntStyle;本例中的动画要改变3个属性,要分别计算每个属性的每帧改变量,对于透明属性,要通过运行时样式来获取。(3)启动定时器:定时器引用,用于结束动画vartimer;动画处理函数functioncallback()duration=+newDate()-StartTime;/+相当于.valuef();if(duration>=3000)/当时间超过3秒时,停止动画clearinterval(timer);if(tLeft!=200).left=,200px'if(tTop!=200).top=,2OOpx,;)移动位置.left=tLeft+disStep+,px,;.top=tTop+disStep+,px,;缩小尺寸.width=tWidth-sizestep+,px,;.height=tHeight-sizestep+,px,;透明渐变.cssText+=',+,opacity:'+(ocurStyle(,opacity')-opaStep/100)+,;filter:Alpha(Opacity=,+(ocurStyle'opacity')*100-opaStep)+,),;这句很重要SetTimeout(callback,SPF);)开启时间varStartTime=+newDate();/+相当于.valuef();执行时长varduration=0;timer=setinterval(callback,SPF);在设置元素透明属性的时候,使用了StyIe对象的CSSTeXt属性,该属性接收CSS定义字符串作为参数,直接对样式进行修改。对于需要动态设置元素样式的操作来说,这种方式更加简便。虽然增加了更多的改变属性,但是每个属性的改变都是线性的,如何实现加速、减速或者弹簧效果的非线性改变呢?答案即将揭晓。10.2.3 非线性处理如果只需要加速或者减速效果,可以很容易地实现一个加/减速度变量,在每帧更新元素位置之前,先更新速度本身,这样就是一个简单的二次曲线效果了。不过对于更复杂的效果,可以选择使用通用的方程函数,比如下面这个例子:/定时器引用,用于结束动画vartimer;/动画处理函数functioncallback()duration=+newDate()-StartTime;/+相当于.valuef();if(duration>=3000)/当时间超过3秒时,停止动画clearinterval(timer);if(tLeft!=200).left=,200px'if(tTop!=200).top=,2OOpx,;)移动位置vardeltaDis=quadin(duration,0,100z3000);.left=tLeft+deltaDis+,px,;.top=tTop+deltaDis+,px'缩小尺寸vardeltasize=quadin(duration,100,-100,3000);.width=tWidth-deltasize+,px,;.height=tHeight-deltaSize+,px'透明渐变vardeltaAlpha=quadin(duration,100,-100,3000);.cssText+=',+,opacity:'+(ocurStyle,opacity')-deltaAlpha/100)+'filter:Alpha(Opacity=1+(ocurStyle(,opacity'*100-deltaAlpha)+,),;这句很重要SetTimeout(callback,SPF);)渐变公式functionquadin(t,b,c,d)(returnc*(t=d)*t+b;)开启时间varStartTime=+newDate();/+相当于.valuef();执行时长varduration=0;timer=setinterval(callback,SPF);注意上面的例子,所有的变化量都是动态算出来的。我们不打算解释这个公式的数学含义,但还是要说一下它的参数。有没有注意到计算公式的第一个参数?没错,动画运行时长就是第一个参数,而最后一个参数则是动画的总时长,现在聪明的读者一定已经明白计算公式的原理了吧。提示10.4通用接口元素动画大量地应用在页面组件的过渡效果中,比如菜单的收缩效果也就是改变尺寸,菜单的弹簧效果也就是改变位置。在这些效果的实现中,有一个问题是,难道每次改变一个元素属性都要写一堆的控制代码吗?如果对多个元素进行动画控制,那代码岂不是要非常多。一个通用的容易扩展的接口就是问题的解决之道。一个理想的元素动画接口应该只需要指定动画完成所需要的时间、需要进行改变的属性、需要改变属性的目标值,然后动画就会自动出现了。不需要每次编写复杂的控制代码,也不需要每次计算不同属性的改变量。下面的接口便接近于我们正在寻找的方式:transform(obj,(Ieft:100,opacity:0.5,fontsize:90),1000)理想中的transform接口可以识别不同的属性并自动进行控制。下面就一起来分析如何实现这种接口: 如果只接收属性的目标值,那么如何获取属性目前的值?这样才能算出目标单位与源单位之间的差距。这个问题可以通过运行时样式来进行获取。 如何计算每个属性的每帧改变量? 知道总时间,知道帧数,知道每帧改变量。那唯一的问题就剩下编码了。创建上述程序的操作步骤如下。(1)解析参数中包含的属性以及属性值:functiontransform(obj,params,period)varFPS=24;varSPF=1000/FPS;开启时间varStartTime=+newDateO;/+相当于.valuef();执行时长varduration=0;总时间,默认100omSperiod=period1000;目标属性集合vardesbj=();当前属性集合varsrcbj=();定义当前值和目标值vardesValue,srcValue;运行时样式varOcurStyle=mputedStyle?mputedStyle(obj,null):ntStyle;因为参数ParamS中有可能定义了很多属性,所以使用属性集合来存放每个属性以及对应的值。(2)计算所有属性的每帧步长:/遍历params对象中的属性for(variinparams)(获取在运行时样式中的当前属性值SrcValue=OcurStyle(i);获取目标属性值desValue=params(i);如果运行时样式中有该属性,可能有不合法的属性名if(SrcValue)(/如果属性值为auto,改为OsrcValue=ce(autoi,O,);如果当前属性值不是数字或者为空,那么无法进行动画处理if(!(0-9)+i.test(srcValue)|(Of(,)!=-1)continue;把属性i的值分别存储desbj(i)=parseFloat(desValueIO);srcbj(i=parseFloat(srcValue0);获取每个属性的每帧改变量ParamStepObj(i)=(desbj(i)-srcbji)/frames;)在基于时间同步的动画处理中,我们可以预先计算出所有属性的每毫秒改变值,但我们需要为此付出大量内存,有两个选择:降低精度,只计算10亳秒级别的改变值,或者运行时计算。(3)动画核心控制:计算不同属性的每帧改变量varopacity;varnValue;vartimer=SetTimeout(function()(duration=+newDate()-StartTime;/+相当于.valuef();if(duration>=period)(当时间超过指定时长时,停止动画clearinterval(timer);)/动画处理核心for(variinparamStepObj)(if(/opacity/i.test(i)(如果元素没有透明样式,那么必须初始化一个if(i!=0)&&(!(i)(.cssText+=,/opacity:1;filter:Alpha(Opacity=100),;).cssText+='/opacity:,+(parseFloat(ocurStyle('opacity')+paramStepObji)+,;filter:Alpha(Opacity='+(parseFloat(ocurStyle('opacity')*100+paramStepObji)*100)+'),;else(try(i=(i)?parseFloat(i)+paramStepObj(i):srcbji)+paramStepObj(i);catch(el)(i=,Opx,;)这句很重要SetTimeout(callback,SPF);,SPF);通过比较属性名i的值为OPaCily,进入特定的透明控制逻辑。在处理一个元素的透明属性之前,这个元素必须已经被设置过透明属性,否则在样式中就无法获取到OPaCity属性的值,也就无法完成透明渐变效果。(4)调用接口:调用接口varblock=ementByld(,block,*);transform(block,left:300,top:500,opacity:0),1000);对于transform接口,基本上已经可以满足大部分的需求了,但还有一些特性需要完善,比如增加有效性验证、增加颜色的处理、增加相对参数值以及方法原型化等。10.5上机练习(1)创建一个可以交互的动画效果。(2)现在的动画接口都是指定绝对单位,比如:transform(block,left:300,top:500,opacity:0,period:1);这句就是移动block元素到(300,500)坐标的地方。尝试为接口增加相对单位,例如:transform(block,left:,-30,top:'+30,opacity:0,period:1);就是向左移动30像素,向下移动30像素。(3)为transform增加非线性处理机制。