摘要:傳送門從到,開(kāi)發(fā)一個(gè)動(dòng)畫(huà)庫(kù)如今市面上關(guān)于動(dòng)畫(huà)的開(kāi)源庫(kù)多得數(shù)不勝數(shù),有關(guān)于甚至是渲染的,百花齊放,效果炫酷。當(dāng)你看到的時(shí)候可能不大明白外界傳入的到底是啥其實(shí)是一個(gè)數(shù)組,它的每一個(gè)元素都保存著獨(dú)立動(dòng)畫(huà)的起始與結(jié)束兩種狀態(tài)。
傳送門:從0到1,開(kāi)發(fā)一個(gè)動(dòng)畫(huà)庫(kù)(2)
如今市面上關(guān)于動(dòng)畫(huà)的開(kāi)源庫(kù)多得數(shù)不勝數(shù),有關(guān)于CSS、js甚至是canvas渲染的,百花齊放,效果炫酷。但你是否曾想過(guò),自己親手去實(shí)現(xiàn)(封裝)一個(gè)簡(jiǎn)單的動(dòng)畫(huà)庫(kù)?
本文將從零開(kāi)始,講授如何搭建一個(gè)簡(jiǎn)單的動(dòng)畫(huà)庫(kù),它將具備以下幾個(gè)特征:
從實(shí)際動(dòng)畫(huà)中抽象出來(lái),根據(jù)給定的動(dòng)畫(huà)速度曲線,完成“由幀到值”的計(jì)算過(guò)程,而實(shí)際渲染則交給開(kāi)發(fā)者決定,更具拓展性
支持基本的事件監(jiān)聽(tīng),如onPlay、onStop、onReset 、onEnd,及相應(yīng)的回調(diào)函數(shù)
支持手動(dòng)式觸發(fā)動(dòng)畫(huà)的各種狀態(tài),如play、stop、reset、end
支持自定義路徑動(dòng)畫(huà)
支持多組動(dòng)畫(huà)的鏈?zhǔn)接|發(fā)
完整的項(xiàng)目在這里:https://github.com/JS-Hao/tim...,歡迎各種吐槽和指正^_^
OK,話不多說(shuō),現(xiàn)在正式開(kāi)始。
作為開(kāi)篇,本節(jié)將介紹的是最基本、最核心的步驟——構(gòu)建“幀-值”對(duì)應(yīng)的函數(shù)關(guān)系,完成“由幀到值”的計(jì)算過(guò)程。
目錄結(jié)構(gòu)首先介紹下我們的項(xiàng)目目錄結(jié)構(gòu):
/timeline /index..js /core.js /tween.js
/timeline是本項(xiàng)目的根目錄,各文件的作用分別如下:
index.js 項(xiàng)目入口文件
core.js 動(dòng)畫(huà)核心文件
easing.js 存放基本緩動(dòng)函數(shù)
引入緩動(dòng)函數(shù)所謂動(dòng)畫(huà),簡(jiǎn)單來(lái)說(shuō),就是在一段時(shí)間內(nèi)不斷改變目標(biāo)某些狀態(tài)的結(jié)果。這些狀態(tài)值在運(yùn)動(dòng)過(guò)程中,隨著時(shí)間不斷發(fā)生變化,狀態(tài)值與時(shí)間存在一一對(duì)應(yīng)的關(guān)系,這就是所謂的“幀-值”對(duì)應(yīng)關(guān)系,常說(shuō)的動(dòng)畫(huà)緩動(dòng)函數(shù)也是相同的道理。
有了這種函數(shù)關(guān)系,給定任意一個(gè)時(shí)間點(diǎn),我們都能計(jì)算出對(duì)應(yīng)的狀態(tài)值。OK,那如何在動(dòng)畫(huà)中引入緩動(dòng)函數(shù)呢?不說(shuō)廢話,直接上代碼:
首先我們?cè)赾ore.js中創(chuàng)建了一個(gè)Core類:
class Core { constructor(opt) { // 初始化,并將實(shí)例當(dāng)前狀態(tài)設(shè)置為"init" this._init(opt); this.state = "init"; } _init(opt) { this._initValue(opt.value); // 保存動(dòng)畫(huà)總時(shí)長(zhǎng)、緩動(dòng)函數(shù)以及渲染函數(shù) this.duration = opt.duration || 1000; this.timingFunction = opt.timingFunction || "linear"; this.renderFunction = opt.render || this._defaultFunc; // 未來(lái)會(huì)用到的事件函數(shù) this.onPlay = opt.onPlay; this.onEnd = opt.onEnd; this.onStop = opt.onStop; this.onReset = opt.onReset; } _initValue(value) { // 初始化運(yùn)動(dòng)值 this.value = []; value.forEach(item => { this.value.push({ start: parseFloat(item[0]), end: parseFloat(item[1]), }); }); } }
我們?cè)跇?gòu)造函數(shù)中對(duì)實(shí)例調(diào)用_init函數(shù),對(duì)其初始化:將傳入的參數(shù)保存在實(shí)例屬性中。
當(dāng)你看到_initValue的時(shí)候可能不大明白:外界傳入的value到底是啥?其實(shí)value是一個(gè)數(shù)組,它的每一個(gè)元素都保存著獨(dú)立動(dòng)畫(huà)的起始與結(jié)束兩種狀態(tài)。這樣說(shuō)好像有點(diǎn)亂,舉個(gè)栗子好了:假設(shè)我們要?jiǎng)?chuàng)建一個(gè)動(dòng)畫(huà),讓頁(yè)面上的div同時(shí)往右、左分別平移300px、500px,此外還同時(shí)把自己放大1.5倍。在這個(gè)看似復(fù)雜的動(dòng)畫(huà)過(guò)程中,其實(shí)可以拆解成三個(gè)獨(dú)立的動(dòng)畫(huà),每一動(dòng)畫(huà)都有自己的起始與終止值:
對(duì)于往右平移,就是把css屬性的translateX的0px變成了300px
同理,往下平移,就是把tranlateY的0px變成500px
放大1.5倍,也就是把`scale從1變成1.5
因此傳入的value應(yīng)該長(zhǎng)成這樣:[[0, 300], [0, 500], [1, 1.5]] 。我們將數(shù)組的每一個(gè)元素依次保存在實(shí)例的value屬性中。
此外,renderFunction是由外界提供的渲染函數(shù),即opt.render,它的作用是:
動(dòng)畫(huà)運(yùn)動(dòng)的每一幀,都會(huì)調(diào)用一次該函數(shù),并把計(jì)算好的當(dāng)前狀態(tài)值以參數(shù)形式傳入,有了當(dāng)前狀態(tài)值,我們就可以自由地選擇渲染動(dòng)畫(huà)的方式啦。
接下來(lái)我們給Core類添加一個(gè)循環(huán)函數(shù):
_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的作用是:倘若當(dāng)前時(shí)間進(jìn)度t還未到終點(diǎn),則根據(jù)當(dāng)前時(shí)間進(jìn)度計(jì)算出目標(biāo)現(xiàn)在的狀態(tài)值,并以參數(shù)的形式傳給即將調(diào)用的渲染函數(shù),即renderFunction,并繼續(xù)循環(huán)。如果大于duration,則將目標(biāo)的運(yùn)動(dòng)終止值傳給renderFunction,運(yùn)動(dòng)結(jié)束,將狀態(tài)設(shè)為end。
代碼中的Tween是從tween.js文件引入的緩動(dòng)函數(shù),tween.js的代碼如下(網(wǎng)上搜搜基本都差不多= =):
/* * t: current time(當(dāng)前時(shí)間); * b: beginning value(初始值); * c: change in value(變化量); * d: duration(持續(xù)時(shí)間)。 * 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(); // 執(zhí)行動(dòng)畫(huà)循環(huán) 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中引入它后就可以愉快地調(diào)用啦^ _ ^
PS:該項(xiàng)目是用webpack打包并以timeline.min.js作為輸出文件,由于暫時(shí)沒(méi)用到index.js文件,因此暫時(shí)以core.js作為打包入口啦~
看到這里,本文就差不多結(jié)束了,下節(jié)將介紹如何在項(xiàng)目中加入各類事件監(jiān)聽(tīng)及觸發(fā)方式。
本系列文章將會(huì)繼續(xù)不定期更新,歡迎各位大大指正^_^
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://m.specialneedsforspecialkids.com/yun/92654.html
摘要:傳送門從到,開(kāi)發(fā)一個(gè)動(dòng)畫(huà)庫(kù)上一節(jié)講到了最基礎(chǔ)的內(nèi)容,為動(dòng)畫(huà)構(gòu)建幀值對(duì)應(yīng)的函數(shù)關(guān)系,完成由幀到值的計(jì)算過(guò)程。這一節(jié)將在上節(jié)代碼的基礎(chǔ)上談?wù)勅绾谓o一個(gè)完整的動(dòng)畫(huà)添加各類事件。 傳送門:從0到1,開(kāi)發(fā)一個(gè)動(dòng)畫(huà)庫(kù)(1) 上一節(jié)講到了最基礎(chǔ)的內(nèi)容,為動(dòng)畫(huà)構(gòu)建幀-值對(duì)應(yīng)的函數(shù)關(guān)系,完成由幀到值的計(jì)算過(guò)程。這一節(jié)將在上節(jié)代碼的基礎(chǔ)上談?wù)勅绾谓o一個(gè)完整的動(dòng)畫(huà)添加各類事件。 在添加各類事件之前,我們先對(duì)...
摘要:傳送門從到,開(kāi)發(fā)一個(gè)動(dòng)畫(huà)庫(kù)如今市面上關(guān)于動(dòng)畫(huà)的開(kāi)源庫(kù)多得數(shù)不勝數(shù),有關(guān)于甚至是渲染的,百花齊放,效果炫酷。當(dāng)你看到的時(shí)候可能不大明白外界傳入的到底是啥其實(shí)是一個(gè)數(shù)組,它的每一個(gè)元素都保存著獨(dú)立動(dòng)畫(huà)的起始與結(jié)束兩種狀態(tài)。 傳送門:從0到1,開(kāi)發(fā)一個(gè)動(dòng)畫(huà)庫(kù)(2) 如今市面上關(guān)于動(dòng)畫(huà)的開(kāi)源庫(kù)多得數(shù)不勝數(shù),有關(guān)于CSS、js甚至是canvas渲染的,百花齊放,效果炫酷。但你是否曾想過(guò),自己親手...
摘要:幸運(yùn)的是,供應(yīng)似乎與需求相匹配,并且有多種選擇。讓我們來(lái)看看年值得關(guān)注的十大動(dòng)畫(huà)庫(kù)。八年了,仍然是一個(gè)強(qiáng)大的動(dòng)畫(huà)工具。接下來(lái)在這個(gè)令人驚嘆的動(dòng)畫(huà)庫(kù)列表上的就是了。,通常被稱為動(dòng)畫(huà)平臺(tái),我們忽略它在列表中的排名,它是列表中最受歡迎的庫(kù)之一。 原文鏈接原譯文鏈接 現(xiàn)代網(wǎng)站的客戶端依靠高質(zhì)量的動(dòng)畫(huà),這就使得JavaScript動(dòng)畫(huà)庫(kù)的需求不斷增加。幸運(yùn)的是,供應(yīng)似乎與需求相匹配,并且有多種選...
閱讀 2613·2021-11-02 14:39
閱讀 4338·2021-10-11 10:58
閱讀 1465·2021-09-06 15:12
閱讀 1850·2021-09-01 10:49
閱讀 1334·2019-08-29 18:31
閱讀 1889·2019-08-29 16:10
閱讀 3343·2019-08-28 18:21
閱讀 876·2019-08-26 10:42