基本原理
概述
缓动的基本原理很简单,就是设置一个初值和终值,在每帧更新的时候根据特定的缓动函数算出对应的中间值并插值。
本工具类分为4个部分:缓动工具类、缓动辅助类、缓动核心类、缓动时间类。同时,本工具提供链式调用,方便进行多个缓动动作的连续执行。
-
缓动工具类负责管理缓动的调用,以及一个作用域所有的缓动。
-
缓动辅助类负责管理单个缓动的初始化,同时也是链式调用的核心类。
-
缓动核心类负责管理具体的缓动逻辑,所以缓动都是通过该类执行。
-
缓动时间类负责管理缓动的更新调用,本项目托管到Laya的更新脚本上。
由于本类在设计之初是在Laya引擎运行,因此小部分代码依托Laya存在,使用的时候需要根据具体项目进行修改。
使用方法
所有的操作均从缓动工具类调用,但首先在运行之前要执行TweenUtil.start()方法来初始化整个缓动工具的更新逻辑。
to和from方法表示两个不同的缓动类型,to为从当前状态到目标状态,from为从目标状态到当前状态。这两个方法都会返回一个 subTween 对象,我们可以以这个 subTween 对象进行链式调用,以点的方法重复调用to、from等方法。
例如:
let obj = { x:0 };
TweenUtil.to(obj,{
x:1
},100).to({
x:2
},100).to({
x:3
},200);
此处执行了三个缓动,obj的x值从0到1到2再到3。三个缓动按照顺序执行一次。
同时,本项目也提供了循环执行缓动的功能,只需要在最后调用 loop() 方法,就可以对 loop 之前的所有步骤重复执行。另外,本项目也支持设置循环次数,只需要如下调用即可:
let obj = { x:0 };
TweenUtil.to(obj,{
x:1
},100).to({
x:2
},100).to({
x:3
},200).loop();
let obj2 = { x:0 };
TweenUtil.to(obj2,{
x:1
},100).to({
x:2
},100).to({
x:3
},200).loop(3);
如果存在两个loop,若前面的loop为无限循环,则后面的loop无法执行,若不是无限循环,则后面的loop会执行第一个loop到第二个loop之间的缓动行为,第一个loop之前的缓动行为全部舍弃。其他情况同理。
如果要清理缓动,有两种方法,一种是调用 TweenUtil.clear() 方法,传入一个缓动对象,则这个缓动对象会被清理,另一种是清理作用域上的所有缓动对象,调用 TweenUtil.clearAll() 方法,传入作用域,则该作用域上的所有缓动对象都会被清理。
特别注意,本项目目前不支持缓动对象池,因为有可能会出现引用bug,因此需要缓动对象池的话请自行实现。
缓动工具类原理
本类内置3个变量 _tweenId ,_tweenDic , _tweenClock
- _tweenId变量用于自增缓动id,也是缓动对象字典中的唯一标识符,每个作用域对应一个缓动id。
- _tweenDic变量为缓动对象字典,用于存储缓动对象,结构为 { tweenId : SubTween[] } , key为缓动id,value为缓动辅助类数组,保存该作用域上的所有缓动对象。
- _tweenClock变量为缓动核心类对象,在缓动核心类中会调用它来获取缓动时间类
本类的核心方法to和from的原理相同,方法分为两个部分:
- 创建一个缓动辅助类对象,设置对象的目标并且调用对应 to/from 方法。
- 检查作用域的缓动id,如果不存在,则初始化缓动id,然后检查字典中是否存在对应id并把创建的对象保存到数组中,最后再返回这个对象。
清除缓动的原理则如下方详细代码所示。
缓动辅助类原理
本类内置8个变量 target , _listIndex , _tweenList , _tweening , _tweenObj , _tweenIndex , _loop , _limit
- target变量如名字所示,就是缓动的目标对象。
- _listIndex变量用于缓动列表的索引,主要是区别各个loop缓动动作组,只有调用loop的情况下会增加。
- _tweenList变量用于保存缓动动作,是一个二维数组,第一维保存缓动动作组,第二维保存缓动动作序列。
- _tweening变量用于判断当前是否正在缓动
- _tweenObj变量保存本类当前实例化的缓动核心类对象
- _tweenIndex变量用于缓动序列的索引
- _loop变量用于判断是否开启循环
- _limit变量用于保存缓动循环次数限制,是个一维数组
本类的核心方法to和from的原理相同,方法分为两个部分:
- 保存缓动参数到 _tweenList[ _listIndex ] 数组中
- 如果缓动未启动,则启动缓动
本类最关键的,就是启动缓动的逻辑,具体逻辑如下图所示
本类的启动缓动,实际上就是上一个缓动动作和下一个缓动动作的连接,具体的缓动执行和结束,还得看缓动核心类。
缓动核心类原理
本类内置11个变量 _target , _prop , _duration , _ease , _complete , _delay , _direction , _isDone , _time , _def , _isDelay
- _target变量如名字所示是缓动的对象
- _prop变量保存需要缓动的所有属性
- _duration变量是缓动的持续时间
- _ease变量为缓动函数,所有缓动属性都需要通过该函数进行插值
- _complete变量为回调函数,在缓动结束后执行,主要是执行缓动辅助类中的doTween
- _delay变量是缓动延迟时间,在延迟时间结束后才会开始执行缓动
- _direction变量用于区分to和from
- _isDone变量用于标记是否完成缓动
- _time变量用于记录当前缓动执行时间
- _def变量用于保存缓动对象的原始属性
- _isDelay变量用于标记是否延迟执行缓动
本类的to和from方法也是初始化传入的缓动参数,并且把本类对象推到缓动时间类的遍历列表中。
本类最关键的就是缓动每一帧更新的逻辑,具体逻辑如下图所示
注意,缓动函数的传参格式为: 经过时间,起始值,结束值,总时长,返回值为0到1的之间的数值
缓动时间类原理
本类在当前示例中依托Laya更新,也可以自定义定时器来更新。
核心逻辑主要就是update,遍历 _tweens ,先判断缓动核心对象是否结束缓动,结束缓动就移除,否则就执行 update 函数 ,传入 deltaTime 即 dt,单位为毫秒。
注意本类一般需要在整个项目中成为单例,不过本类并没有实现单例类,如果需要的话可自行实现。记得配合修改 TweenUtil.start 的内容。
代码
缓动工具类
export default class TweenUtil {
public static _tweenId = 1;
public static _tweenDic = {};
private static _tweenClock: TweenClock;
/**
* 启动缓动工具类 本函数负责启动缓动时间类,函数体可根据需求自定义修改
*/
public static start() {
let scriptScene = new Laya.Scene3D();
Laya.stage.addChild(scriptScene);
this._tweenClock = scriptScene.addComponent(TweenClock);
}
public static getTweenClock() {
return this._tweenClock;
}
/**
* to缓动 同Laya.Tween.to
* @param target
* @param prop
* @param time
* @param ease
* @param delay
* @param callback
* @returns
*/
public static to(caller, target, prop, time, ease?, delay?, callback?) {
// console.log("缓动",target);
let subTween = new SubTween();
subTween.target = target;
subTween.to(prop, time, ease, delay, callback);
if (!caller.tweenId) {
caller.tweenId = this._tweenId++;
}
if (!this._tweenDic[caller.tweenId]) {
this._tweenDic[caller.tweenId] = [];
}
this._tweenDic[caller.tweenId].push(subTween);
return subTween;
}
/**
* from缓动 同Laya.Tween.to
* @param target
* @param prop
* @param time
* @param ease
* @param delay
* @param callback
* @returns
*/
public static from(caller, target, prop, time, ease?, delay?, callback?) {
let subTween = new SubTween();
subTween.target = target;
subTween.to(prop, time, ease, delay, callback);
if (!caller.tweenId) {
caller.tweenId = this._tweenId++;
}
if (!this.caller[caller.tweenId]) {
this.caller[caller.tweenId] = [];
}
this._tweenDic[caller.tweenId].push(subTween);
return subTween;
}
/**
* 清除目标缓动
* @param tweenObj
*/
public static clear(tweenObj: SubTween) {
tweenObj.clear();
}
/**
* 清除目标节点所有缓动
* @param target
*/
public static clearAll(caller) {
// console.log("清除", caller)
if (!this._tweenDic[caller.tweenId]) return;
let len = this._tweenDic[caller.tweenId].length;
for (let i = 0; i < len; i++) {
(this._tweenDic[caller.tweenId][i] as SubTween).clear();
}
this._tweenDic[caller.tweenId] = [];
}
}
缓动辅助类
export class SubTween {
/** 目标节点 */
public target;
/** 缓动列表索引 */
private _listIndex = 0;
/** 缓动列表 */
private _tweenList;
/** 是否正在缓动 */
private _tweening = false;
/** 缓动对象 */
private _tweenObj: TweenCore;
/** 缓动索引 */
private _tweenIndex = 0;
/** 是否循环 */
private _loop = false;
/** 缓动循环次数限制列表 */
private _limit:number[] = [];
/**
* 缓动 从现在属性到目标属性
* @param prop
* @param time
* @param ease
* @param delay
* @param callback
* @returns
*/
public to(prop, time, ease?, delay?, callback?) {
if (!this._tweenList) {
this._tweenList = [];
}
if (!this._tweenList[this._listIndex]) {
this._tweenList[this._listIndex] = [];
}
this._tweenList[this._listIndex].push({
prop: prop,
time: time,
ease: ease,
delay: delay,
callback: callback,
type: 0
});
this.startTween();
return this;
}
/**
* 缓动 从目标属性到现在属性
* @param prop
* @param time
* @param ease
* @param delay
* @param callback
* @returns
*/
public from(prop, time, ease?, delay?, callback?) {
if (!this._tweenList) {
this._tweenList = [];
}
if (!this._tweenList[this._listIndex]) {
this._tweenList[this._listIndex] = [];
}
this._tweenList[this._listIndex].push({
prop: prop,
time: time,
ease: ease,
delay: delay,
callback: callback,
type: 1
});
this.startTween();
return this;
}
/**
* 循环前面所有的动作
* @param 循环次数 不填或者0为无限循环 只有在非无限循环的情况下可以执行本次loop后面的缓动
*/
public loop(limit = 0) {
this._loop = true;
this._limit[this._listIndex] = limit - 1;
this._listIndex++;
return this;
}
/**
* 清除缓动
*/
public clear() {
if (this._tweenObj) {
this._tweenObj.clear();
}
this._tweenList = null;
return this;
}
/**
* 开始缓动
*/
private startTween() {
if (!this._tweening) {
this._tweening = true;
this.doTween();
}
}
/**
* 执行缓动
*/
private doTween() {
let self = this;
if (!this._tweenList) return;
let param = this._tweenList[0][this._tweenIndex++];
if (param) {
if (param.type == 0) {
this._tweenObj = new TweenCore();
this._tweenObj.to(this.target, param.prop, param.time, param.ease, () => {
param.callback && param.callback();
self.doTween();
}, param.delay);
} else {
this._tweenObj = new TweenCore();
this._tweenObj.from(this.target, param.prop, param.time, param.ease, () => {
param.callback && param.callback();
self.doTween();
}, param.delay);
}
} else {
if (this._loop) {
this._tweenIndex = 0;
if (this._limit[0] == -1) {
this.doTween();
} else {
if (this._limit[0] > 0) {
this._limit[0]--;
} else {
this._tweenList.shift();
this._limit.shift();
this._listIndex--;
if (this._tweenList.length == 0) {
this._tweenList = null;
}
}
this.doTween();
}
} else {
this._tweenList = null;
this._limit = [];
}
}
}
}
缓动核心类
export class TweenCore {
private _target;
private _prop;
private _duration;
private _ease;
private _complete;
private _delay;
private _direction;
private _isDone;
private _time = 0;
private _def = {};
private _isDelay = false;
public to(target: any, props: any, duration: number, ease?: Function | null, complete?, delay?: number) {
this._target = target;
this._prop = props;
this._duration = duration;
this._ease = ease || Laya.Ease.linearInOut;
this._complete = complete;
this._delay = delay ? delay : 0;
this._delay && (this._isDelay = true);
this._direction = 1;
this._isDone = false;
for (let key in this._prop) {
this._def[key] = this._target[key];
}
TweenUtil.getTweenClock().push(this);
return this;
}
public from(target: any, props: any, duration: number, ease?: Function | null, complete?, delay?: number) {
this._target = target;
this._prop = props;
this._duration = duration;
this._ease = ease || Laya.Ease.linearInOut;
this._complete = complete;
this._delay = delay ? delay : 0;
this._delay && (this._isDelay = true);
this._direction = 0;
this._isDone = false;
for (let key in this._prop) {
this._def[key] = this._target[key];
}
TweenUtil.getTweenClock().push(this);
return this;
}
public update(dt) {
if (this._target && !this._isDone) {
//计算进度
this._time += dt;
if (this._isDelay) {
if (this._time >= this._delay) {
this._isDelay = false;
this._time = 0;
}
} else {
if (this._time > this._duration) {
this._time = this._duration;
this._isDone = true;
}
//更新
let ease = this._ease(this._time, 0, 1, this._duration);
for (let key in this._prop) {
if (key == "update") {
this._prop[key]();
} else {
this._target[key] = this._def[key] + (this._prop[key] - this._def[key]) * (this._direction ? ease : (1 - ease));
}
}
//结束回调
if (this._isDone) {
this.clear();
this._complete && this._complete();
this._complete = null;
}
}
}
}
public clear() {
TweenUtil.getTweenClock().sub(this);
this._target = null;
this._prop = null;
this._duration = null;
this._ease = null;
this._delay = null;
this._direction = null;
this._isDone = null;
this._time = 0;
this._def = {};
this._isDelay = false;
}
public getDone() {
return this._isDone;
}
}
缓动时钟类
此处挂载到Laya上,也可以自定义更新逻辑
export class TweenClock extends Laya.Script3D {
private _tweens:TweenCore[] = [];
onAwake() {
console.log("TweenClock启动")
}
public push(tween) {
this._tweens.push(tween);
}
public sub(tween) {
let index = this._tweens.indexOf(tween);
index != -1 && this._tweens.splice(index, 1);
}
public onUpdate() {
let time = Laya.timer.delta;
let tween: TweenCore;
for (let i = this._tweens.length - 1; i >= 0; i--) {
tween = this._tweens[i];
if (tween) {
if (tween.getDone()) {
this._tweens.splice(i, 1);
continue;
}
tween.update(time);
}
}
}
}
Laya缓动函数参考
class Ease {
static linearNone(t, b, c, d) {
return c * t / d + b;
}
static linearIn(t, b, c, d) {
return c * t / d + b;
}
static linearInOut(t, b, c, d) {
return c * t / d + b;
}
static linearOut(t, b, c, d) {
return c * t / d + b;
}
static bounceIn(t, b, c, d) {
return c - Ease.bounceOut(d - t, 0, c, d) + b;
}
static bounceInOut(t, b, c, d) {
if (t < d * 0.5)
return Ease.bounceIn(t * 2, 0, c, d) * .5 + b;
else
return Ease.bounceOut(t * 2 - d, 0, c, d) * .5 + c * .5 + b;
}
static bounceOut(t, b, c, d) {
if ((t /= d) < (1 / 2.75))
return c * (7.5625 * t * t) + b;
else if (t < (2 / 2.75))
return c * (7.5625 * (t -= (1.5 / 2.75)) * t + .75) + b;
else if (t < (2.5 / 2.75))
return c * (7.5625 * (t -= (2.25 / 2.75)) * t + .9375) + b;
else
return c * (7.5625 * (t -= (2.625 / 2.75)) * t + .984375) + b;
}
static backIn(t, b, c, d, s = 1.70158) {
return c * (t /= d) * t * ((s + 1) * t - s) + b;
}
static backInOut(t, b, c, d, s = 1.70158) {
if ((t /= d * 0.5) < 1)
return c * 0.5 * (t * t * (((s *= (1.525)) + 1) * t - s)) + b;
return c / 2 * ((t -= 2) * t * (((s *= (1.525)) + 1) * t + s) + 2) + b;
}
static backOut(t, b, c, d, s = 1.70158) {
return c * ((t = t / d - 1) * t * ((s + 1) * t + s) + 1) + b;
}
static elasticIn(t, b, c, d, a = 0, p = 0) {
var s;
if (t == 0)
return b;
if ((t /= d) == 1)
return b + c;
if (!p)
p = d * .3;
if (!a || (c > 0 && a < c) || (c < 0 && a < -c)) {
a = c;
s = p / 4;
}
else
s = p / Ease.PI2 * Math.asin(c / a);
return -(a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * d - s) * Ease.PI2 / p)) + b;
}
static elasticInOut(t, b, c, d, a = 0, p = 0) {
var s;
if (t == 0)
return b;
if ((t /= d * 0.5) == 2)
return b + c;
if (!p)
p = d * (.3 * 1.5);
if (!a || (c > 0 && a < c) || (c < 0 && a < -c)) {
a = c;
s = p / 4;
}
else
s = p / Ease.PI2 * Math.asin(c / a);
if (t < 1)
return -.5 * (a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * d - s) * Ease.PI2 / p)) + b;
return a * Math.pow(2, -10 * (t -= 1)) * Math.sin((t * d - s) * Ease.PI2 / p) * .5 + c + b;
}
static elasticOut(t, b, c, d, a = 0, p = 0) {
var s;
if (t == 0)
return b;
if ((t /= d) == 1)
return b + c;
if (!p)
p = d * .3;
if (!a || (c > 0 && a < c) || (c < 0 && a < -c)) {
a = c;
s = p / 4;
}
else
s = p / Ease.PI2 * Math.asin(c / a);
return (a * Math.pow(2, -10 * t) * Math.sin((t * d - s) * Ease.PI2 / p) + c + b);
}
static strongIn(t, b, c, d) {
return c * (t /= d) * t * t * t * t + b;
}
static strongInOut(t, b, c, d) {
if ((t /= d * 0.5) < 1)
return c * 0.5 * t * t * t * t * t + b;
return c * 0.5 * ((t -= 2) * t * t * t * t + 2) + b;
}
static strongOut(t, b, c, d) {
return c * ((t = t / d - 1) * t * t * t * t + 1) + b;
}
static sineInOut(t, b, c, d) {
return -c * 0.5 * (Math.cos(Math.PI * t / d) - 1) + b;
}
static sineIn(t, b, c, d) {
return -c * Math.cos(t / d * Ease.HALF_PI) + c + b;
}
static sineOut(t, b, c, d) {
return c * Math.sin(t / d * Ease.HALF_PI) + b;
}
static quintIn(t, b, c, d) {
return c * (t /= d) * t * t * t * t + b;
}
static quintInOut(t, b, c, d) {
if ((t /= d * 0.5) < 1)
return c * 0.5 * t * t * t * t * t + b;
return c * 0.5 * ((t -= 2) * t * t * t * t + 2) + b;
}
static quintOut(t, b, c, d) {
return c * ((t = t / d - 1) * t * t * t * t + 1) + b;
}
static quartIn(t, b, c, d) {
return c * (t /= d) * t * t * t + b;
}
static quartInOut(t, b, c, d) {
if ((t /= d * 0.5) < 1)
return c * 0.5 * t * t * t * t + b;
return -c * 0.5 * ((t -= 2) * t * t * t - 2) + b;
}
static quartOut(t, b, c, d) {
return -c * ((t = t / d - 1) * t * t * t - 1) + b;
}
static cubicIn(t, b, c, d) {
return c * (t /= d) * t * t + b;
}
static cubicInOut(t, b, c, d) {
if ((t /= d * 0.5) < 1)
return c * 0.5 * t * t * t + b;
return c * 0.5 * ((t -= 2) * t * t + 2) + b;
}
static cubicOut(t, b, c, d) {
return c * ((t = t / d - 1) * t * t + 1) + b;
}
static quadIn(t, b, c, d) {
return c * (t /= d) * t + b;
}
static quadInOut(t, b, c, d) {
if ((t /= d * 0.5) < 1)
return c * 0.5 * t * t + b;
return -c * 0.5 * ((--t) * (t - 2) - 1) + b;
}
static quadOut(t, b, c, d) {
return -c * (t /= d) * (t - 2) + b;
}
static expoIn(t, b, c, d) {
return (t == 0) ? b : c * Math.pow(2, 10 * (t / d - 1)) + b - c * 0.001;
}
static expoInOut(t, b, c, d) {
if (t == 0)
return b;
if (t == d)
return b + c;
if ((t /= d * 0.5) < 1)
return c * 0.5 * Math.pow(2, 10 * (t - 1)) + b;
return c * 0.5 * (-Math.pow(2, -10 * --t) + 2) + b;
}
static expoOut(t, b, c, d) {
return (t == d) ? b + c : c * (-Math.pow(2, -10 * t / d) + 1) + b;
}
static circIn(t, b, c, d) {
return -c * (Math.sqrt(1 - (t /= d) * t) - 1) + b;
}
static circInOut(t, b, c, d) {
if ((t /= d * 0.5) < 1)
return -c * 0.5 * (Math.sqrt(1 - t * t) - 1) + b;
return c * 0.5 * (Math.sqrt(1 - (t -= 2) * t) + 1) + b;
}
static circOut(t, b, c, d) {
return c * Math.sqrt(1 - (t = t / d - 1) * t) + b;
}
}
Ease.HALF_PI = Math.PI * 0.5;
Ease.PI2 = Math.PI * 2;