如何利用JS實(shí)現(xiàn)時(shí)間軸動(dòng)畫效果
css動(dòng)畫
在前端開發(fā)中,一些簡單的動(dòng)效往往是使用 css3 的 @keyframes 來實(shí)現(xiàn)的 ,如:
.div1 { width: 100px; height: 100px; background: red; animation: changeColor 2s; } @keyframes changeColor { 0% {background: red;} 50% {background: yellow;} 100% {background: blue;} }
假如設(shè)定的時(shí)間是2s,那么 這段動(dòng)畫的描述是這樣的:在2秒內(nèi),某元素的背景色由紅色變?yōu)辄S色,又變?yōu)樗{(lán)色了。
假定有這樣一個(gè)需求:
一個(gè)動(dòng)畫共持續(xù)4秒,在0-2秒內(nèi) div1 從紅色變?yōu)辄S色再變?yōu)樗{(lán)色,div2 在2-4秒內(nèi)寬度由100px變?yōu)?00px。
.div1 { width: 100px; height: 100px; background: red; animation: changeColor 2s forwards; } @keyframes changeColor { 0% {background: red;} 50% {background: yellow;} 100% {background: blue;} } .div2 { width: 100px; height: 100px; background: pink; animation: changeWidth 4s forwards; } @keyframes changeWidth { 0% {width: 100px} 50% {width: 100px} 100% {width: 200px} }
看起來好像也還行,那么 div1 在第1秒變色, div2 在2-5秒內(nèi)變形, div3 在3-7秒內(nèi)變色變形,div4在4-10秒內(nèi)旋轉(zhuǎn)...,然后這些一起構(gòu)成了需求想要的動(dòng)畫效果。
這樣的話我們要找出整個(gè)動(dòng)畫的總時(shí)長,然后每個(gè)div的運(yùn)動(dòng)時(shí)間算出相對總時(shí)間的百分比區(qū)域,然后設(shè)定 keyframe 動(dòng)畫。
到這里 keyframes 貌似就有些復(fù)雜度了,我們平時(shí)一直在用 keyframes,那么 keyframes 或者說動(dòng)畫的本質(zhì)是什么?
由于人類視覺停留的原理,這樣一張張圖片連續(xù)展示就看起來像動(dòng)畫了,翻書動(dòng)畫或者顯示器逐幀刷新顯示都是利用了這個(gè)原理。在動(dòng)畫創(chuàng)作時(shí),除了像素、2d動(dòng)畫還在逐幀繪制,大多數(shù)類型的動(dòng)畫都已在編輯時(shí)方便將幀轉(zhuǎn)換為時(shí)間進(jìn)行計(jì)算。
keyframes 作為一個(gè)高度定制化的產(chǎn)物,只是在名稱上對“幀”這個(gè)概念進(jìn)行了保留,它對單個(gè)元素的動(dòng)畫編輯是簡單方便的。
但通過示例二就可以看出,它沒有對多元素組合動(dòng)畫進(jìn)行針對性的設(shè)計(jì),代碼上 div1 和 div2 是沒有邏輯關(guān)系表現(xiàn)的 (雖然在最終運(yùn)行時(shí)會(huì)進(jìn)行組合,即頁面上的多數(shù)動(dòng)畫是維護(hù)在一個(gè)計(jì)時(shí)器里執(zhí)行的。),它們沒有統(tǒng)一的時(shí)間軸,沒有父級包裹,要實(shí)現(xiàn)這些,需要編碼者自己維護(hù)。
什么是時(shí)間軸動(dòng)畫?
對有過視頻剪輯經(jīng)驗(yàn)的人來說,視頻剪輯的核心工具:基于時(shí)間軸的多軌視頻編輯器是極為高效和方便的,它實(shí)現(xiàn)了對多個(gè)動(dòng)畫/特效片段的整合,稱為時(shí)間軸動(dòng)畫。我們可以基于此實(shí)現(xiàn)一個(gè)簡易的js版本來針對多元素長時(shí)間的動(dòng)畫需求。
如圖所示,整個(gè)視頻由多段小動(dòng)畫交錯(cuò)拼合而成,公用一個(gè)時(shí)間軸,由此構(gòu)成了整個(gè)視頻。
類比到前端,我們也可以用類似的方法,以時(shí)間軸為基準(zhǔn),整合多個(gè)動(dòng)畫片段。
我們可以有以下思路:
先創(chuàng)建一個(gè)obj對象作為參數(shù),我們稱作 “動(dòng)畫對象”,它是整個(gè)動(dòng)畫過程的描述。
再創(chuàng)建一個(gè)執(zhí)行動(dòng)畫的函數(shù),我們稱作 ”動(dòng)畫函數(shù)“,它接受”動(dòng)畫對象“和一個(gè)回調(diào)函數(shù)(動(dòng)畫結(jié)束時(shí)執(zhí)行)作為參數(shù)。
接下來就進(jìn)入代碼實(shí)現(xiàn):
動(dòng)畫對象
我們首先看以下代碼:
var anim = [ { // 動(dòng)畫對象,可以多個(gè) dom: document.getElementById('a1'), // dom對象 anim: [ // 動(dòng)畫數(shù)組,里面可為多個(gè)對象 { style: 'height', // 要改變的樣式名稱如width,height from: '10rem', // 樣式的起始值要和to保持一致 to: '20rem', // 樣式的目標(biāo)值要和from保持一致 start: 0, // 開始時(shí)間 毫秒 end: 100, // 結(jié)束時(shí)間 毫秒 }, { style: 'width', from: '10rem', to: '20rem', start: 100, end: 200, }, { style: 'transform', from: 'rotate(20deg)', to: 'rotate(40deg)', start: 0, end: 2000, } ] }, { dom: document.getElementById('a2'), anim: [ { style: 'background', from: 'rgba(0.0, 0.0, 0.0, 1)', to: 'rgba(255, 0, 100, 0.5)', start: 0, end: 3000, } ] }, ]; timeLineAnim(anim, function () { console.log('animation finished'); });
anim 對象里清晰的描述了每個(gè)對象有幾個(gè)動(dòng)畫,每個(gè)動(dòng)畫是怎么進(jìn)行的。
然后 anim 對象作為一個(gè)參數(shù)傳入了 timeLineAnim 中,在實(shí)際執(zhí)行中它會(huì)進(jìn)行如下操作:
div1在0.1秒內(nèi)高度從 10rem 變?yōu)榱?20rem,接著變寬,同時(shí)前2秒內(nèi)還在旋轉(zhuǎn)。
div2在0-3秒內(nèi)顏色由rgba(0, 0, 0, 1) 變?yōu)閞gba(255, 0, 100, 0.5)。
在3秒后,動(dòng)畫結(jié)束,控制臺打出了 animation finished。
它的效果如下圖所示:
動(dòng)畫函數(shù)
實(shí)現(xiàn)這個(gè)動(dòng)畫函數(shù),首先簡單構(gòu)思下思路:1:將參數(shù) anim 中的 from、to 的數(shù)字提取出來,便于計(jì)算。
2:開啟 requestAnimationFrame,根據(jù)時(shí)間間隔和動(dòng)畫參數(shù) anim 計(jì)算出當(dāng)前幀的 css,并賦到對應(yīng)的 dom 元素上。
3:在最后一個(gè)動(dòng)畫結(jié)束后,關(guān)閉requestAnimationFrame,并執(zhí)行回調(diào)函數(shù)。
現(xiàn)在就進(jìn)入編碼階段:在以上思路的基礎(chǔ)上,我們在具體實(shí)現(xiàn)的過程中,通過 timeCount 來判斷動(dòng)畫是否結(jié)束,使用 cssText 進(jìn)行樣式的賦值。
function timeLineAnim(arr, cb) { /* initData: 對數(shù)據(jù)進(jìn)行初始化,將from、to中的信息轉(zhuǎn)為數(shù)字,計(jì)算出動(dòng)畫的個(gè)數(shù) setCss: 逐幀執(zhí)行,根據(jù)傳入的單個(gè)動(dòng)畫對象和每幀的時(shí)間間隔,計(jì)算出當(dāng)前幀的樣式,并修改元素的style值 */ function initData(obj) { var reg = /[\d\.]+/g; var numArrFrom = obj.from.match(reg); var numArrTo = obj.to.match(reg); if (!numArrFrom || !numArrTo || numArrFrom.length == 0 || numArrTo.length == 0 || numArrFrom.length != numArrTo.length) { console.warn('數(shù)據(jù)輸入錯(cuò)誤'); } for (var i = 0; i < numArrFrom.length; i++) { numArrFrom[i] = parseFloat(numArrFrom[i]); numArrTo[i] = parseFloat(numArrTo[i]); } var strArr = obj.from.split(reg); if (numArrFrom.length <= 0) { return; }; animCount++; return { strArr: strArr, numArrFrom: numArrFrom, numArrTo: numArrTo, totalLength: strArr.length + numArrFrom.length, isEnd: false, } } function setCss(obj, timeCount) { var styleStr = ''; var tempCount = animCount; // 將對象的多個(gè)動(dòng)畫,根據(jù)時(shí)間計(jì)算出樣式,并合并 for (var i = 0; i < obj.anim.length; i++) { var target = obj.anim[i]; if (timeCount > target.start && !target.initData.isEnd) { // 根據(jù)起止時(shí)間 計(jì)算出當(dāng)前動(dòng)畫進(jìn)度 var percent = (timeCount - target.start) / (target.end - target.start); if (percent >= 1) { percent = 1; target.initData.isEnd = true; tempCount--; } styleStr += getValue(percent); } } obj.dom.style.cssText += ';' + styleStr; animCount = tempCount; // 這里進(jìn)行單個(gè)動(dòng)畫每幀樣式的生成,這里是勻速運(yùn)動(dòng) function getValue(percent) { var numFrom = target.initData.numArrFrom; var numTo = target.initData.numArrTo; var arr = []; for (var i = 0; i < numFrom.length; i++) { arr.push(numFrom[i] + (numTo[i] - numFrom[i]) * percent); } var numIndex = 0; var strIndex = 0; var turnForNum = false; var str = target.style + ':'; for (var i = 0; i < target.initData.totalLength; i++) { if (turnForNum) { str += arr[numIndex]; numIndex++; turnForNum = false; } else { str += target.initData.strArr[strIndex]; strIndex++; turnForNum = true; } } return str + ';'; } } // 主體邏輯: 初始化數(shù)據(jù)、幀動(dòng)畫、動(dòng)畫結(jié)束執(zhí)行回調(diào) var animCount = 0; for (var i = 0; i < arr.length; i++) { for (var j = 0; j < arr[i].anim.length; j++) { arr[i].anim[j].initData = initData(arr[i].anim[j]); } } this.timer && cancelAnimationFrame(this.timer); var _this = this; (function animate(timeCount) { if (animCount > 0) { for (var i = 0; i < arr.length; i++) { setCss(arr[i], timeCount); } _this.timer = requestAnimationFrame(animate); } else { cancelAnimationFrame(_this.timer); cb && cb(); } })(); }
伸手黨可直接復(fù)制運(yùn)行。至此,一個(gè)基于時(shí)間軸的簡易動(dòng)畫函數(shù)就完成了,不到100行的代碼就可以使你愉快的使用聲明式的方式編輯時(shí)間軸動(dòng)畫。
此外,在getValue階段,你還可以根據(jù)自己的需求搭配不同的運(yùn)動(dòng)曲線函數(shù),豐富動(dòng)畫的功能。
由于參數(shù)是類json的,因此如果需要更進(jìn)一步的話,你完全可以制作一個(gè)UI界面來可視化的生成動(dòng)畫參數(shù),從而方便非程序員從業(yè)者使用。
思考
開頭的部分講述了市面上少部分動(dòng)畫如2d像素類,2d動(dòng)畫的制作仍然是以幀來進(jìn)行的。
而無論在 keyframes 里 還是 本文給出的 timeLineAnim 函數(shù) 都是以 時(shí)間來進(jìn)行設(shè)計(jì)的。
那么:如果我們改成用幀來進(jìn)行計(jì)算和制作會(huì)如何呢?有什么優(yōu)缺點(diǎn)嗎?歡迎評論區(qū)討論。
總結(jié)
到此這篇關(guān)于如何利用JS實(shí)現(xiàn)時(shí)間軸動(dòng)畫效果的文章就介紹到這了,更多相關(guān)JS時(shí)間軸動(dòng)畫內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
基于Echarts實(shí)現(xiàn)繪制立體柱狀圖的示例代碼
這篇文章主要為大家詳細(xì)介紹了如何基于Echarts實(shí)現(xiàn)繪制立體柱狀圖的功能,文中的示例代碼講解詳細(xì),具有一定的借鑒價(jià)值,需要的可以參考一下2023-02-02Vue之vue-tree-color組件實(shí)現(xiàn)組織架構(gòu)圖案例詳解
這篇文章主要介紹了Vue之vue-tree-color組件實(shí)現(xiàn)組織架構(gòu)圖案例詳解,本篇文章通過簡要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下2021-09-09JavaScript 實(shí)現(xiàn)網(wǎng)頁打印處理
JavaScript 實(shí)現(xiàn)網(wǎng)頁打印處理...2007-04-04javascript 變態(tài)的節(jié)點(diǎn)集合
今天想實(shí)現(xiàn)jQuery的unwrap效果,換言之,就是用其孩子把其父節(jié)點(diǎn)干掉。為了效率,用到文檔碎片,而取孩子時(shí)使用到childNodes(返回一個(gè)nodeList)2010-03-03淺談JavaScript中你可能不知道URL構(gòu)造函數(shù)的屬性
這篇文章主要介紹了淺談JavaScript中你可能不知道URL構(gòu)造函數(shù)的屬性,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-07-07仿google adsense顏色選擇器代碼,從中易廣告聯(lián)盟程序提取
仿google adsense顏色選擇器代碼,從中易廣告聯(lián)盟程序提取...2007-11-11Javascript中設(shè)置默認(rèn)參數(shù)值示例
這篇文章主要介紹了Javascript中默認(rèn)參數(shù)值的設(shè)置,很簡單,但很實(shí)用,需要的朋友可以參考下2014-09-09