手把手教你写 js 动画
相信大家对这种数字渐变动画效果应该不面生吧。接下来分析一下如何用 js 实现这个动画。
数字渐变动画的实现
仔细想想,这种数字变化不就是在一段时间内从 0 到 5000 吗。换个思维,这不就是 从 0 到 1 的过程(也可以说是 从 0% 到 100%)。动画是需要时间的,在指定的时间内,数字从 0 到 1,是不是可以抽象成时间逐渐流逝?换句话说不就是时间消耗完了?那么数字从 0 到 1 的过程,不就可以转变为时间从开始到结束的过程。接着这个思路,就可以把动画抽象成,当前时间过了百分之多少,再拿这个百分比做相关转换不就可以了。接下来实现这个数字渐变。
<div class="box">0</div>
<script>
const oBox = document.querySelector('.box')
const startTime = + new Date
let timer = null
function step() {
const percent = Math.min(1, (+new Date - startTime) / 5000)// 动画时间为 5s
if (percent < 1) {
oBox.innerHTML = ~~(5000 * percent)
timer = requestAnimationFrame(step)
} else {
oBox.innerHTML = ~~(5000 * 1)
cancelAnimationFrame(timer)
}
}
timer = requestAnimationFrame(step)
</script>
可以看到动画效果和预期是一致的,说明通过时间消耗的多少来做动画的思路是对的。
实现 Animator 类
总结上面数字动画的实现方式,我们可以实现这样一个动画类,它可以设置 动画时长,并在 动画过程 中主动调用 onUpdate
函数,在动画完成后,调用 onComplete
函数。
class Animator {
constructor() {
this.durationTime = 0
this.eventHandlers = new Map()
}
duration(time) {
if (typeof time !== 'number') {
throw new Error('Duration must be a number')
}
this.durationTime = time
return this
}
on(type, handler) {
if (typeof handler !== 'function') {
throw new Error('Handler must be a function')
}
this.eventHandlers.set(type, handler)
return this
}
animate() {
const duration = this.durationTime
const update = this.eventHandlers.get('update') || (t => t)
const complete = this.eventHandlers.get('complete') || (() => {})
let timer = null
const startTime = +new Date()
function step() {
const percent = Math.min(1, (+new Date() - startTime) / duration)
if (percent < 1) {
update(percent)
timer = requestAnimationFrame(step)
} else {
cancelAnimationFrame(timer)
update(1)
complete()
}
}
timer = requestAnimationFrame(step)
}
}
使用该类也可以实现上面数字渐变动画:
const oBox = document.querySelector('.box')
new Animator()
.duration(3000)
.on('update', t => (oBox.innerHTML = ~~(t * 5000)))
.on('complete', () => alert('ok'))
.animate()
增强 Animator 类
相信很多前端同学对 easing
、easing-in-out
都不陌生,那么如何用 js 实现类似的动画效果呢?这其中的奥秘就是在于 缓动函数。我们通过将动画转变为时间流逝的百分比来做动画,时间的流逝是线性的,可以想象从 0 到 1 时间的曲线是不变的,但是我们可以用时间流逝百分比 乘以某个函数,只要该函数能保证从 0 到 1 仍旧是从 0 到 1,中间百分比变化我们就可以不管。举个例子: y = x * x,当 0 <= x <=1
的时候,y 的结果便还是 0 到 1 之间,很显然 x => x * x,就是一个缓动函数。
增强 Animator 类:
class Animator {
constructor() {
this.durationTime = 0
this.easingFn = k => k
this.eventHandlers = new Map()
}
easing(fn) {
if (typeof fn !== 'function') {
throw new Error('Easing must be a function, such as k => k')
}
this.easingFn = fn
return this
}
duration(time) {
if (typeof time !== 'number') {
throw new Error('Duration must be a number')
}
this.durationTime = time
return this
}
on(type, handler) {
if (typeof handler !== 'function') {
throw new Error('Handler must be a function')
}
this.eventHandlers.set(type, handler)
return this
}
animate() {
const duration = this.durationTime
const easing = this.easingFn
const update = this.eventHandlers.get('update') || (t => t)
const complete = this.eventHandlers.get('complete') || (() => {})
let timer = null
const startTime = +new Date()
function step() {
const percent = Math.min(1, (+new Date() - startTime) / duration)
if (percent < 1) {
update(easing(percent))
timer = requestAnimationFrame(step)
} else {
cancelAnimationFrame(timer)
update(easing(1))
complete()
}
}
timer = requestAnimationFrame(step)
}
}
使用 :
const oBox1 = document.querySelector('.box1')
const oBox2 = document.querySelector('.box2')
const oBox3 = document.querySelector('.box3')
new Animator()
.duration(3000)
.easing(x => k * k)
.on('update', t => oBox1.style.left = t * 500 + 'px')
.on('complete', () => alert('ok'))
.animate()
new Animator()
.duration(3000)
.easing(k => (1 - --k * k * k * k))
.on('update', t => oBox2.style.left = t * 500 + 'px')
.on('complete', () => alert('ok'))
.animate()
new Animator()
.duration(3000)
.easing(k => 1 - Math.sqrt(1 - k * k))
.on('update', t => oBox3.style.left = t * 500 + 'px')
.on('complete', () => alert('ok'))
.animate()
更多好玩的 easing 函数,推荐看 tween.js 的缓动函数。