GSAP(GreenSocks Animation Platform)是一个性能较好的前端动画库。最近在写一个前端SVG动画编辑器时选择了它作为底层的动画库。为了减少踩坑,我大致浏览了它的源代码,这篇文章主要是对我的理解进行记录。
我会先简单介绍一下这个动画库的API,再介绍它的插件机制,最后会从一个用例出发跟踪其运行机制。
1. API
GSAP一共有四个用来创建动画片段的api。TweenLite, TweenMax, TimelineLite和TimelineMax。TweenLite和TweenMax用于创建单个动画片段。TimelineLite和TimelineMax用于创建多个动画片段的组合。我会忽略Timeline API,只介绍Tween API.
TweenMax和TweenLite的区别主要有两点。一是TweenMax引入了许多实用插件比如CSSPlugin,BezierPlugin等,二是TweenMax继承了TweenLite并提供了一些额外的API比如repeat(), yoyo()等。以下是少部分TweenMax和TweenLite共有的API。
TweenMax.to
TweenMax.to(target:Object, duration:Number, vars:Object ) : TweenMax
TweenMax.to是一个用来创建TweenMax的静态方法。当它被创建出来时,会在指定时间范围内将目标对象的给定属性从当前值变化到指定的值。动画是直接播放的。
例子:
var tween = TweenMax.to(obj, 1, {x:100});
上面这个例子中,obj的x属性会从当前值过渡到100,整个动画持续1秒,TweenMax实例会存放在变量tween之中。
2. TweenMax.fromTo
TweenMax.fromTo( target:Object, duration:Number, fromVars:Object, toVars:Object ) : TweenMax
该方法和TweenMax.to类似,区别在于动画属性的起始值为给定的fromVars
3. play
.play( from:*, suppressEvents:Boolean ) : *
TweenMax实例的继续播放动画方法,from为可选参数,表示播放的起始时间。 suppressEvent为false时将不会触发每帧更新动画时的回调函数,默认值为true
4. pause
.pause( atTime:*, suppressEvents:Boolean ) : *
TweenMax实例的暂停动画方法,atTime为可选参数,表示暂停的时间点。
5. seek
.seek( time:*, suppressEvents:Boolean ) : *
将动画跳跃至给定的时间点, 无论当前动画是暂停还是播放。
以上是常用API,其余API不作介绍。
2.GSAP插件
GSAP使用不同的插件来处理不同类型的属性的动画,比如AttrPlugin会负责DOM节点属性以及SVG节点属性的更新,而CSS属性则会被CSSPlugin处理。
在介绍插件的加载和使用机制之前,我们先来看一下GSAP的模块机制(源代码中是称为class,但是我觉得还是称其为模块比较好)。GSAP有一套自己的模块加载机制,这个模块机制和Angularjs的模块机制类似,每个模块有自己的名字和依赖的模块的名字。在初始化一个模块之前,GSAP会去查找当前模块依赖的模块是否初始化,如果所有的被依赖模块都已经初始化,GSAP会初始化当前模块。如果有任意一个被依赖模块还没有加载,GSAP会创建一个空模块作为它的实例,并且将当前当前的模块放在被依赖模块的子模块集合中,当这个被依赖模块被加载后它会通知所有依赖它的子模块。
下面的代码是CSSPlugin的模块定义部分:
_gsScope._gsDefine("plugins.CSSPlugin", ["plugins.TweenPlugin","TweenLite"], function(TweenPlugin, TweenLite) { // CSSPlugin 模块代码})
它声明CSSPlugin模块依赖TweenPlugin和TweenLite。
TweenPlugin是所有插件的基类。TweenPlugin和TweenLite是紧密结合在一起的,它会和TweenLite共享一些私有方法,其中最重要的是_addPropTween方法和setRatio方法。_addPropTween方法会在TweenLite创建的时候被调用,这个方法负责创建处理动画的最小单位pt(我也不知道这是什么英文单词的缩写)。pt对象的属性如下:
{ t, //动画的目标对象,有可能是原始目标也有可能是插件的实例 p, //动画作用的属性名或者方法名 s, //起始值 c, //change f. //表示p是不是函数 _next, //下一个pt _prev, //上一个pt pg, //是否是plugin n, // name pr, // 优先级 m // mod (function|0)}
TweenLite中会包含一个pt的链表,在每帧动画发生的时候会被依次调用。
setRatio负责在每帧动画发生时更新目标对象属性,它的参数是一个0-1范围内的浮点数,0表示动画的起始状态,1表示结束状态。setRatio方法会从第一个pt开始遍历并调用pt上的方法或者设置属性的值。
此外TweenPlugin还有一个静态方法activate(plugins),该方法负责把插件的构造函数存入插件列表中。
接下来我们就从一个例子中来看一下GSAP是如何工作的。
3. 使用GSAP创建颜色渐变动画
为了简单起见,下面这个例子是将一个svg circle元素的填充颜色从红色渐变成绿色。
<!DOCTYPE html><html><head> <title>颜色渐变</title> <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/2.1.3/TweenMax.min.js"></script></head><body> <svg width="100" height="100"> <circle id="test" fill="red" stroke="black" stroke-width="2" cx="50" cy="50" r="48"></circle> </svg> <script> var tween = TweenMax.fromTo(document.getElementById("test"), 5, {attr: {fill: "red"}}, {attr: {fill: "green"}}) tween.seek(1) </script></body></html>
实际效果如下:
接下来我会从上面这个例子出发来看一下GSAP是如何在每一帧动画发生的时候去设置circle的颜色。动画本身用的是浏览器的requireAnimationFrame接口,这个不作介绍。
首先是加载TweenMax。
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/2.1.3/TweenMax.min.js"></script>
在上文中提到TweenMax包含TweenLite,AttrPlugin和CSSPlugin。AttrPlugin负责调用对象的setAttribute方法去设置颜色,CSSPlugin在这个例子中会负责解析颜色。整个代码执行过程分为以下三步:
(1)GSAP代码的加载和初始化。
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/2.1.3/TweenMax.min.js"></script>
在这一步中会注册AttrPlugin和CSSPlugin。
const AttrPlugin =_gsScope._gsDefine.plugin({ propName: "attr", API: 2, version: "0.6.1", //called when the tween renders for the first time. This is where initial values should be recorded and any setup routines should run. init: function(target, value, tween, index) { var p, end; if (typeof(target.setAttribute) !== "function") { return false; } for (p in value) { end = value[p]; if (typeof(end) === "function") { end = end(index, target); } this._addTween(target, "setAttribute", target.getAttribute(p) + "", end + "", p, false, p); this._overwriteProps.push(p); } return true; }});
上面这段代码是注册AttrPlugin,其中 _gsScope._gsDefine.plugin是用以注册无模块依赖的Plugin的简便方法。
propName: "attr"
propName字段表示插件将会作用在动画起始值和结束值的"attr"属性上。
注意在API介绍中,用来创建动画片段的TweenMax.fromTo方法中接受的起始值和结束值都是JSON。
init字段存放创建插件实例时会被调用的初始化方法。
CSSPlugin代码量较多,只截取例子中会用到的部分。
_gsScope._gsDefine("plugins.CSSPlugin", ["plugins.TweenPlugin","TweenLite"], function(TweenPlugin, TweenLite) { var CSSPlugin; // CSSPlugin的定义部分 CSSPlugin.colorStringFilter = function(a) { var combined = a[0] + " " + a[1], toHSL; if (_colorExp.test(combined)) { toHSL = (combined.indexOf("hsl(") !== -1 || combined.indexOf("hsla(") !== -1); a[0] = _formatColors(a[0], toHSL); a[1] = _formatColors(a[1], toHSL); } _colorExp.lastIndex = 0; }; if (!TweenLite.defaultStringFilter) { TweenLite.defaultStringFilter = CSSPlugin.colorStringFilter; } // 其余定义部分 TweenPlugin.activate([CSSPlugin]); return CSSPlugin;})
CSSPlugin模块使用的是_gsScope._gsDefine,因为它有两个依赖模块。
在初始化模块的时候,CSSPlugin定义了一个colorStringFilter方法,该方法用来解析DOM属性和CSS属性中的颜色字符串比如"red","rgb(0,0,0)"等。CSSPlugin将会把TweenLite的defaultStringFilter设置为该方法。
(2)TweenMax的创建
var tween = TweenMax.fromTo(document.getElementById("test"), 5, {attr: {fill: "red"}}, {attr: {fill: "green"}})
代码段中的这一句表示对id是"text"的元素创建一个TweenMax,动画持续时间为5秒,给定起始值fill为red结束值fill为green。
在TweenMax对象的创建阶段,它会根据传入参数的属性来决定使用哪个Plugin。在注册AttrPlugin时,参数中有一个属性叫
propName: "attr"
这个参数决定了TweemMax会将动画参数对象中的attr部分交给AttrPlugin处理。
AttrPlugin在解析属性的时候看到fill的值为"red"(字符串),将会调用defaultStringFilter来解析这个值。
经过一系列初始化操作之后,最终返回的TweenMax大致会包含如下图所示的内容。
其中AttrPlugin的_firstPT会包含三个pt分别用来插值计算R,G,B三个通道的值,这里为了简化图片没有将它们都列出来。
(3)播放动画
tween.seek(1)
表示将动画跳跃至1秒钟的位置。TweenMax将会从_firstPT遍历调用所有的pt。
在处理pt时,TweenMax会根据pt上的f字段的值来判断到底是要调用pt绑定的对象(pt.t)的方法还是设置对象的字段的值。
第一步先从TweenMax自身的_firstPT出发,根据判断调用了绑定的对象(AttrPlugin)实例上的setRatio方法(位于AttrPlugin的原型链上)。该方法和TweenMax用来遍历pt的方法是同一个函数。
第二步也是根据AttrPlugin的_firstPT的属性继续递归调用setRatio函数。
第三步AttrPlugin._firstPT.t中包含了四个pt,setRatio会先遍历前三个函数计算出R,G,B通道的值,最后再调用_applyPT对象上的setAttribute方法设置svg ellipse元素的fill属性。
总结
GreenSocks动画库在处理动画时比较巧妙地用pt作为插值计算的最小单位把复杂的属性值比如颜色或者三维空间坐标拆分成了一串pt来分别计算不同通道的值。这样对复杂属性的操作就简化成了如何将其拆分为一串pt,减少了处理各种复杂属性的代码量(给看源代码的人造成了不少麻烦,滑稽)。以上就是我在看了源代码之后的笔记,希望以后调试起来更加得心应手。