spine与lottie-web对比

spine动画介绍
spine是什么
Spine 是一款针对游戏开发的 2D 骨骼动画编辑工具,提供高效简洁的工作流程,以创建游戏所需的动画。

spine出现的背景
传统的帧动画每帧都需要一张图片,会产生大量的资源。每新增一个动画都会大大增加游戏的磁盘空间和内存要求,流畅播放帧率则更甚。这不仅极大增加了美工的工作量,当必须缩减动画数量以符合大小限制时,也会对最终成品产生影响。

Spine动画优势
最小的体积: 传统的动画需要提供每一帧图片。而 Spine 动画只保存骨骼的动画数据,它所占用的空间非常小,并能为你的游戏提供独一无二的动画。
美术需求: Spine 动画需要的美术资源更少,能为您节省出更多的人力物力更好的投入到游戏开发中去。
流畅性: Spine 动画使用差值算法计算中间帧,这能让你的动画总是保持流畅的效果。
装备附件: 图片绑定在骨骼上来实现动画。如果你需要可以方便的更换角色的装备满足不同的需求。甚至改变角色的样貌来达到动画重用的效果。
混合: 动画之间可以进行混合。比如一个角色可以开枪射击,同时也可以走、跑、跳或者游泳。(动画直接可以平滑过渡)
程序动画: 可以通过代码控制骨骼,比如可以实现跟随鼠标的射击,注视敌人,或者上坡时的身体前倾等效果。

spine资源素材
.json文件或二进制.skel文件:包含所有骨架信息(二进制形式加载更快、gc更小)
.png文件:包含当前版本所有图片的集合,也可单一素材,可导出一张或多张
.atlas文件:包含打包的图集信息,记录素材图片在雪碧图上的位置信息特征,一个atlas文件可对应多个素材图片

spine资源结构

spine基础概念
骨架Skeleton:指代的是数据的集合,包含构成此骨架的所有骨骼、插槽、附件及其他信息。
骨骼bones:一个人物本身由多个关节的骨骼组成。除了根骨骼以外,每个骨骼都有对应的父骨骼,骨骼与骨骼之间的关系最终构造成类似树的结构。
插槽slot:一个骨骼bone下可能有多个slot插槽,每个slot插槽下可以放置一个附件实例。
插槽本身的存在有两个重要的意义,一个是灵活的控制渲染顺序,一个是分组同类附件。
一个插槽可以有多个附件,但一次只能看到一个。举个简单的栗子,图中手枪所在的位置的插槽是"武器"插槽,而该插槽可以放置不同的武器附件,例如"手枪"附件或"菜刀"附件。
附件attachment:slot插槽内当前渲染的附件实例,即真实上屏渲染的实物素材。
皮肤skin:skin可以看做是attachment的集合,或者可以认为是attachment的一个映射查询表,一个人物可以由多套skin,通过切换skin的方式去查询不同的附件映射表,便可以变相的实现人物的全身换装。

其他相关概念
关键帧:在编辑器中,动画是借助关键帧完成的,从开始到结束的过渡动画,由spine补间处理。
权重与网格:权重用于将网格顶点绑定到一个或多个骨骼。变换骨骼时,顶点也会随之变换。权重令网格能够随着操纵骨骼而自动变形,从而让原本复杂的网格变形动画变得与骨骼动画一样简单。
区域附件:普通的图片展示附件。
点附件:空间中的一个点和旋转,相比骨头的优势可以为不同的皮肤设置更改位置和旋转,例如不同的枪从不同的位置射击。
网格附件:支持在图片内设置多边形,之后可操纵多边形的顶点,以有效的方式让图片弯曲和变形。
边界框附件:附加到骨骼上的多边形,骨骼变化的时候也会随之变形,可用于撞击检测,创建物理主体等。
剪裁附件:剪裁功能让你可以定义一个多边形区域,与边界框附件类似,它会屏蔽绘制顺序中的其他插槽。
路径附件:用于设置路径。
IK约束:反向动力学约束 子骨头终点固定的场景。
变换约束:变换约束指的是将对骨骼的世界旋转、移动缩放等复制到多个骨骼上。
路径约束:使用路径来调整骨骼变换,骨骼可以沿着路径,也可以调整旋转以指向路径。

spine渲染流程

1、atlasAttachmentLoader实例负责atlas文件的解析,解析后与素材建立“关联关系”
2、SkeletonData实例对骨骼数据、插槽数据做预处理
3、Skeleton实例渲染层上屏渲染的真实直接数据源,渲染层将读取Skeleton实例上的插槽信息,渲染对应的附件
(updateWorldTransform触发骨骼位置的计算、setToSetUpPose更新实例)

Lottie动画介绍
Lottie介绍
Lottie一个复杂帧动画的解决方案,提供了一套从设计师使用AE到各端开发者实现动画的工具流。在设计师通过 AE 完成动画后,使用 AE 插件 Bodymovin导出动画数据,前端直接引用lottie-web库,默认渲染方式是svg,原理就是用JS操作svg API。前端完全不需要关心动画的过程,json文件里有每一帧动画的信息,而库会帮我们执行每一帧。

为什么使用Lottie
Lottie之前复杂动画的实现方式
1、GIF:占用空间大,有些动画显示效果不佳,需要适配分辨率,还原度低
2、帧动画:占用空间大,适配问题
3、组合式动画:通过大量代码实现复杂动画
Lottie解决的问题
1、支持跨平台,开发成本较低,一套Lottie动画可以在Android/IOS/Web多端使用
2、还原度高、兼容性好
3、占用空间小,多分辨率适配

Lottie数据结构

{
    "fr": 60, // 帧率
    "ip": 0,  // 起始关键帧
    "op": 30, // 结束关键帧
    "w": 1280,// 视图宽
    "h": 720, // 视图高
    "assets": [ ], // 资源集合 
    "layers": [{   // 图层
        "ty": 0,   // 图层类型。
        "refId": "comp_0", // 引用的资源,图片/预合成层
        "ks": {},  // 变换 下面单独介绍
        layer: [], // 该图层包含的子图层
        shapes: [],// 形状图层
        "w": 1334,
        "h": 750,
        "bm": 0  // 混合模式
    }], // 图层集合
    "masker": [] // 蒙层集合
}

assets 资源集合
一个数组,资源信息包含的是矢量图信息,如形状,大小等等,也包含位图;还可能是预合成层,即对已存在的某些图层进行分组,把它们放置到新的合成中,作为新的一个资源对象,这儿layers的对象结构是上面一级属性中的layers图层集合是一样的
layers 图层信息
layers对象也是一个数组,数组中的每个元素对应一个图层,图层信息包括的图层的位置,大小,形状,起始关键帧,结束关键帧等,一个个图层叠加起来构成最终的动画效果


"ks": {
     "ddd": 0,  // 是否为3d
     "ind": 16, // layer唯一Id
     "ty": 2,   // 图层类型
     "nm": "右手耶",// 图层名称
     "parent": 19, // 父图层,使用index标识
     "refId": "image_14", // 引用的资源 图片/预合成层
     "sr": 1,    // 时间拉伸
     "ao": 0,    // 沿路径运动时是不是头朝正
    "ip": 2.16, // 该图层开始关键帧
    "op": 54,   // 该图层结束关键帧
    "st": 0.72, // 开始时间
    "bm": 0,    //混合模式
    "ks": {
        "o": {}, // 透明度
        "r": {}, // 旋转
        "p": {
            'a': 1,
            'k':[{
                't': 0, // 带有t的元素, 即为帧动画
                's': [300, 700, 0]
                },{
                't': 49, // 关键帧为49时 位置信息变为(300,1800,0)
                's': [300, 1800, 0]
                }
            ],
            'ix': 2
        }, // 位置
        "a": {}, // 锚点
        "s": {}  // 缩放            
 }

详细资源结构请参考:https://www.processon.com/view/link/5c2ece6ae4b08a768398b06d

Lottie渲染流程

部分代码解析
// loadAnimation的方法: 加载、解析json、播放动画

function loadAnimation(params) {
    var animItem = new AnimationItem(); // 创建动画对象
    setupAnimation(animItem, null);// 主要是添加一些时间监听函数
    animItem.setParams(params);    // 根据输入的参数和json数据,渲染成相应的动画
    return animItem;
}

// 选择渲染器

AnimationItem.prototype.setParams = function(params) {
    var animType = params.animType ? params.animType : params.renderer ? params.renderer : 'svg';
    switch(animType){
        case 'canvas':
            this.renderer = new CanvasRenderer(this, params.rendererSettings);
            break;
        case 'svg':
            this.renderer = new SVGRenderer(this, params.rendererSettings);
            break;
        default:
            this.renderer = new HybridRenderer(this, params.rendererSettings);
            break;
    }
    // 初始化一系列参数
    // assetLoader 加载数据
    this.preloadImages();// 预加载 图片资源
    this.loadSegments(); 
    this.updaFrameModifier();
    this.waitForFontsLoaded();
}

// 图层创建所有的元素

BaseRenderer.prototype.buildAllItems = function(){
    var i, len = this.layers.length;
    for(i=0;i<len;i+=1){
        this.buildItem(i);
    }
};
SVGRenderer.prototype.buildItem  = function(pos){
    var elements = this.elements;
    var element = this.createItem(this.layers[pos]);

    elements[pos] = element; // 将元素添加到svg中
    this.appendElementInPos(element,pos);
};

// 根据图层类型,创建相应的svg元素类的实例

BaseRenderer.prototype.createItem = function(layer){
    switch(layer.ty){ 
        case 2:
            return this.createImage(layer);
        case 0:
            return this.createComp(layer);
        case 1:
            return this.createSolid(layer);
        case 3:
            return this.createNull(layer);
        case 4:
            return this.createShape(layer);
        case 5:
            return this.createText(layer);
        case 13:
            return this.createCamera(layer);
    }
    return this.createNull(layer);
};

Lottie常用API


lottie的主要方法
lottie.play()  // 播放动画。
lottie.stop()  // 停止动画。 动画关闭
lottie.pause() // 暂停动画。 动画停止在暂停前一帧
lottie.setSpeed(speed) // 设置播放速度,参数 speed 为 Number ,1为正常速度。
lottie.goToAndStop(value, isFrame) // 跳到某一帧并暂停播放。第一个参数是 Number 。第二个参数是 Boolean,设置true则表明第一个参数代表的是帧数,false代表第一个参数为时间值(单位毫秒),默认 false。
lottie.goToAndPlay(value, isFrame) // 跳到某一帧并播放。
lottie.setDirection(direction) // 设置播放方向。1 为正着播,-1反着播。
lottie.playSegments(segments, forceFlag) // 播放某一片段。第一个参数为一维数组或多维数组,每个数组包含两个值(开始帧,结束帧),第二个参数是一个 Boolean ,决定是否立即强制播放该片段。
lottie.destroy()     // 注销动画。
lottie.setQuality() // 播放质量,默认 high,改变贝塞尔平滑度从而影响帧之间的补间动画片段数量。
lottie.loadAnimation({
  container: element, // 容器节点
  renderer: 'svg', // 渲染模式 默认svg
  loop: true,      // 是否循环播放
  autoplay: true,  // 是否自动播放
  assetsPath: ''   // 图片资源路径
  initialSegment: [12, 40] // 初始化动画帧片段 (默认显示片段首帧)
  animationData:'amim.json', // JSON数据,与path互斥
  path: 'data.json'        // JSON文件路径
  rendererSettings: {
    context: canvasContext;  // 指定canvasContext
    clearCanvas: boolean;    // 是否先清除canvas画布,canvas模式独占,默认false。
    progressiveLoad: boolean;// 是否开启渐进式加载,只有在需要的时候才加载dom元素,在有大量动画的时候会提升初始化性能,但动画显示可能有一些延迟,svg模式独占,默认为false。
    hideOnTransparent: boolean;// 当元素opacity为0时隐藏元素,svg模式独占,默认为true。
    className: string;      // 容器追加class,默认为''
  }
})

lottie的事件监听

complete      // 动画播放结束时触发(循环播放不会触发)
loopComplete  // 进入下一个循环时触发
enterFrame    // 每进入一帧触发一次
segmentStart  // 进入片段播放时触发
config_ready  // 初始化配置完成时触发
data_ready    // 在所有的segments被加载完毕后触发 image资源的加载前触发
loaded_images // 所有图片资源加载完毕的时候会触发
DOMLoaded  // dom 元素加载完成时触发,这个是比较可靠的可以替换data_ready的事
destroy   // 注销动画时触发

Lottie性能优化
1、降帧
2、资源压缩、资源缓存预加载
3、setSubframe 按照ae设置帧渲染
4、setQuality 缩减形变补间动画贝塞尔平滑度
5、多段动画资源合并

lottie动画播放基本原理
1、先将动画实例化为AnimationItem
2、requestAnimationFrame每次触发时,调用advanceTime() -> setCurrentRawFrameValue() -> gotoFrame() 计算出要更新的属性值
3、调用 renderer 的 renderFrame() 来更新界面
Lottie的setSubframe()解密

AnimationItem.prototype.setSubframe = function (flag) {
  this.isSubframeEnabled = !!flag;
};

AnimationItem.prototype.gotoFrame = function () {
  this.currentFrame = this.isSubframeEnabled ? this.currentRawFrame : ~~this.currentRawFrame;

  if (this.timeCompleted !== this.totalFrames && this.currentFrame > this.timeCompleted) {
    this.currentFrame = this.timeCompleted;
  }
  this.trigger('enterFrame');
  this.renderFrame();
};

currentFrame 是指要计算播放的当前帧数,关闭了 subFrame 时,会对其取整(~~),这样就不会存在小数位的帧数,那么自然就按照原始的帧数(帧率*总时间)来播放

Lottie的setQuality()解密
setQuality实际上是控制补间动画的数量,也可以理解为会影响贝塞尔平滑度

var buildBezierData = (function () {
    var storedData = {};
    return function (pt1, pt2, pt3, pt4) {
      var bezierName = (pt1[0] + '_' + pt1[1] + '_' + pt2[0] + '_' + pt2[1] + '_' + pt3[0] + '_' + pt3[1] + '_' + pt4[0] + '_' + pt4[1]).replace(/\./g, 'p');
      if (!storedData[bezierName]) {
        var curveSegments = defaultCurveSegments;
        var k;
        var i;
        var len;
        var ptCoord;
        var perc;
        var addedLength = 0;
        var ptDistance;
        var point;
        var lastPoint = null;
        if (pt1.length === 2 && (pt1[0] !== pt2[0] || pt1[1] !== pt2[1]) && pointOnLine2D(pt1[0], pt1[1], pt2[0], pt2[1], pt1[0] + pt3[0], pt1[1] + pt3[1]) && pointOnLine2D(pt1[0], pt1[1], pt2[0], pt2[1], pt2[0] + pt4[0], pt2[1] + pt4[1])) {
          curveSegments = 2;
        }
        var bezierData = new BezierData(curveSegments);
        len = pt3.length;
        for (k = 0; k < curveSegments; k += 1) {
          point = createSizedArray(len);
          perc = k / (curveSegments - 1);
          ptDistance = 0;
          for (i = 0; i < len; i += 1) {
            ptCoord = bmPow(1 - perc, 3) * pt1[i] + 3 * bmPow(1 - perc, 2) * perc * (pt1[i] + pt3[i]) + 3 * (1 - perc) * bmPow(perc, 2) * (pt2[i] + pt4[i]) + bmPow(perc, 3) * pt2[i];
            point[i] = ptCoord;
            if (lastPoint !== null) {
              ptDistance += bmPow(point[i] - lastPoint[i], 2);
            }
          }
          ptDistance = bmSqrt(ptDistance);
          addedLength += ptDistance;
          bezierData.points[k] = new PointData(ptDistance, point);
          lastPoint = point;
        }
        bezierData.segmentLength = addedLength;
        storedData[bezierName] = bezierData;
      }
      return storedData[bezierName];
    };
  }());
posted @ 2022-04-16 00:44  木水枫  阅读(1617)  评论(3编辑  收藏  举报