Skip to content

动画

本章节描述动画的概念以及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 })

示例

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
<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
  },
  {
    onUpdate(state) {
      boxRef.value!.style.transform = `translateX(${state.x}px) rotate(${state.rotate}deg)`
    }
  }
)

const handleStart = () => {
  tween2.to(
    { x: 200, rotate: 360 },
    {
      duration: duration.value,
      easingFunction: Tween.easing[tweenFn.value]
    }
  )
}
const handleBack = async () => {
  tween2.to(
    { x: 0, rotate: 0 },
    {
      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
})

MIT Licensed