Ts缓动系统

基本原理

概述

缓动的基本原理很简单,就是设置一个初值和终值,在每帧更新的时候根据特定的缓动函数算出对应的中间值并插值。

本工具类分为4个部分:缓动工具类、缓动辅助类、缓动核心类、缓动时间类。同时,本工具提供链式调用,方便进行多个缓动动作的连续执行。

  1. 缓动工具类负责管理缓动的调用,以及一个作用域所有的缓动。

  2. 缓动辅助类负责管理单个缓动的初始化,同时也是链式调用的核心类。

  3. 缓动核心类负责管理具体的缓动逻辑,所以缓动都是通过该类执行。

  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

  1. _tweenId变量用于自增缓动id,也是缓动对象字典中的唯一标识符,每个作用域对应一个缓动id。
  2. _tweenDic变量为缓动对象字典,用于存储缓动对象,结构为 { tweenId : SubTween[] } , key为缓动id,value为缓动辅助类数组,保存该作用域上的所有缓动对象。
  3. _tweenClock变量为缓动核心类对象,在缓动核心类中会调用它来获取缓动时间类

本类的核心方法to和from的原理相同,方法分为两个部分:

  1. 创建一个缓动辅助类对象,设置对象的目标并且调用对应 to/from 方法。
  2. 检查作用域的缓动id,如果不存在,则初始化缓动id,然后检查字典中是否存在对应id并把创建的对象保存到数组中,最后再返回这个对象。

清除缓动的原理则如下方详细代码所示。

缓动辅助类原理

本类内置8个变量 target , _listIndex , _tweenList , _tweening , _tweenObj , _tweenIndex , _loop , _limit

  1. target变量如名字所示,就是缓动的目标对象。
  2. _listIndex变量用于缓动列表的索引,主要是区别各个loop缓动动作组,只有调用loop的情况下会增加。
  3. _tweenList变量用于保存缓动动作,是一个二维数组,第一维保存缓动动作组,第二维保存缓动动作序列。
  4. _tweening变量用于判断当前是否正在缓动
  5. _tweenObj变量保存本类当前实例化的缓动核心类对象
  6. _tweenIndex变量用于缓动序列的索引
  7. _loop变量用于判断是否开启循环
  8. _limit变量用于保存缓动循环次数限制,是个一维数组

本类的核心方法to和from的原理相同,方法分为两个部分:

  1. 保存缓动参数到 _tweenList[ _listIndex ] 数组中
  2. 如果缓动未启动,则启动缓动

本类最关键的,就是启动缓动的逻辑,具体逻辑如下图所示

本类的启动缓动,实际上就是上一个缓动动作和下一个缓动动作的连接,具体的缓动执行和结束,还得看缓动核心类。

缓动核心类原理

本类内置11个变量 _target , _prop , _duration , _ease , _complete , _delay , _direction , _isDone , _time , _def , _isDelay

  1. _target变量如名字所示是缓动的对象
  2. _prop变量保存需要缓动的所有属性
  3. _duration变量是缓动的持续时间
  4. _ease变量为缓动函数,所有缓动属性都需要通过该函数进行插值
  5. _complete变量为回调函数,在缓动结束后执行,主要是执行缓动辅助类中的doTween
  6. _delay变量是缓动延迟时间,在延迟时间结束后才会开始执行缓动
  7. _direction变量用于区分to和from
  8. _isDone变量用于标记是否完成缓动
  9. _time变量用于记录当前缓动执行时间
  10. _def变量用于保存缓动对象的原始属性
  11. _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);
            }
        }
    }
}
{% ghcard busyoGG/TweenUtilForLaya::theme=onedark %}

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;

GitHub项目地址

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值