根据经典力学的观点,世界上全部的原子每时每刻仿佛都会根据当前速度、受力和位置计算出下一刻的速度、受力和位置。上帝有一台超级计算机吗?非也,反而计算机是咱们利用原子的这些特性拼装出来的。如今,咱们却要用计算机,像上帝那样,再造一个世界。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 中管理列表实现起来更加方便。