传送门:从0到1,开辟一个动画库(2)
如今市面上关于动画的开源库多得不可胜数,有关于CSS、js以至是canvas衬着的,百花齐放,效果炫酷。但你是不是曾想过,本身亲手去完成(封装)一个简朴的动画库?
本文将从零最先,解说怎样搭建一个简朴的动画库,它将具有以下几个特性:
- 从现实动画中笼统出来,依据给定的动画速率曲线,完成“由帧到值”的盘算历程,而现实衬着则交给开辟者决议,更具拓展性
- 支撑基础的事宜监听,如
onPlay
、onStop
、onReset
、onEnd
,及响应的回调函数 - 支撑手动式触发动画的种种状况,如
play
、stop
、reset
、end
- 支撑自定义途径动画
- 支撑多组动画的链式触发
完全的项目在这里:https://github.com/JS-Hao/tim…,迎接种种吐槽和斧正^_^
OK,话不多说,如今正式最先。
作为开篇,本节将引见的是最基础、最中心的步骤——构建“帧-值”对应的函数关联,完成“由帧到值”的盘算历程。
目次组织
起首引见下我们的项目目次组织:
/timeline
/index..js
/core.js
/tween.js
/timeline
是本项目的根目次,各文件的作用离别以下:
-
index.js
项目进口文件 -
core.js
动画中心文件 -
easing.js
寄存基础缓动函数
引入缓动函数
所谓动画,简朴来讲,就是在一段时刻内不停转变目的某些状况的效果。这些状况值在活动历程当中,跟着时刻不停发生变化,状况值与时刻存在一一对应的关联,这就是所谓的“帧-值”对应关联,常说的动画缓动函数也是雷同的原理。
有了这类函数关联,给定恣意一个时刻点,我们都能盘算出对应的状况值。OK,那怎样在动画中引入缓动函数呢?不说空话,直接上代码:
起首我们在core.js中建立了一个Core
类:
class Core {
constructor(opt) {
// 初始化,并将实例当前状况设置为'init'
this._init(opt);
this.state = 'init';
}
_init(opt) {
this._initValue(opt.value);
// 保留动画总时长、缓动函数以及衬着函数
this.duration = opt.duration || 1000;
this.timingFunction = opt.timingFunction || 'linear';
this.renderFunction = opt.render || this._defaultFunc;
// 将来会用到的事宜函数
this.onPlay = opt.onPlay;
this.onEnd = opt.onEnd;
this.onStop = opt.onStop;
this.onReset = opt.onReset;
}
_initValue(value) {
// 初始化活动值
this.value = [];
value.forEach(item => {
this.value.push({
start: parseFloat(item[0]),
end: parseFloat(item[1]),
});
});
}
}
我们在组织函数中对实例挪用_init
函数,对其初始化:将传入的参数保留在实例属性中。
当你看到_initValue
的时刻能够不大邃晓:外界传入的value
究竟是啥?实在value
是一个数组,它的每个元素都保留着自力动画的肇端与完毕两种状况。如许说彷佛有点乱,举个栗子好了:假定我们要建立一个动画,让页面上的div同时往右、左离别平移300px、500px,另外还同时把本身放大1.5倍。在这个看似庞杂的动画历程当中,实在能够拆解成三个自力的动画,每一动画都有本身的肇端与停止值:
- 关于往右平移,就是把css属性的
translateX
的0px变成了300px - 同理,往下平移,就是把
tranlateY
的0px变成500px - 放大1.5倍,也就是把
`scale
从1变成1.5
因而传入的value应当长成如许:[[0, 300], [0, 500], [1, 1.5]]
。我们将数组的每个元素顺次保留在实例的value属性中。
另外,renderFunction
是由外界供应的衬着函数,即opt.render
,它的作用是:
动画活动的每一帧,都邑挪用一次该函数,并把盘算好的当前状况值以参数情势传入,有了当前状况值,我们就能够自由地挑选衬着动画的体式格局啦。
接下来我们给Core类增添一个轮回函数:
_loop() {
const t = Date.now() - this.beginTime,
d = this.duration,
func = Tween[this.timingFunction] || Tween['linear'];
if (t >= d) {
this.state = 'end';
this._renderFunction(d, d, func);
} else {
this._renderFunction(t, d, func);
window.requestAnimationFrame(this._loop.bind(this));
}
}
_renderFunction(t, d, func) {
const values = this.value.map(value => func(t, value.start, value.end - value.start, d));
this.renderFunction.apply(this, values);
}
_loop
的作用是:倘使当前时刻进度t
还未到尽头,则依据当前时刻进度盘算出目的如今的状况值,并以参数的情势传给行将挪用的衬着函数,即renderFunction
,并继承轮回。假如大于duration
,则将目的的活动停止值传给renderFunction
,活动完毕,将状况设为end
。
代码中的Tween
是从tween.js文件引入的缓动函数,tween.js的代码以下(网上搜搜基础都差不多= =):
/*
* t: current time(当前时刻);
* b: beginning value(初始值);
* c: change in value(变化量);
* d: duration(持续时刻)。
* Get effect on 'http://easings.net/zh-cn'
*/
const Tween = {
linear: function (t, b, c, d) {
return c * t / d + b;
},
// Quad
easeIn: function (t, b, c, d) {
return c * (t /= d) * t + b;
},
easeOut: function (t, b, c, d) {
return -c * (t /= d) * (t - 2) + b;
},
easeInOut: function (t, b, c, d) {
if ((t /= d / 2) < 1) return c / 2 * t * t + b;
return -c / 2 * ((--t) * (t - 2) - 1) + b;
},
// Cubic
easeInCubic: function (t, b, c, d) {
return c * (t /= d) * t * t + b;
},
easeOutCubic: function (t, b, c, d) {
return c * ((t = t / d - 1) * t * t + 1) + b;
},
easeInOutCubic: function (t, b, c, d) {
if ((t /= d / 2) < 1) return c / 2 * t * t * t + b;
return c / 2 * ((t -= 2) * t * t + 2) + b;
},
// Quart
easeInQuart: function (t, b, c, d) {
return c * (t /= d) * t * t * t + b;
},
easeOutQuart: function (t, b, c, d) {
return -c * ((t = t / d - 1) * t * t * t - 1) + b;
},
easeInOutQuart: function (t, b, c, d) {
if ((t /= d / 2) < 1) return c / 2 * t * t * t * t + b;
return -c / 2 * ((t -= 2) * t * t * t - 2) + b;
},
// Quint
easeInQuint: function (t, b, c, d) {
return c * (t /= d) * t * t * t * t + b;
},
easeOutQuint: function (t, b, c, d) {
return c * ((t = t / d - 1) * t * t * t * t + 1) + b;
},
easeInOutQuint: function (t, b, c, d) {
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
easeInSine: function (t, b, c, d) {
return -c * Math.cos(t / d * (Math.PI / 2)) + c + b;
},
easeOutSine: function (t, b, c, d) {
return c * Math.sin(t / d * (Math.PI / 2)) + b;
},
easeInOutSine: function (t, b, c, d) {
return -c / 2 * (Math.cos(Math.PI * t / d) - 1) + b;
},
// Expo
easeInExpo: function (t, b, c, d) {
return (t == 0) ? b : c * Math.pow(2, 10 * (t / d - 1)) + b;
},
easeOutExpo: function (t, b, c, d) {
return (t == d) ? b + c : c * (-Math.pow(2, -10 * t / d) + 1) + b;
},
easeInOutExpo: function (t, b, c, d) {
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
easeInCirc: function (t, b, c, d) {
return -c * (Math.sqrt(1 - (t /= d) * t) - 1) + b;
},
easeOutCirc: function (t, b, c, d) {
return c * Math.sqrt(1 - (t = t / d - 1) * t) + b;
},
easeInOutCirc: function (t, b, c, d) {
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
easeInElastic: function (t, b, c, d, a, p) {
let s;
if (t == 0) return b;
if ((t /= d) == 1) return b + c;
if (typeof p == "undefined") p = d * .3;
if (!a || a < Math.abs(c)) {
s = p / 4;
a = c;
} 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;
},
easeOutElastic: function (t, b, c, d, a, p) {
let s;
if (t == 0) return b;
if ((t /= d) == 1) return b + c;
if (typeof p == "undefined") p = d * .3;
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);
},
easeInOutElastic: function (t, b, c, d, a, p) {
let s;
if (t == 0) return b;
if ((t /= d / 2) == 2) return b + c;
if (typeof p == "undefined") p = d * (.3 * 1.5);
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 -.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) * .5 + c + b;
},
// Back
easeInBack: function (t, b, c, d, s) {
if (typeof s == "undefined") s = 1.70158;
return c * (t /= d) * t * ((s + 1) * t - s) + b;
},
easeOutBack: function (t, b, c, d, s) {
if (typeof s == "undefined") s = 1.70158;
return c * ((t = t / d - 1) * t * ((s + 1) * t + s) + 1) + b;
},
easeInOutBack: function (t, b, c, d, s) {
if (typeof s == "undefined") s = 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
easeInBounce: function (t, b, c, d) {
return c - Tween.easeOutBounce(d - t, 0, c, d) + b;
},
easeOutBounce: function (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;
}
},
easeInOutBounce: function (t, b, c, d) {
if (t < d / 2) {
return Tween.easeInBounce(t * 2, 0, c, d) * .5 + b;
} else {
return Tween.easeOutBounce(t * 2 - d, 0, c, d) * .5 + c * .5 + b;
}
}
};
export default Tween;
末了,给Core
类增添play
要领:
_play() {
this.state = 'play';
this.beginTime = Date.now();
// 实行动画轮回
const loop = this._loop.bind(this);
window.requestAnimationFrame(loop);
}
play() {
this._play();
}
core.js的完全代码以下:
import Tween from './tween';
class Core {
constructor(opt) {
this._init(opt);
this.state = 'init';
}
_init(opt) {
this._initValue(opt.value);
this.duration = opt.duration || 1000;
this.timingFunction = opt.timingFunction || 'linear';
this.renderFunction = opt.render || this._defaultFunc;
/* Events */
this.onPlay = opt.onPlay;
this.onEnd = opt.onEnd;
this.onStop = opt.onStop;
this.onReset = opt.onReset;
}
_initValue(value) {
this.value = [];
value.forEach(item => {
this.value.push({
start: parseFloat(item[0]),
end: parseFloat(item[1]),
});
})
}
_loop() {
const t = Date.now() - this.beginTime,
d = this.duration,
func = Tween[this.timingFunction] || Tween['linear'];
if (t >= d) {
this.state = 'end';
this._renderFunction(d, d, func);
} else {
this._renderFunction(t, d, func);
window.requestAnimationFrame(this._loop.bind(this));
}
}
_renderFunction(t, d, func) {
const values = this.value.map(value => func(t, value.start, value.end - value.start, d));
this.renderFunction.apply(this, values);
}
_play() {
this.state = 'play';
this.beginTime = Date.now();
const loop = this._loop.bind(this);
window.requestAnimationFrame(loop);
}
play() {
this._play();
}
}
window.Timeline = Core;
在html中引入它后就能够愉快地挪用啦^ _ ^
PS:该项目是用webpack打包并以timeline.min.js作为输出文件,因为临时没用到index.js文件,因而临时以core.js作为打包进口啦~
<!DOCTYPE html>
<html>
<head>
<title></title>
<style type="text/css">
#box {
width: 100px;
height: 100px;
background: green;
}
</style>
</head>
<body>
<div id="box"></div>
<script type="text/javascript" src="timeline.js"></script>
<script type="text/javascript">
const box = document.querySelector('#box');
const timeline = new Timeline({
duration: 3000,
value: [[0, 400], [0, 600]],
render: function(value1, value2) {
box.style.transform = `translate(${ value1 }px, ${ value2 }px)`;
},
timingFunction: 'easeOut',
})
timeline.play();
</script>
</body>
</html>
看到这里,本文就差不多完毕了,下节将引见怎样在项目中到场各种事宜监听及触发体式格局。
本系列文章将会继承不定期更新,迎接列位大大斧正^_^