需求
借助JavaScript,实现网页动画效果
代码
简易Demo:es/js_animation_test.html
Anikyu源代码仓库:anikyu
先说几句
本人不才,大学期间搞过各种各样的设计类软件,包括做动画的Flash、After Effects、Premiere,做3D的3ds Max、Cinema 4D;终于,在大学的第三年,专业开了网页设计课(大二时其实我也有水过一个做网页的校选修),我又不慎接触了做网页的Dreamweaver。Adobe全家桶齐了。
要学做网页,就得学HTML;要把网页写好看,就得学CSS;要让网页有点生气,就得学JavaScript;这不就蹚入了前端这潭浑水。
// 其实本人并不认可自己是程序员,最多是会写网页的设计师
大学期间,本人学的是数字媒体技术专业,计算机学院里的专业,C、C++、数据结构、数据库概论、计算机图形学等课程都有上过,外加游戏原画、Flash、三维设计一类的偏向设计类的课程,然后自己又稍稍接触了一点点PHP。曾经我想,要不未来做UI/视频/3D设计师吧;但最终找工作的时候,认真思考了一下,专业课设计类课程似乎支持不了我的一些野心,外加我比较菜,大学期间所有软件都接触很浅;到后来一直不知道3D模型贴图咋贴,就试着通过学Three.js来学3D。所以就暂时先试着用Dreamweaver这个技能来找工作吧。
至于设计,我认为总有一天我会回来的~
论网页与传统动画的联系
本人认为,网页与传统动画一样,都是能够令人产生愉悦的可以用于观赏的产品;但比传统动画更强大的是,网页能够与用户直接产生交互。
专业动画设计软件里,动画的实现一般是逐帧动画或是关键帧动画。
- 逐帧动画
创作者每一帧绘制一次,最终将多帧连续播放,实现动画。
- 关键帧动画
创作者在每个关键点绘制一帧,最终由程序在两个关键帧之间进行插值,得到中间帧。插值可以是线性插值,也可以是包含缓动的插值。
对于网页动画,在早期,若动画效果很复杂,甚是精美,一般都直接使用Flash等插件来制作;简单的动画则使用JavaScript中的setInterval()来实现,即每隔若干毫秒(保持性能的同时不让用户感到卡顿)对某一对象某一数值进行一次增加,请求一次动画帧,直到该对象该值的量到达预想值,由此实现动画。jQuery中的animate()对其进行了封装。同时还有SVG动画,使用animate标签来进行实现。随着CSS3的出现,DOM元素动画/过渡效果可以使用CSS实现。
当然,CSS动画的使用也有一些局限性:
- 不能够灵活控制动画播放进度
- 仅可以支持DOM元素动画
最近在学习Three.js,其核心库似乎不包含动画。其官方示例有使用Tween.js这个动画库,但由于我比较想试着自己实现一个类似的动画库(其实是因为它的用法和我的想法不太相符)。
因此,我自己封装了一个基于JavaScript实现的动画库 —— Anikyu 。
当然本文先并不着重介绍我所封装的库。对于其使用方法,请直接看anikyu的仓库。
网页的维度
我们都知道,网页有两个维度:宽度和高度。
如果要用到动画,那就再加个时间。
(好吧,如果非要说WebGL或是CSS 3D等等三维技术,那再加个深度;此处不讨论)。
要让一个对象会动,就必须引入时间这个概念;因为时间点是一个点,其中包含的是对象在在当前时间点的状态;既然要让对象会动,就得找下一个时间点,最终连点成线,就产生了一个时间轴,这样就有了时间这个维度。
如何在两个时间点之间进行基本的补间?
现在我们把两个时间点之间的变化过程看成一个整体,开始时间点动画进度为0,结束时间点动画进度为1。
计算过程
设开始时间为64,结束时间为1024,当前时间为256,那么:
总时间差 = 结束时间 - 开始时间 = 1024 - 64 = 960
当前动画进度 = (当前时间 - 开始时间)/总时间差 = (256 - 64)/960 = 0.2 ;
设开始值为222,结束值为666,那么:
当前时间点(相对于开始值)的增量
= (结束值 - 开始值)*当前动画进度
= (666 - 222)*0.2
= 88.8
因此在该时间点下:
当前对象的值 = 开始值 + 当前时间的增量 = 222 + 88.8 = 310.8
之后使用定时器执行上方的步骤,每次时间改变,重新计算一次进度和时间点增量,并赋值给目标对象,直到当前动画进度达到1,此时动画完成。
加入缓动效果
在上面的代码中,我们已经实现了最简单的补间,即匀速补间。但在很多情况下,仅有匀速补间是不够的,例如我们可能要实现一个动画运行先快(慢)后慢(快)/中间快(慢)两头慢(快)/稍微出去一点再回来/弹跳等诸如此类的小效果。在After Effects、Cinema 4D等设计类软件时间轴面板上,我们可以对缓动函数进行可视化的编辑,从而我们可以直观地看到缓动函数的曲线。
与此类似,借助CSS3所提供的transition-timing-function属性,我们也可以直接在CSS中实现缓动动画,其属性值包括:linear、ease、ease-in、ease-out、ease-in-out,外加这五个属性值所基于的cubic-bezier(n,n,n,n) —— 三次贝塞尔曲线。
CSS3 所提供的缓动函数已经基本满足我们日常的一些动画需求,即上文所提及的先快(慢)后慢(快)/中间快(慢)两头慢(快)/稍微出去一点再回来;但对于弹跳效果,由于CSS3 缓动动画仅支持两点之间的简单缓动,而弹跳效果在动画过程中无法使用CSS3 所支持的三次贝塞尔曲线来表达(弹跳动画包含多个转折点),因此无法使用纯CSS来实现弹跳。
但在JavaScript中,我们可以自己编写缓动函数。
缓动函数参见 ECharts 示例页面
In | Out | InOut |
---|---|---|
quadraticIn | quadraticOut | quadraticInOut |
cubicIn | cubicOut | cubicInOut |
quarticIn | quarticOut | quarticInOut |
quinticIn | quinticOut | quinticInOut |
sinusoidalIn | sinusoidalOut | sinusoidalInOut |
exponentialIn | exponentialOut | exponentialInOut |
circularIn | circularOut | circularInOut |
elasticIn | elasticOut | elasticInOut |
backIn | backOut | backInOut |
bounceIn | bounceOut | bounceInOut |
函数曲线示例:
如果自行百度,可以发现这些补间函数基本都是大同小异的。它们都接受一个值 当前动画播放实际进度k,返回一个经过处理的进度值 k2 用以供计算函数算出当前时间点的变化量。
接上一步的计算过程
假设当前动画状态以及所给定值和上一步一样,缓动函数使用的是bounceIn,则:
当前时间点(相对于开始值)的增量
= (结束值 - 开始值) * 缓动函数(当前动画进度)
= (666 - 222)*bounceOut(0.2)
= 134.31
因此在该时间点下:
当前对象的值 = 开始值 + 当前时间的增量 = 222 + 134.31 = 356.31
加入定时器以实现动画
正如上文所提及,动画的存在依赖于时间这个维度。一个时间点表示一个时间点的状态,那如何得到动画播放期间每一个时间点的状态呢?
答案就是定时器。定时器可以让浏览器每间隔一段时间来执行一段函数。在早期浏览器(IE9或更低版本)中仅能够使用setInterval()来实现不断地请求动画,新版本浏览器中则引入了专门用于创建动画的requestAnimationFrame()。
requestAnimationFrame与setInterval区别在于:
- requestAnimationFrame无需手动指定更新间隔时间,其更新跟随屏幕的更新而更新(由屏幕刷新率来确定),这种更新机制类似CSS3 动画的更新;setInterval需要手动指定更新间隔时间,其更新由定时器来控制。这使得使用setInterval请求动画时可能会因来不及绘制而发生丢帧或过度绘制而产生性能问题,而requestAnimationFrame能够更加精准控制动画的绘制。
- 在页面不可见时,requestAnimationFrame会暂停执行,而setInterval只要不被清除将会一直执行。这导致setInterval的运行将十分耗电。
Anikyu包含了requestAnimationFrame的 Polyfill,以用于支持IE9浏览器。
通过定时器不断执行下列操作:
获取到当前时间 - 将当前时间和开始时间进行一系列计算得到当前时间点状态 - 将状态赋值给原始对象
即可实现动画。
代码
先定义两个函数
function clamp(value, min, max) {
return Math.max(min, Math.min(max, value));
}
clamp函数用于钳制数值范围。由于当前动画进度的值必定是一个[0,1]区间的值,因此我们必须让动画进度限制在该范围内,否则可能会导致最终效果值小于/大于预期值
function bounceOut(k) {
if (k < (1 / 2.75)) { return 7.5625 * k * k; }
else if (k < (2 / 2.75)) { return 7.5625 * (k -= (1.5 / 2.75)) * k + 0.75; }
else if (k < (2.5 / 2.75)) { return 7.5625 * (k -= (2.25 / 2.75)) * k + 0.9375; }
else { return 7.5625 * (k -= (2.625 / 2.75)) * k + 0.984375; }
}
bounceOut函数是上文所提及的30个缓动函数之一,可在动画进度快要结束时产生弹回的效果。将当前动画进度传入后,可以获得一个经过处理的进度,在实际计算时根据该进度得到当前时间点状态,实现变速运动。若当前动画进度不经过该函数处理,则动画为匀速运动。
获取动画对象,并定义初始值、结束值,获取动画起始时间
let el = document.getElementById('el')
let init = 222;
let end = 666;
let timeDelta = 960;
let startTime = Date.now()
编写动画函数
function animate() {
interval = requestAnimationFrame(animate)
let loop = () => {
let currentProgress = (Date.now() - startTime) / timeDelta
currentProgress = clamp(currentProgress, 0, 1)
let sumNumber = (end - init) * bounceOut(currentProgress)
// let sumNumber = (end - init) * currentProgress
let currentStatus = init + sumNumber
if (currentProgress === 1) {
cancelAnimationFrame(interval)
el.style.transform = `translate(${end}px)`
} else {
el.style.transform = `translate(${currentStatus}px)`
}
}
loop()
}
animate()
以上就是我对JavaScript动画的理解。借此机会,我也封装了一个上文所提及的动画库,Anikyu。
Anikyu是本人春节期间在家,正好也是疫情期间,做这个动画demo时突然想封装的一个库。正巧借此机会也学习了一下ES6没用过的特性、Webpack、Babel,同时也将该库发布到了 npm 。
各位要是觉得靠谱不妨来用一下,支持IE 9+浏览器,支持Node.js环境。
我也尝试将Anikyu代码结构、开发过程写一篇新的博客:
尝试通过封装一个库来学习JavaScript(ES6)相关特性以及相关构建工具]
(#7)
可能有点多,我想起来的时候慢慢补充。