如今市面上关于动画的开源库多得数不胜数,有关于CSS、js甚至是canvas渲染的,百花齐放,效果炫酷。但你是否曾想过,自己亲手去实现(封装)一个简单的动画库?
本文将从零开始,讲授如何搭建一个简单的动画库,它将具备以下几个特征:
- 从实际动画中抽象出来,根据给定的动画速度曲线,完成“由帧到值”的计算过程,而实际渲染则交给开发者决定,更具拓展性
-
支持基本的事件监听,如
onPlay
、onStop
、onReset
、onEnd
,及相应的回调函数 -
支持手动式触发动画的各种状态,如
play
、stop
、reset
、end
- 支持自定义路径动画
- 支持多组动画的链式触发
完整的项目在这里: 点赞行为高尚! ,欢迎各种吐槽和指正^_^
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, c = this.value.end - this.value.start, 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, c = this.value.end - this.value.start, 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>
看到这里,本文就差不多结束了,下节将介绍如何在项目中加入各类事件监听及触发方式。
本系列文章将会继续不定期更新,欢迎各位大大指正^_^
注:本文内容来自互联网,旨在为开发者提供分享、交流的平台。如有涉及文章版权等事宜,请你联系站长进行处理。