根據經典力學的觀點,世界上全部的原子每時每刻彷彿都會根據當前速度、受力和位置計算出下一刻的速度、受力和位置。上帝有一臺超級計算機嗎?非也,反而計算機是咱們利用原子的這些特性拼裝出來的。如今,咱們卻要用計算機,像上帝那樣,再造一個世界。css
我不知道這個世界上有沒有「仿世學」,可是既然動畫是要模仿現實世界,那麼實現動畫的根本方法就是借鑑上帝的辦法——模擬天然規律。本文以 React Motion 實現原理爲背景,介紹一種通用的模擬物理規律的方法,以及如何使用這種方法實現 React Motion 的緩動函數。讓咱們來當一回上帝吧。html
動畫的原理看似複雜,其實就是每幀不停得渲染。一張靜態頁面的渲染就是在一幀中渲染。如何渲染每一幀呢?咱們能夠用最簡單,同時也是最繁瑣的方法,就像最原始的動畫片那樣,寫 n 張靜態頁面,而後每隔一幀切換一張。react
假如咱們已經勤奮地寫好了 P_1, P_2, ... P_n 這 n 張頁面,咱們用它來實現一個簡單的動畫:git
// pages: [P1, P2, P3 ... Pn]; const pageCount = pages.length; const startAnimation = (currPageIndex) => { if (currIndex === pageCount) { return ; } document.body.innerHTML = (pages[currPageIndex++]); setTimeout(startAnimation.bind(null, currPageIndex), frameTime) } startAnimation(0);
用這種方法有着顯而易見的問題:github
寫 n 張頁面頁面渲染效率十分低下。web
每次從新設置 body.innerHTML,性能過低了。spring
咱們來逐個解決上述問題。api
每一幀的界面都遵循必定的規律,類似性很高,中間必然有不少重複勞動。既然是重複勞動,咱們能夠放心的交給計算機去完成。寫一個渲染函數,只須要向這個函數描述一下當前頁面的信息,這個函數就能把頁面給渲染出來。瀏覽器
能夠用局部更新的方式來取代塊更新,其中 React 的 Virtual DOM 更新方便地解決了這個問題。ide
咱們再以一個左右切換的 toggle 動畫爲例,寫一個渲染函數:
const render = x => ` <div class="toggle-slider"> <div class="toggle-box" style="transform: translate3d(${x}, 0, 0)"> </div> `
有了這個函數以後,只須要告訴它 x 的當前值,新的頁面就開始自動繪製了。因爲 toggle 的運動規律,x 的值也不用手動依次給出,咱們仍然能夠寫一個自動計算 x 的函數。這個自動計算 x 的函數,或者說計算頁面狀態的函數,就是緩動函數。
假設這個 toggle 是勻速運動的,緩動函數即可以寫成這樣:
$$ distance(總路程) = endX - beginX $$
$$ v = \frac{distance}{duration(總時間)} $$
$$ x = v \cdot t + beginX $$
用代碼來表示,
const cal = (beginX, endX, duration, beginTime) => { const now = performance.now(); const passedTime = now - beginTime; return (endX - beginX) / duration * passedTime + beginX; }
最後完成這個 toggle 動畫:
const beginX = 0; const endX = 300; const duration = 5000; const frameTime = 1000 / 60; let beginTime = performance.now(); const startAnimation = () => { const currX = cal(beginX, endX, duration, beginTime); document.body.innerHTML = render(currX); setTimeout(startAnimation, frameTime); } startAnimation(0);
能夠看到,上述章節使用 setTimeout 來模擬時間的逝去,然而瀏覽器爲動畫過程提供了一個更爲專一的 API - requestAnimationFrame
。
const update = now => { // calculate new state... // rerender here... raf(update); }; raf(update);
raf 使用起來就像 setTimeout 同樣,但有如下優勢:
全部註冊到 raf 中的回調,瀏覽器會統一管理, 在適當的時候一同執行全部回調。
當頁面不可見,例如當前標籤頁被切換,隱藏在後面的時候,爲了減小終端的損耗,raf 就會暫停。(若是像 jQuery 那樣, 使用 setTimeout 實現動畫,此時頁面就會進行沒有意義的重繪)。
raf 的這個特性,還能夠利用在實時模塊中,讓標籤頁隱藏時中止發請求。
在開始使用 raf 前,咱們須要一個 raf 的 polyfill ,好比 chrisdickinson/raf
而後,咱們嘗試用 React 和 raf 來重構一次 Toggle 動畫。在數據上,用中介者模式實現一個簡單的單向數據流:
const createStoreX = initialX => { let currX = initialX; let listeners = []; return { getX: () => currX, subscribe: listener => { listeners = [...listeners, listener]; }, changeX: newX => { currX = newX; listeners.forEach(listener); } }; } const finalCreateStoreX = (createStoreX => initialX => { const store = createStoreX(initialX); return { ...store, changeX: newX => { store.changeX(newX); } }; })(createStoreX); const store = finalCreateStoreX(0); const View = x => ( <div className="toggle-slider"> <div className="toggle-box" style={{ transform: `translate3d(${x}, 0, 0)` }}> </div> ); class Page extends React.Component { handleChangeX = () => { this.setState({ x: storeX.getX() }) } componentDidMount = () => { storeX.subscribe(this.handleChangeX) } render = () => <Page><View x={this.state.x} /></Page> } const startAnimation = (beginPos = 0, endPos = 300, duration = 5000, frameTime = 17) => { const now = performance.now(); const loop = () => { const passedTime = performance.now() - now; const distance = endPos - beginPos; const currX = distance/duration*passedTime + beginPos; storeX.changeX(currX); } setTimeout(loop, frameTime); }; reactDOM.render(<Page />, document.body)
有沒有以爲很棒!但仍然有優化的空間。動畫是源自現實世界的,人類早已習慣了一個變速運動的物理環境,這樣的一個勻速動畫會讓人相對感受不適。爲了優化用戶體驗,React Motion 使用了一種常見的變速運動 —— 彈簧運動。
React Motion 使動畫看起來像一個彈簧那樣(一個有空氣阻力的彈簧,若是沒有空氣阻力,彈簧就會不停地作簡諧運動了)。你們能夠嘗試使用 React Motion 的spring-parameters-chooser,配置一個合適的勁度係數和空氣阻力。彈簧動畫可使網站增添一些俏皮的元素,讓用戶體驗起來更加舒暢!
下面就讓咱們進入主題,開始解讀 React Motion 的緩動過程。
先模擬彈簧的物理規律,實現彈簧動畫。
假設有一個彈簧,彈簧上綁了一個砝碼,回到初中物理,根據胡克定律,砝碼的受到彈簧的拉力爲:
$$ F_{spring} = k\varDelta{x} (k爲彈簧的勁度係數)$$
咱們假設該砝碼受到的空氣阻力 kdamping 與砝碼當前的速度 vt 呈正相關,其中阻尼係數爲 kdamping 。
對砝碼進行受力分析得:
$$ F = F_{spring} - F_{damping} = k_{spring}\varDelta{x} - k_{damping} \times v_{t} $$
設 at 爲砝碼當前加速度,得:
$$ F = ma_t $$
設 v' 和 x' 分別爲通過 $$ dt $$ 時間後,砝碼新的速度和位移,得:
$$ a_t = \lim_{dt \to 0} \frac{dv}{dt} = \lim_{dt \to 0} \frac{v^{'} - v_t}{dt} $$
$$ v_t = \lim_{dt \to 0} \frac{dx}{dt} = \lim_{dt \to 0} \frac{x^{'} - x_t}{dt} $$
即:
$$ v^{'} = \lim_{dt \to 0} a_t*d_t + v_t $$
$$ x^{'} = \lim_{dt \to 0} v_t*d_t + x_t $$
咱們拿到了計算新狀態的公式,可是 dt 是無限趨近於 0,怎麼去模擬這個無限趨近於 0 呢?
如今只知道,當 dt 越趨近於 0 時,等式兩邊的值越接近(極限的單調有界準則可證)。能夠把 dt 設爲一個很是小的常量,雖然會形成必定的偏差,可是不足爲慮,只要騙過人類的眼睛就能夠了。
這樣咱們就能夠計算得出 v' 和 x' 。對以上過程不斷重複,就能計算出任意時刻的位移和速度。
這是個通用的模擬物理規律的緩動過程,是否讓你茅塞頓開?看一個一樣的模擬物理規律的動畫,有沒有手癢?
可是,原諒我又說了 「可是」,若是咱們要用 raf 實現這個緩動的話,raf 不能設置 callback 的延遲時間,而咱們的 dt 是一個固定的很是小的常量。這種狀況下,怎麼計算新的狀態呢?
咱們設 raf callback 的延遲時間爲 Δt ,第二部分已經說過,這個 Δt 是瀏覽器本身決定的。
無論 Δt 是多少,能夠用幾個緩動過程連續疊加(一個緩動過程的時間是 dt )來拼湊出 Δt 。
不過 Δt 每每不是 dt 的整數倍,對於最後多出來的一小塊時間,咱們能夠取一個比例值。
const dt = 1000 / 60; let preTime = 0 , initialState = { currX: -250, currV: 0, } const update = () => { const currTime = performance.now(); const deltaTime = currTime - preTime; const steps = deltaTime / dt; const multiObj = (obj, k) => { return Object.keys(obj).reduce((res, key) => { return { ...res, [key]: obj[key] * k } }, {}) }; const getCurrState = (prevState, steps) => { if (steps < 1) { return multiObj(cal(prevState), steps) } return getCurrState(cal(prevState), steps - 1) }; render(getCurrState(initialState, steps)) raf(update); } update()
CSS 動畫與 JS 動畫的區別是,使用 CSS 動畫,不須要寫緩動過程。好比在 transition 中,可使用現成的 cubic bizier 的緩動(其中 ease, ease-in, ease-out 等都是特定參數值的 cubic bizier)。
(值得一提的是,transition的實現也使用了 raf 的機制,當標籤頁被切換時, transition 動畫也會暫停,你們不妨試一試)
CSS 的 animation 使用設置關鍵幀的方式實現動畫,適合完成多步、往返或者不斷重複的動畫。
那麼咱們何時須要 JS 動畫呢——當你對 CSS 提供的緩動函數不滿意的時候。打個比方,若是想實現像淘寶網在加購成功後,讓商品 logo 沿着弧線運動的動畫。
React Motion 所作的事,只不過本身實現了一套緩動函數。若是你不關心緩動過程,用 CSS 動畫能夠直接替換。
至於 React 當中的 ReactCSSTransitionGroup,是React提供的支持列表動畫的 API 。試想一下,當渲染函數發現新的列表狀態中,消失了某一項。那麼要繪製這一項消失的動畫,必須先讓這一項暫存在 DOM 中,直到動畫結束,再從 DOM 消失。這個實現起來比較麻煩,因此 React 提供了這個 API 幫助咱們實現動畫。值得注意的是,ReactCSSTransitionGroup 只是對列表的增與刪提供動畫支持。若是隻是對列表項進行修改,不要生硬的套用 ReactCSSTransitionGroup,本身在 state 中管理列表實現起來更加方便。