24
2021
06

自己实现缓动库

做动效经常会用到缓动,市面上的缓动库也有好多,不同的语言,引擎都有自己的缓动库,比如比较出名的Tweenlite。

网上也有好多讲缓动的文章。但是大多数是讲怎么应用的,很少有讲缓动函数是怎么实现的。

有好多开源的小巧的缓动库,感兴趣的同学可以自己去读源码。

这里通过我自己写的一个简易的缓动库,简单介绍一下缓动库的实现原理。

并不是为了造轮子,而是为了学习一些原理性的东西,探究问题的本质,而不是仅仅停留在应用的层面。学会了原理,可以用来解决更多地问题。

先贴出源码(Typescript)。

HTweenLite.ts。

import {HEasing, HIEaseFunction} from "./HEasing";

export class HTweenLite {
  private static list: HTween[] = [];
  /**
   * 缓动方法
   * @param tar   缓动目标,一个目标只能同时指定一个缓动
   * @param dur   缓动持续时间
   * @param delay   延时启动缓动
   * @param to  缓动目标值
   * @param ease  缓动类型
   * @param update 缓动更新,缓动只是实现了数据的缓动,对象的状态变换还需要自己在中实现
   */
  public static to(tar: Object, dur: number, delay: number, to: Object, ease: HIEaseFunction, update: HIUpdate): void {
    const tween: HTween = new HTween(tar, dur, delay, HTweenLite.getObj(tar, to), to, ease, update);
    this.list.push(tween);
  }

  /**
   * 缓动方法
   * @param tar   缓动目标,一个目标只能同时指定一个缓动
   * @param dur   缓动持续时间
   * @param delay   延时启动缓动
   * @param from  缓动初始值
   * @param ease  缓动类型
   * @param update 缓动更新,缓动只是实现了数据的缓动,对象的状态变换还需要自己在中实现
   */
  public static from(tar: Object, dur: number, delay: number, from: Object, ease: HIEaseFunction, update: HIUpdate): void {
    const tween: HTween = new HTween(tar, dur, delay, from, HTweenLite.getObj(tar, from), ease, update);
    this.list.push(tween);
  }

  /**
   * 取消指定对象的缓动
   * @param tar
   * @param complete
   */
  public static kill(tar: object, complete: boolean): void {
    for(let i: number = HTweenLite.list.length - 1;i >= 0; i--){
      const tween: HTween = HTweenLite.list[i];
      if(tween.target == tar){
        tween.kill(complete);
      }
    }
  }

  /**
   * 取消所有缓动
   * @param complete
   */
  public static killAll(complete: boolean): void {
    for(let i: number = HTweenLite.list.length - 1;i >= 0; i--){
      HTweenLite.list[i].kill(complete);
    }
  }
  /**
   * 移除tween
   * @param htween
   */
  public static removeTween(htween: HTween): void {
    const ind: number = HTweenLite.list.indexOf(htween);
    if (ind != -1) {
      HTweenLite.list.splice(ind, 1);
    }
  }

  private static getObj(tar: Object, fromOrTo: Object): Object {
    const result: Object = {};
    for (let key in fromOrTo) {
      if (fromOrTo.hasOwnProperty(key)) {
        result[key] = tar[key];
      }
    }
    return result;
  }

}

type HIUpdate = (complete: boolean) => void;

class HTween{
  public target: Object;
  private duration: number;
  private from: Object;
  private to: Object;
  private t: number = 0;
  private update: HIUpdate;
  private timer: number;
  private ease: HIEaseFunction;
  private TIME_STEP: number = 0.03;

  constructor(tar: object, duration: number, delay: number, from: Object, to: Object, ease: HIEaseFunction, update: HIUpdate){
    this.target=tar;
    this.duration=duration;
    this.from=from;
    this.to=to;
    this.update=update;
    this.ease=ease || HEasing.None.easeIn;
    this.TIME_STEP = duration / Math.ceil(duration / 0.03);
    if (delay > 0) {
      this.timer = setTimeout(this.tt, delay) as Object as number;
    } else {
      this.tt();
    }
  }

  public kill(complete: boolean): void {
    if(complete){
      for (let key in this.to) {
        if(this.to.hasOwnProperty(key)) {
          this.target[key] = this.to[key];
        }
      }
      if (this.update) {
        this.update(true);
      }
    }
    clearInterval(this.timer);
    HTweenLite.removeTween(this);
  }

  private tt = () => {
    this.t += this.TIME_STEP;
    if(this.t >= this.duration){
      this.kill(true);
    }
    else{
      for (let key in this.from) {
        if(this.from.hasOwnProperty(key)) {
          //计算
          this.target[key] = this.ease(this.t, this.from[key], this.to[key] - this.from[key], this.duration);
        }
      }
      if(this.update){
        this.update(false);
      }
      this.timer = setTimeout(this.tt, this.TIME_STEP * 1000) as Object as number;
    }
  }

}

HEasing.ts

/**
 * 参数解释
 * @param t  当前时间,即缓动经过的时间,0-d
 * @param b  用于缓动的属性的初始值
 * @param c  用于缓动的属性的改变总量
 * @param d  缓动总持续时间
 */
export type HIEaseFunction = (t: number, b: number, c: number, d: number, ...args: number[]) => number;

export const HEasing = {
  None: {
    easeIn: (t: number, b: number, c: number, d: number) => {
      return b+t*c/d;
    }
  },
  Quad : {
    easeIn: (t: number, b: number, c: number, d: number) => {
      return c*(t/=d)*t+b;
    },
    easeOut: (t: number, b: number, c: number, d: number) => {
      return -c *(t/=d)*(t-2) + b;
    },
    easeInOut: (t: number, b: number, c: number, d: number) => {
      if ((t/=d/2) < 1) return c/2*t*t + b;
      return -c/2 * ((--t)*(t-2) - 1) + b;
    }
  },
  Cubic: {
    easeIn: (t: number, b: number, c: number, d: number) => {
      return c*(t/=d)*t*t + b;
    },
    easeOut: (t: number, b: number, c: number, d: number) => {
      return c*((t=t/d-1)*t*t + 1) + b;
    },
    easeInOut: (t: number, b: number, c: number, d: number) => {
      if ((t/=d/2) < 1) return c/2*t*t*t + b;
      return c/2*((t-=2)*t*t + 2) + b;
    }
  },
  Quart: {
    easeIn: (t: number, b: number, c: number, d: number) => {
      return c*(t/=d)*t*t*t + b;
    },
    easeOut: (t: number, b: number, c: number, d: number) => {
      return -c * ((t=t/d-1)*t*t*t - 1) + b;
    },
    easeInOut: (t: number, b: number, c: number, d: number) => {
      if ((t/=d/2) < 1) return c/2*t*t*t*t + b;
      return -c/2 * ((t-=2)*t*t*t - 2) + b;
    }
  },
  Quint: {
    easeIn: (t: number, b: number, c: number, d: number) => {
      return c*(t/=d)*t*t*t*t + b;
    },
    easeOut: (t: number, b: number, c: number, d: number) => {
      return c*((t=t/d-1)*t*t*t*t + 1) + b;
    },
    easeInOut: (t: number, b: number, c: number, d: number) => {
      if ((t/=d/2) < 1) return c/2*t*t*t*t*t + b;
      return c/2*((t-=2)*t*t*t*t + 2) + b;
    }
  },
  Sine: {
    easeIn: (t: number, b: number, c: number, d: number) => {
      return -c * Math.cos(t/d * (Math.PI/2)) + c + b;
    },
    easeOut: (t: number, b: number, c: number, d: number) => {
      return c * Math.sin(t/d * (Math.PI/2)) + b;
    },
    easeInOut: (t: number, b: number, c: number, d: number) => {
      return -c/2 * (Math.cos(Math.PI*t/d) - 1) + b;
    }
  },
  Strong: {
    easeIn: (t: number, b: number, c: number, d: number) => {
      return c*(t/=d)*t*t*t*t + b;
    },
    easeOut: (t: number, b: number, c: number, d: number) => {
      return c*((t=t/d-1)*t*t*t*t + 1) + b;
    },
    easeInOut: (t: number, b: number, c: number, d: number) => {
      if ((t/=d/2) < 1) return c/2*t*t*t*t*t + b;
      return c/2*((t-=2)*t*t*t*t + 2) + b;
    }
  },
  Expo: {
    easeIn: (t: number, b: number, c: number, d: number) => {
      return (t==0) ? b : c * Math.pow(2, 10 * (t/d - 1)) + b;
    },
    easeOut: (t: number, b: number, c: number, d: number) => {
      return (t==d) ? b+c : c * (-Math.pow(2, -10 * t/d) + 1) + b;
    },
    easeInOut: (t: number, b: number, c: number, d: number) => {
      if (t==0) return b;
      if (t==d) return b+c;
      if ((t/=d/2) < 1) return c/2 * Math.pow(2, 10 * (t - 1)) + b;
      return c/2 * (-Math.pow(2, -10 * --t) + 2) + b;
    }
  },
  Circ: {
    easeIn: (t: number, b: number, c: number, d: number) => {
      return -c * (Math.sqrt(1 - (t/=d)*t) - 1) + b;
    },
    easeOut: (t: number, b: number, c: number, d: number) => {
      return c * Math.sqrt(1 - (t=t/d-1)*t) + b;
    },
    easeInOut: (t: number, b: number, c: number, d: number) => {
      if ((t/=d/2) < 1) return -c/2 * (Math.sqrt(1 - t*t) - 1) + b;
      return c/2 * (Math.sqrt(1 - (t-=2)*t) + 1) + b;
    }
  },
  /**未实现**/
  Elastic: {
    easeIn: (t: number, b: number, c: number, d: number, a: number = 0, p: number = 0) => {
      if (t == 0) return b;
      if ((t /= d) == 1) return b + c;
      if (!p) p = d * 0.3;
      let s: number;
      if (!a || a < Math.abs(c)) {
        a = c;
        s = p / 4;
      } else {
        s = p / (2 * Math.PI) * Math.asin(c / a);
      }
      return -(a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * d - s) * (2 * Math.PI) / p)) + b;
    },
    easeOut: (t: number, b: number, c: number, d: number, a: number = 0, p: number = 0) => {
      if (t == 0) return b;
      if ((t /= d) == 1) return b + c;
      if (!p) p = d * 0.3;
      let s: number;
      if (!a || a < Math.abs(c)) {
        a = c;
        s = p / 4;
      } else {
        s = p / (2 * Math.PI) * Math.asin(c / a);
      }
      return a * Math.pow(2, -10 * t) * Math.sin((t * d - s) * (2 * Math.PI) / p) + c + b;
    },
    easeInOut: (t: number, b: number, c: number, d: number, a: number = 0, p: number = 0) => {
      if (t == 0) return b;
      if ((t /= d / 2) == 2) return b + c;
      if (!p) p = d * (0.3 * 1.5);
      let s: number;
      if (!a || a < Math.abs(c)) {
        a = c;
        s = p / 4;
      } else {
        s = p / (2 * Math.PI) * Math.asin(c / a);
      }
      if (t < 1) {
        return -0.5 * (a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * d - s) * (2 * Math.PI) /p)) + b;
      }
      return a * Math.pow(2, -10 * (t -= 1)) * Math.sin((t * d - s) * (2 * Math.PI) / p ) * 0.5 + c + b;
    }
  },
  Back: {
    easeIn: (t: number, b: number, c: number, d: number, s: number = 1.70158) => {
      return c*(t/=d)*t*((s+1)*t - s) + b;
    },
    easeOut: (t: number, b: number, c: number, d: number, s: number = 1.70158) => {
      return c*((t=t/d-1)*t*((s+1)*t + s) + 1) + b;
    },
    easeInOut: (t: number, b: number, c: number, d: number, s: number = 1.70158) => {
      if ((t/=d/2) < 1) return c/2*(t*t*(((s*=(1.525))+1)*t - s)) + b;
      return c/2*((t-=2)*t*(((s*=(1.525))+1)*t + s) + 2) + b;
    }
  },
  Bounce: {
    easeIn: (t: number, b: number, c: number, d: number) => {
      return c - HEasing.Bounce.easeOut(d-t, 0, c, d) + b;
    },
    easeOut: (t: number, b: number, c: number, d: number) => {
      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;
      }
    },
    easeInOut: (t: number, b: number, c: number, d: number) => {
      if (t < d/2) {
        return HEasing.Bounce.easeIn(t*2, 0, c, d) * .5 + b;
      } else {
        return HEasing.Bounce.easeOut(t*2-d, 0, c, d) * .5 + c*.5 + b;
      }
    }
  }
}

HTweenLite.ts包含两个类:HTweenLite和HTween。HTweenLite是我们对外暴露的缓动函数库,HTween并不想对外暴露。

HTweenLite实现了缓动函数的常用接口,from、to、kill、killAll。

每调用一次缓动,HTweenLite就会创建一个HTween对象,并存储起来,缓动结束或者调用kill、killAll的时候,销毁HTween对象。

HTween接收缓动参数,初始值from、目标值to、缓动持续时间duration、缓动函数ease、缓动对象target、回调函数update,还额外加了一个延迟参数delay(这个不是重点)。

HTween会启动一个定时器,根据初始值from、目标值to、总时间duration,使用缓动函数ease,计算当前时间t对应的值。

说一些题外话。很多同学会有错误的认识,比如认为缓动库是用来做动画的,比如缓动库是用来操作显示对象(比如div,比如sprite)的。其实缓动库是很抽象的,纯数据计算的,可以不依赖任何对象,甚至不依赖语言。

我们继续。HTween里边也没什么难点(共用定时器可能比较难,我们不考虑)。缓动库中最核心的就是缓动函数了。ease参数,ease参数是一个HIEaseFunction类型的函数,HEasing中实现了一些常用的缓动函数。

我们先看一下缓动函数的接口HIEaseFunction。

(t: number, b: number, c: number, d: number, ...args: number[]) => number

包含了4个参数(我们先忽略后边的不定项参数),返回一个数值。

第一个参数t是当前时间;

第二个参数b是初始值;

第三个参数是值的改变总量;

第四个参数是缓动总持续时间;

至于为什么是这四个参数,为什么参数名是t、b、c、d,我也不知道,缓动函数都是这么写的,我也就照抄了,参数都一样,方便大家交流(也方便复制)。

函数实现的功能就是,根据给定的参数,返回t时刻的值。不同的公式,就得到不同的值。最终把值映射给显示对象,就实现了缓动效果。

缓动函数的图像,可以搜索得到。缓动函数的公式为什么这么写,我也不知道,有知晓的大神,望不吝赐教。

如果我们有不同的需求,也可以定义自己的缓动函数。



« 上一篇下一篇 »

相关文章:

box2d原理学习  (2021-3-16 13:43:38)

Android动画原理分析(转)  (2015-12-24 16:54:43)

(转)图像之魔棒工具实现  (2015-8-24 9:33:37)

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。