动画
本章节描述动画的概念以及cat-kit提供的动画API的使用方法
概述
给出状态的起始到结束, 然后自动补出起始到结束中间状态的动画叫做补间动画.
定义一个补间动画我们使用以下最简单的代码来描述
ts
// 下述代码描述了从起始状态0到结束状态100并且持续时间为300的补间动画
tween({
start: 0,
end: 100,
duration: 300
})
一般一个流畅的动画具备的要素有如下几点:
- 保持较高且稳定的帧率, 一般至少要30帧, 60帧是一个较流畅的标准帧数. 现代计算机的显示器帧率普遍达到60, 120的刷新率在手机和电脑上逐渐普及.
- 由于动画是在短时间内进行大量渲染的一个过程, 因此动画应尽可能的减少渲染范围, 遵循局部渲染原则即只渲染可动的物体.
- 动画执行API应尽可能的轻量以减少CPU的负担
cat-kit提供了一套简洁流畅可扩展的补间动画API, 助力开发者以最少的成本来实现更多的动画效果.
Tween API
Tween是一个类用来新建一个补间动画实例, 以下是该类的使用方式
ts
interface AnimeConfig<State> {
/** 动画持续时间, 单位毫秒 */
duration?: number
/** 缓动函数 */
easingFunction?: (progress: number) => number
/** 动画完成后的回调 */
onComplete?(state: State): void
}
interface TweenConfig<State> {
/** 动画持续时间, 单位毫秒 */
duration?: number
/** 每一帧状态更新时的回调 */
onUpdate?(state: State): void
/** 动画完成后的回调 */
onComplete?(state: State): void
/** 缓动函数 */
easingFunction?: (progress: number) => number
}
interface Tween<State> {
new (
state: State,
config?: TweenConfig<State>
): {
state: State
to(state: State, config?: AnimeConfig<State>): Promise<state>
}
/** 提供的默认缓动函数 */
easing: {
linear: (progress: number) => number
easeInQuad: (progress: number) => number
easeOutQuad: (progress: number) => number
easeInOutQuad: (progress: number) => number
easeInBack: (progress: number) => number
easeOutBack: (progress: number) => number
easeInOutBack: (progress: number) => number
}
}
// 新建Tween实例
// tween状态可以传入多个状态
const tween = new Tween(
{ x: 0, y: 0 },
{
// 动画持续1000毫秒
duration: 1000,
// 定义一个缓动函数, 默认为linear线性匀速
easingFunction: Tween.easing.easeInQuad
}
)
// 开始运动
tween.to({ x: 100, y: 100 })
示例
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
<template>
<div>
<div>
运动方式
<label>
<input type="radio" value="linear" v-model="tweenFn" />
线性
</label>
<label>
<input type="radio" value="easeInQuad" v-model="tweenFn" />
先慢后快
</label>
<label>
<input type="radio" value="easeOutQuad" v-model="tweenFn" />
先快后慢
</label>
<label>
<input type="radio" value="easeInOutQuad" v-model="tweenFn" />
先慢后快再慢
</label>
<label>
<input type="radio" value="easeInOutBack" v-model="tweenFn" />
回弹
</label>
</div>
<div>运动时长: <input type="text" v-model.number="duration" /></div>
<br />
<div>
<div>
数字补间: {{ n(tween.state.number).fixed({ maxPrecision: 2 }) }}
</div>
<input v-once type="text" v-model.number="number" />
</div>
<div>
<div>
物体运动
<v-button @click="handleStart">前进</v-button>
<v-button @click="handleBack">后退</v-button>
</div>
<div class="box" ref="boxRef"></div>
</div>
</div>
</template>
<script lang="ts" setup>
import { Tween, n } from '@cat-kit/fe'
import { reactive, shallowRef, watch } from 'vue'
const tweenFn = shallowRef('linear')
const duration = shallowRef(1000)
const number = shallowRef(0)
const tween = new Tween(
reactive({
number: number.value
}),
{
onUpdate(state) {
console.log(state.number)
}
}
)
watch(number, n =>
tween.to(
{ number: n },
{ easingFunction: Tween.easing[tweenFn.value], duration: duration.value }
)
)
// 物体运动
const boxRef = shallowRef<HTMLDivElement>()
let tween2 = new Tween(
{
x: 0,
rotate: 0,
radius: 0,
r: 255,
g: 0,
b: 0,
scaleY: 1
},
{
onUpdate(state) {
boxRef.value!.attributeStyleMap.set(
'transform',
`translateX(${state.x}px) rotate(${state.rotate}deg) scaleY(${state.scaleY})`
)
boxRef.value!.attributeStyleMap.set(
'border-radius',
`${state.radius < 0 ? 0 : state.radius}%`
)
boxRef.value!.attributeStyleMap.set(
'background-color',
`rgb(${Math.abs(state.r)},${Math.abs(state.g)},${Math.abs(state.b)})`
)
// boxRef.value!.style.transform = `translateX(${state.x}px) rotate(${state.rotate}deg)`
}
}
)
const handleStart = () => {
tween2.to(
{ x: 200, rotate: 360, radius: 50, r: 0, g: 255, b: 0, scaleY: 0.3 },
{
duration: duration.value,
easingFunction: Tween.easing[tweenFn.value]
}
)
}
const handleBack = async () => {
tween2.back({
duration: duration.value,
easingFunction: Tween.easing[tweenFn.value]
})
}
</script>
<style scoped>
.box {
width: 50px;
height: 50px;
background-color: #f00;
}
</style>
拓展Tween API
Tween API提供了极少的方法, 这是故意为之的, 一方面, 更少的API意味着更易用, 更小的体积. 另一方面, 在补间动画领域, 已经有很强大的库GSAP, 如果你对动画的需求比较多, 比较复杂那么可以试试该库.
当然,这不是本节的核心. 当Tween API不能满足你的需求并且你也不想使用非常复杂的库时. 你可以像下面这样扩展
ts
import { Tween, type TweenConfig } from '@cat-kit/fe'
class CustomTween<State extends Record<string, number>> extends Tween<State> {
constructor(state: State, config: TweenConfig<State>) {
super(state, config)
}
myTo(state: State) {
this.raf({
tick: p => {
// 在这里实现每一帧的算法
},
onComplete: () => {},
duration: this.duration
})
}
}
const myTween = new Tween({ x: 0 })
myTween.myTo({ x: 100 })
或者你也可以自己实现缓动函数, 你可以在这个网站去查询各种缓动函数.
ts
/** 抖动缓动函数 */
const easeOutBounce = (progress: number) => {
const n1 = 7.5625
const d1 = 2.75
if (progress < 1 / d1) {
return n1 * progress * progress
} else if (progress < 2 / d1) {
return n1 * (progress -= 1.5 / d1) * progress + 0.75
} else if (progress < 2.5 / d1) {
return n1 * (progress -= 2.25 / d1) * progress + 0.9375
} else {
return n1 * (progress -= 2.625 / d1) * progress + 0.984375
}
}
const tween = new Tween({ x: 0 }, {
easingFunction: easeOutBounce
})