缓动是动效中不可避免要用到的东西,缓动函数是大部分动效计算的基础,其中就包括了 css 的动画属性。你可以通过这里来了解一些缓动函数和它的实际效果。缓动函数,说的简单点,就是通过时间计算当前时间点上的值。经过抽象之后,缓动函数的计算结果是一个比例,用这个比例,去乘以对应的实际值,就是该时间点上的应得值。
缓动函数
上面是一个 sin 函数的曲线,假如一个人的身高和年龄的关系按照 sin 函数来长,年龄小的时候长的慢,青年时期长的很快,年龄再大又不长。假设身高为 h,年龄为 t,那么 h = easeInSin(t). easeInSin 的 js 实现就是今天我们这篇文章的主角。这里可以告诉你答案,easeInSin = t => 1 + Math.sin(Math.PI / 2 * t - Math.PI / 2)
这便是一个缓动函数。
你一眼看过去,觉得有意思,但是不知道具体怎么应用。现在,你来想象,你现在就是用 canvas 来画这个人,他的身高和胖瘦都是按照 easeInSin 来变化,你的 canvas 画布高度为 200,你的人现在 20 岁,你这个人现在在画布中的身高为 172,现在,让你用 canvas 动画,实现这个人从一个受精卵长大成现在这个身高,你是不是就有思路了。假设时间每过 1 秒相当于这个人年长 1 岁,那么你需要通过 setTimeout 来得到一个 20 秒的计时器,每一个秒刻度时,需要通过 easeInSin 函数算出这个时候这个人应该多高,具体翻译为:h = easeInSin(t) * 172, 乘以 172 就得到当前岁数对应的身高了,然后在通过 js 重绘人物就行了。
现在回头看,是不是很简单。
接下来,我们都来了解一下都有哪些缓动函数。
一般来说,有 4 种形式的缓动函数,它们通过 h 值的增长速度(加速度)加以区分,只增不减的是 in 函数,只减不增的是 out 函数,先增后减的是 in-out 函数,先减后增的是 out-in 函数。你可以通过这里研究不同缓动函数的图形。一般来说,in 和 out 函数相对应,将 in 函数图形旋转 180 度之后,就得到 out 函数图形。in-out 和 out-in 函数相对,将 in-out 函数旋转 90 度之后,对称翻转,就得到 out-in 函数图形。一般,out-in 函数相对更难实现。
补间值计算
那么有没有现成的库呢?比较知名的 Tween.js 内置了缓动函数,但是,它太过复杂,并非我想要的。我从这里得到了很多高手无私贡献的缓动函数,经过整理之后,撰写了一个类,用来进行补间值的计算。
import Etx from 'etx' class Motion extends Etx { constructor(options = {}) { super() const { type = 'linear', start = 0, end = 1, duration = 0, loop = false } = options this.type = type this.start = start this.end = end this.duration = duration this.loop = loop this.current = start this.status = -1 this.time = 0 } animate() { if (this.status < 1) { return } requestAnimationFrame(() => { const currentTime = Date.now() const t = (currentTime - this.time) / this.duration const tw = t > 1 ? 1 : t < 0 ? 0 : t const easing = Motion.easings[this.type] const scale = easing(tw) const end = this.end const start = this.start const value = (end - start) * scale + start this.current = value this.emit('update', value) if (tw === 1 && this.loop) { this.time = currentTime } else if (tw === 1) { this.stop() return } this.animate() }) } start() { if (!easings[this.type] || this.duration <= 0) { const value = this.end this.current = value this.emit('update', value) this.stop() return } if (this.status > 0) { return } if (this.status < 0) { this.time = Date.now() } this.status = 1 this.emit('start') this.animate() } pause() { this.status = 0 this.emit('pause') } stop() { this.status = -1 this.emit('stop') } } // https://gist.github.com/gre/1650294 // https://easings.net/ Motion.easings = { // no easing, no acceleration linear: t => t, // accelerating from zero velocity easeInQuad: t => t*t, // decelerating to zero velocity easeOutQuad: t => t*(2-t), // acceleration until halfway, then deceleration easeInOutQuad: t => t<.5 ? 2*t*t : -1+(4-2*t)*t, // accelerating from zero velocity easeInCubic: t => t*t*t, // decelerating to zero velocity easeOutCubic: t => (--t)*t*t+1, // acceleration until halfway, then deceleration easeInOutCubic: t => t<.5 ? 4*t*t*t : (t-1)*(2*t-2)*(2*t-2)+1, // accelerating from zero velocity easeInQuart: t => t*t*t*t, // decelerating to zero velocity easeOutQuart: t => 1-(--t)*t*t*t, // acceleration until halfway, then deceleration easeInOutQuart: t => t<.5 ? 8*t*t*t*t : 1-8*(--t)*t*t*t, // accelerating from zero velocity easeInQuint: t => t*t*t*t*t, // decelerating to zero velocity easeOutQuint: t => 1+(--t)*t*t*t*t, // acceleration until halfway, then deceleration easeInOutQuint: t => t<.5 ? 16*t*t*t*t*t : 1+16*(--t)*t*t*t*t, // elastic bounce effect at the beginning easeInElastic: t => (.04 - .04 / t) * Math.sin(25 * t) + 1, // elastic bounce effect at the end easeOutElastic: t => .04 * t / (--t) * Math.sin(25 * t), // elastic bounce effect at the beginning and end easeInOutElastic: t => (t -= .5) < 0 ? (.02 + .01 / t) * Math.sin(50 * t) : (.02 - .01 / t) * Math.sin(50 * t) + 1, easeInSin: t => 1 + Math.sin(Math.PI / 2 * t - Math.PI / 2), easeOutSin: t => Math.sin(Math.PI / 2 * t), easeInOutSin: t => (1 + Math.sin(Math.PI * t - Math.PI / 2)) / 2, }
使用方法:
const motion = new Motion({ type: 'easeInElastic', start: 24, end: 60, duration: 2000, }) function render() { const height = motion.current document.querySelecotry('#some').style.height = height + 'px' } motion.on('update', render) motion.start()
上面定义了一个基于 easeInElastic 缓动函数的计算器,它的缓动起始位置为 24,结束位置为 60,整个动效过程时间是 2 秒。在定义好计算器之后,我们调用 start() 开始计算,通过 on 方法监听计算结果,并且修改某个元素的 height,这样,#some 这个元素的高度,就会从 24 开始缓缓涨到 60,完成整个动作过程。
比较秒的设计是,它提供了一个 pause 方法,可以用来暂停动画。
基于这样的方法,我们可以进行更为复杂的扩展,比如同时设置多个 css 属性之类的。
缓动效果
接下来我们来看看都有哪些缓动效果。总体上,我们将效果分为 4 大类:渐变、位移、转体、缩放。其中转体比较复杂,又可以分为:旋转、翻转、3D特效。
渐变相关的一般包括透明度、长宽、色值等。位移包括直线、曲线,可以通过 css 的位置相关属性来控制。
- fade
- to
- resize
- gradient
- tween
这些单词是在处理缓动效果时经常使用的,比如 fadeIn, fadeOut 等。
如何将缓动函数应用到缓动效果呢?其实很简单,只需要通过上面的缓动计算器修改对应的 css 属性值即可。例如,fadeIn 的实现:
const motion = new Motion({ duration: 0.5, }) function fadeIn() { const opacity = motion.current $('#some').css({ opacity }) } fadeIn() // 初始值 motion.on('update', fadeIn()) motion.start()
这样就完成了一个渐显的动画效果。但是实际上,我们很少直接用 js 去处理这种简单的效果,因为考虑到性能问题,每一次修改 css,都会让浏览器重绘,这会阻塞其他进程,如果计算过于复杂,就会造成卡顿。但是,在一些特殊的场合,我们无法通过简单的 css 来实现的时候,这也是一种方式。