做动效经常会用到缓动,市面上的缓动库也有好多,不同的语言,引擎都有自己的缓动库,比如比较出名的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时刻的值。不同的公式,就得到不同的值。最终把值映射给显示对象,就实现了缓动效果。
缓动函数的图像,可以搜索得到。缓动函数的公式为什么这么写,我也不知道,有知晓的大神,望不吝赐教。
如果我们有不同的需求,也可以定义自己的缓动函数。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。