缓动动画

深入学习Canvas缓动动画技术,掌握各种缓动函数的原理和应用。缓动动画是让动画更自然、更有表现力的关键技术。通过缓动函数控制动画的速度变化,可以创造出各种生动的效果。

什么是缓动

缓动(Easing)是指在动画过程中,物体运动速度的变化规律。

匀速 vs 缓动

类型特点效果
匀速速度恒定机械、生硬
缓动速度变化自然、生动
// 匀速动画
function linear(t) {
  return t
}

// 缓动动画(缓出)
function easeOut(t) {
  return t * (2 - t)
}

缓动函数原理

缓动函数接收一个时间进度(0-1),返回位置进度(0-1)。

数学公式

// t: 当前时间(0-1)
// b: 起始值
// c: 变化量
// d: 总时长

function easeInOutQuad(t, b, c, d) {
  t /= d / 2
  if (t < 1) return c / 2 * t * t + b
  t--
  return -c / 2 * (t * (t - 2) - 1) + b
}

常用缓动函数

二次缓动(Quad)

const quad = {
  easeIn: t => t * t,
  easeOut: t => t * (2 - t),
  easeInOut: t => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t
}

三次缓动(Cubic)

const cubic = {
  easeIn: t => t * t * t,
  easeOut: t => (--t) * t * t + 1,
  easeInOut: t => t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1
}

四次缓动(Quart)

const quart = {
  easeIn: t => t * t * t * t,
  easeOut: t => 1 - (--t) * t * t * t,
  easeInOut: t => t < 0.5 ? 8 * t * t * t * t : 1 - 8 * (--t) * t * t * t
}

五次缓动(Quint)

const quint = {
  easeIn: t => t * t * t * t * t,
  easeOut: t => 1 + (--t) * t * t * t * t,
  easeInOut: t => t < 0.5 ? 16 * t * t * t * t * t : 1 + 16 * (--t) * t * t * t * t
}

正弦缓动(Sine)

const sine = {
  easeIn: t => 1 - Math.cos(t * Math.PI / 2),
  easeOut: t => Math.sin(t * Math.PI / 2),
  easeInOut: t => -(Math.cos(Math.PI * t) - 1) / 2
}

指数缓动(Expo)

const expo = {
  easeIn: t => t === 0 ? 0 : Math.pow(2, 10 * (t - 1)),
  easeOut: t => t === 1 ? 1 : 1 - Math.pow(2, -10 * t),
  easeInOut: t => {
    if (t === 0) return 0
    if (t === 1) return 1
    if (t < 0.5) return Math.pow(2, 20 * t - 10) / 2
    return (2 - Math.pow(2, -20 * t + 10)) / 2
  }
}

圆形缓动(Circ)

const circ = {
  easeIn: t => 1 - Math.sqrt(1 - t * t),
  easeOut: t => Math.sqrt(1 - (--t) * t),
  easeInOut: t => t < 0.5
    ? (1 - Math.sqrt(1 - 4 * t * t)) / 2
    : (Math.sqrt(1 - Math.pow(-2 * t + 2, 2)) + 1) / 2
}

弹性缓动(Elastic)

const elastic = {
  easeIn: t => {
    if (t === 0) return 0
    if (t === 1) return 1
    return -Math.pow(2, 10 * t - 10) * Math.sin((t * 10 - 10.75) * (2 * Math.PI) / 3)
  },
  easeOut: t => {
    if (t === 0) return 0
    if (t === 1) return 1
    return Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * (2 * Math.PI) / 3) + 1
  },
  easeInOut: t => {
    if (t === 0) return 0
    if (t === 1) return 1
    if (t < 0.5) {
      return -(Math.pow(2, 20 * t - 10) * Math.sin((20 * t - 11.125) * (2 * Math.PI) / 4.5)) / 2
    }
    return (Math.pow(2, -20 * t + 10) * Math.sin((20 * t - 11.125) * (2 * Math.PI) / 4.5)) / 2 + 1
  }
}

回弹缓动(Back)

const back = {
  easeIn: t => {
    const c1 = 1.70158
    const c3 = c1 + 1
    return c3 * t * t * t - c1 * t * t
  },
  easeOut: t => {
    const c1 = 1.70158
    const c3 = c1 + 1
    return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2)
  },
  easeInOut: t => {
    const c1 = 1.70158
    const c2 = c1 * 1.525
    return t < 0.5
      ? (Math.pow(2 * t, 2) * ((c2 + 1) * 2 * t - c2)) / 2
      : (Math.pow(2 * t - 2, 2) * ((c2 + 1) * (t * 2 - 2) + c2) + 2) / 2
  }
}

弹跳缓动(Bounce)

const bounce = {
  easeIn: t => 1 - bounce.easeOut(1 - t),
  easeOut: t => {
    const n1 = 7.5625
    const d1 = 2.75
    
    if (t < 1 / d1) {
      return n1 * t * t
    } else if (t < 2 / d1) {
      return n1 * (t -= 1.5 / d1) * t + 0.75
    } else if (t < 2.5 / d1) {
      return n1 * (t -= 2.25 / d1) * t + 0.9375
    } else {
      return n1 * (t -= 2.625 / d1) * t + 0.984375
    }
  },
  easeInOut: t => t < 0.5
    ? (1 - bounce.easeOut(1 - 2 * t)) / 2
    : (1 + bounce.easeOut(2 * t - 1)) / 2
}

缓动函数可视化

缓动函数曲线

缓动动画类

class Tween {
  constructor(options) {
    this.target = options.target
    this.property = options.property
    this.from = options.from
    this.to = options.to
    this.duration = options.duration || 1000
    this.easing = options.easing || (t => t)
    this.onUpdate = options.onUpdate
    this.onComplete = options.onComplete
    
    this.startTime = null
    this.running = false
  }
  
  start() {
    this.startTime = performance.now()
    this.running = true
    this.tick()
    return this
  }
  
  tick() {
    if (!this.running) return
    
    const elapsed = performance.now() - this.startTime
    const progress = Math.min(elapsed / this.duration, 1)
    const easedProgress = this.easing(progress)
    
    const value = this.from + (this.to - this.from) * easedProgress
    this.target[this.property] = value
    
    if (this.onUpdate) {
      this.onUpdate(value, progress)
    }
    
    if (progress < 1) {
      requestAnimationFrame(() => this.tick())
    } else {
      this.running = false
      if (this.onComplete) {
        this.onComplete()
      }
    }
  }
  
  stop() {
    this.running = false
    return this
  }
}

const ball = { x: 50 }
new Tween({
  target: ball,
  property: 'x',
  from: 50,
  to: 350,
  duration: 1000,
  easing: easings.easeOutBounce
}).start()

多属性缓动

class MultiTween {
  constructor(options) {
    this.target = options.target
    this.properties = options.properties
    this.duration = options.duration || 1000
    this.easing = options.easing || (t => t)
    this.onUpdate = options.onUpdate
    this.onComplete = options.onComplete
    
    this.startTime = null
    this.running = false
  }
  
  start() {
    this.startTime = performance.now()
    this.running = true
    this.tick()
    return this
  }
  
  tick() {
    if (!this.running) return
    
    const elapsed = performance.now() - this.startTime
    const progress = Math.min(elapsed / this.duration, 1)
    const easedProgress = this.easing(progress)
    
    Object.entries(this.properties).forEach(([prop, values]) => {
      this.target[prop] = values.from + (values.to - values.from) * easedProgress
    })
    
    if (this.onUpdate) {
      this.onUpdate(this.target, progress)
    }
    
    if (progress < 1) {
      requestAnimationFrame(() => this.tick())
    } else {
      this.running = false
      if (this.onComplete) {
        this.onComplete()
      }
    }
  }
}

new MultiTween({
  target: element.style,
  properties: {
    left: { from: 0, to: 300 },
    top: { from: 0, to: 200 },
    opacity: { from: 1, to: 0.5 }
  },
  duration: 800,
  easing: easings.easeOutCubic
}).start()

缓动动画演示

缓动动画对比

缓动函数选择

根据场景选择

场景推荐缓动原因
UI元素出现easeOut自然进入
UI元素消失easeIn自然退出
弹出框easeOutBack有弹性感
按钮点击easeOutQuad快速响应
页面滚动easeOutCubic平滑停止
加载动画easeInOut循环流畅

缓动方向选择

// easeIn:开始慢,结束快
// 适合:元素离开屏幕

// easeOut:开始快,结束慢
// 适合:元素进入屏幕

// easeInOut:开始慢,中间快,结束慢
// 适合:循环动画、页面滚动

缓动动画工具

const EasingLibrary = {
  linear: t => t,
  
  quad: {
    in: t => t * t,
    out: t => t * (2 - t),
    inOut: t => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t
  },
  
  cubic: {
    in: t => t * t * t,
    out: t => (--t) * t * t + 1,
    inOut: t => t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1
  },
  
  elastic: {
    out: t => {
      if (t === 0 || t === 1) return t
      return Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * (2 * Math.PI) / 3) + 1
    }
  },
  
  bounce: {
    out: t => {
      const n1 = 7.5625
      const d1 = 2.75
      if (t < 1 / d1) return n1 * t * t
      if (t < 2 / d1) return n1 * (t -= 1.5 / d1) * t + 0.75
      if (t < 2.5 / d1) return n1 * (t -= 2.25 / d1) * t + 0.9375
      return n1 * (t -= 2.625 / d1) * t + 0.984375
    }
  },
  
  get(name) {
    const parts = name.split(/(?=[A-Z])/)
    if (parts.length === 1) {
      return this[parts[0].toLowerCase()]
    }
    const [type, direction] = parts
    return this[type.toLowerCase()][direction.toLowerCase()]
  }
}

const easing = EasingLibrary.get('easeOutBounce')

小结

缓动动画的核心要点:

  1. 缓动函数:控制动画速度变化的数学公式
  2. 缓动类型:easeIn、easeOut、easeInOut三种方向
  3. 常用缓动:Quad、Cubic、Bounce、Elastic等
  4. 选择原则:根据场景和用户体验选择合适的缓动
  5. 实现方式:使用Tween类封装缓动动画逻辑

缓动动画是提升用户体验的重要技术,合理使用可以让界面更加生动自然。