# 动画系统

xr-frame中所有的动画都是通过动画系统AnimationSystem统一管理的,在每帧的同一个时机统一更新。动画系统自身没有什么逻辑,所有的逻辑都是在动画组件Animator和动画实现Animation中的。

# 动画实现

动画实现的基础是基类Animation,所有的动画都必须派生于它去实现必要的方法。让我们一一个例子来看看:

import XrFrame from 'XrFrame';
const xrFrameSystem = wx.getXrFrameSystem();

// 定制动画接受的初始化数据接口
interface IXrTeamCameraAnimtionData {
  targets: {
    hikari: XrFrame.Vector3;
    roam: XrFrame.Vector3;
    xinyi: XrFrame.Vector3;
    final: XrFrame.Vector3;
  },
}

// 定制动画接受的播放额外配置接口
interface IXrTeamCameraAnimationOptions {

}

// 定制动画的实现
class XrTeamCameraAnimation extends xrFrameSystem.Animation<
  IXrTeamCameraAnimtionData,
  IXrTeamCameraAnimationOptions
> {
  private _camera: XrFrame.Transform | undefined;
  private _target: XrFrame.Transform | undefined;
  private _targets: IXrTeamCameraAnimtionData['targets'] | undefined;
  private _startC: XrFrame.Vector3 = new xrFrameSystem.Vector3();
  private _endC: XrFrame.Vector3 = new xrFrameSystem.Vector3();
  private _startT: XrFrame.Vector3 = new xrFrameSystem.Vector3();
  private _endT: XrFrame.Vector3 = new xrFrameSystem.Vector3();

  // 动画初始化时会被执行,传入初始数据
  // 必须设置`this.clipNames`,提供给动画组件必要的信息
  public onInit(data: IXrTeamCameraAnimtionData) {
    this._targets = data.targets;
    this.clipNames = ['hikari', 'roam', 'xinyi'];
  }

  // 动画被播放时会被执行,必须返回片段时长`duration`
  // 剩下三个返回参数是可选的,详见API文档
  public onPlay(el: XrFrame.Element, clipName: string, options: IXrTeamCameraAnimationOptions): {
    duration: number,
    loop?: number,
    delay?: number,
    direction?: XrFrame.TDirection
  } {
    this._camera = this._camera || el.getComponent(xrFrameSystem.Transform);
    this._target = el.getComponent(xrFrameSystem.Camera).target;
    this._startT.set(this._target.position);
    this._endT.setValue(this._targets![clipName].x, this._targets![clipName].y, this._targets![clipName].z);
    this._startC.set(this._camera.position);
    this._endC.set(this._endT);
    this._endC.z += 2;

    return {duration: 3};
  }

  // 动画播放进度更新是会被执行,`progress`的范围是`0~1`
  // `el`参数是指这个动画目前作用于哪个元素,因为动画和元素、组件并非总是一一对应的
  public onUpdate(el: XrFrame.Element, progress: number, reverse: boolean) {
    progress = xrFrameSystem.noneParamsEaseFuncs['ease-in-out'](progress);
    this._startT?.lerp(this._endT, progress, this._target?.position);
    this._startC?.lerp(this._endC, progress, this._camera?.position);
  }

  // 动画播放暂停时会被执行,暂停本身的逻辑是自动的
  public onPause(el: XrFrame.Element) {

  }

  // 动画从暂停中唤醒时会被执行
  public onResume(el: XrFrame.Element) {

  }

  // 动画停止时会被执行,包括播放结束和手动停止
  public onStop(el: XrFrame.Element) {

  }
}

这段代码中,我们定制了一个动画,它的几个生命周期来定义其是如何运作的。框架内置了两种动画帧动画gltf动画,但这里我们先不讨论它们,先看看在实现了一个动画后,如何去创建和操纵它,这也就引入了动画组件。

# 动画组件

动画组件Animator用于管理所挂载元素下所有的动画。

和其他组件一样,它也提供了在xml中和脚本控制两种方法。我们先看看脚本控制是怎么做的:

const animator = el.getComponent(xrFrameSystem.Animator);

// 添加一个动画,`clipMap`可选
animator.addAnimation(new XrTeamCameraAnimation(data), clipMap);

// 通过动画类直接创建一个动画,`clipMap`可选
const anim = animator.createAnimation(XrTeamCameraAnimation, data, clipMap);

// 移除一个动画
animator.removeAnimation(anim);

// 播放名为`name`的动画,可以同时播放多个
animator.play(name, options);

// 暂停名为`name`的动画,不填`name`则暂停所有
animator.pause(name);

// 唤醒名为`name`的动画,不填`name`则唤醒所有
animator.resume(name);

// 停止名为`name`的动画,不填`name`则停止所有
animator.stop(name);

// 将名为`name`的片段定格到某个进度
animator.pauseToFrame(name, progress);

这里要特别注意的有几个点:

  1. clipMap本质上是给开发者提供了一个从动画组件片段的名字到动画实例片段名字的映射,这一般在动画组件有多个动画实例、而动画实例中的片段有重名的情况下会很有用。如果不填,怎会默认使用动画实例的片段名来索引。
  2. options是播放时的参数,具体的定义可以参照API文档,要说明的是在play时提供的这个参数,会覆盖掉动画实例onPlay时返回的参数。

相较于脚本控制,大部分开发者在xml中用动画组件会比较常见。所有派生自XRElement元素的元素,都会拥有默认的动画属性映射:

<xr-node
  anim-keyframe="basic-anim"
  anim-clipmap="default:cube"
  anim-autoplay="clip:cube, speed:2"
></xr-node>

这三个属性对应于动画组件的三个数据,最后一个anim-autoplay指定了是否要默认播放以及默认播放的片段和参数,如果不写clip数据则会播放所有的片段。anim-clipmap则是给默认加载的动画指定一个片段映射,而这个默认的动画就是anim-keyframe指定的,它就是前面提到的内置的帧动画,这个可以在后面的章节看到详细说明。

除了anim-keyframe,其他两个参数可以作用于前面提到的gltf动画,也可以在相关章节查看。

# 事件

动画组件为元素提供了以下事件:

事件 参数 立即 wxml 时机
anim-stop 对象,其中name是片段名字 某个片段播放停止时