弹性动画

学习Canvas弹性动画技术,掌握弹簧动画原理和弹性效果的实现方法。弹性动画模拟弹簧的物理特性,创造出有弹性的动画效果,常用于交互反馈和游戏UI。

什么是弹性动画

弹性动画基于弹簧的物理模型,物体在目标位置附近振动,最终稳定在目标位置。

弹簧物理模型

// 弹簧公式:F = -k * x
// F: 弹簧力
// k: 弹簧系数(劲度系数)
// x: 位移

class Spring {
  constructor(options = {}) {
    this.position = options.position || 0
    this.target = options.target || 0
    this.velocity = options.velocity || 0
    this.stiffness = options.stiffness || 0.1    // 刚度
    this.damping = options.damping || 0.8        // 阻尼
    this.mass = options.mass || 1                // 质量
  }
  
  update() {
    const force = (this.target - this.position) * this.stiffness
    const acceleration = force / this.mass
    this.velocity += acceleration
    this.velocity *= this.damping
    this.position += this.velocity
    return this.position
  }
}

弹簧参数详解

刚度(Stiffness)

刚度决定弹簧的"硬度",影响振动频率。

// 低刚度:缓慢、柔和
const softSpring = new Spring({ stiffness: 0.05 })

// 高刚度:快速、紧绷
const stiffSpring = new Spring({ stiffness: 0.3 })

阻尼(Damping)

阻尼决定能量消耗速度,影响振动持续时间。

// 低阻尼:振动时间长
const bouncySpring = new Spring({ damping: 0.9 })

// 高阻尼:快速停止
const quickSpring = new Spring({ damping: 0.5 })

质量(Mass)

质量影响惯性,较大的质量会延迟响应。

// 轻质量:快速响应
const lightSpring = new Spring({ mass: 0.5 })

// 重质量:缓慢响应
const heavySpring = new Spring({ mass: 2 })

弹性动画类

class SpringAnimation {
  constructor(options) {
    this.from = options.from
    this.to = options.to
    this.stiffness = options.stiffness || 100
    this.damping = options.damping || 10
    this.mass = options.mass || 1
    this.velocity = options.velocity || 0
    this.onUpdate = options.onUpdate
    this.onComplete = options.onComplete
    
    this.current = this.from
    this.running = false
    this.settled = false
  }
  
  start() {
    this.running = true
    this.settled = false
    this.tick()
    return this
  }
  
  tick() {
    if (!this.running || this.settled) return
    
    const spring = this.stiffness
    const damper = this.damping
    
    const force = spring * (this.to - this.current)
    const acceleration = (force - damper * this.velocity) / this.mass
    
    this.velocity += acceleration * 0.016
    this.current += this.velocity
    
    if (this.onUpdate) {
      this.onUpdate(this.current)
    }
    
    if (Math.abs(this.velocity) < 0.001 && Math.abs(this.to - this.current) < 0.001) {
      this.current = this.to
      this.settled = true
      this.running = false
      if (this.onComplete) {
        this.onComplete()
      }
      return
    }
    
    requestAnimationFrame(() => this.tick())
  }
  
  stop() {
    this.running = false
    return this
  }
  
  reset() {
    this.current = this.from
    this.velocity = 0
    this.settled = false
    return this
  }
}

弹性动画演示

弹性动画效果

弹簧预设

const SpringPresets = {
  gentle: {
    stiffness: 120,
    damping: 14
  },
  wobbly: {
    stiffness: 180,
    damping: 12
  },
  stiff: {
    stiffness: 300,
    damping: 20
  },
  slow: {
    stiffness: 100,
    damping: 22
  },
  bouncy: {
    stiffness: 200,
    damping: 10
  }
}

function createSpring(preset, options = {}) {
  const config = { ...SpringPresets[preset], ...options }
  return new SpringAnimation(config)
}

const animation = createSpring('bouncy', {
  from: 0,
  to: 100,
  onUpdate: (value) => console.log(value)
})

多维弹性动画

class SpringVector {
  constructor(options) {
    this.x = options.x || 0
    this.y = options.y || 0
    this.targetX = options.targetX || this.x
    this.targetY = options.targetY || this.y
    this.vx = 0
    this.vy = 0
    this.stiffness = options.stiffness || 0.1
    this.damping = options.damping || 0.8
  }
  
  setTarget(x, y) {
    this.targetX = x
    this.targetY = y
  }
  
  update() {
    const ax = (this.targetX - this.x) * this.stiffness
    const ay = (this.targetY - this.y) * this.stiffness
    
    this.vx = (this.vx + ax) * this.damping
    this.vy = (this.vy + ay) * this.damping
    
    this.x += this.vx
    this.y += this.vy
  }
  
  isSettled() {
    const dx = Math.abs(this.targetX - this.x)
    const dy = Math.abs(this.targetY - this.y)
    const speed = Math.sqrt(this.vx * this.vx + this.vy * this.vy)
    return dx < 0.1 && dy < 0.1 && speed < 0.1
  }
}

弹性跟随效果

class ElasticFollow {
  constructor(options) {
    this.target = options.target
    this.stiffness = options.stiffness || 0.1
    this.damping = options.damping || 0.7
    
    this.x = this.target.x
    this.y = this.target.y
    this.vx = 0
    this.vy = 0
  }
  
  update() {
    const ax = (this.target.x - this.x) * this.stiffness
    const ay = (this.target.y - this.y) * this.stiffness
    
    this.vx = (this.vx + ax) * this.damping
    this.vy = (this.vy + ay) * this.damping
    
    this.x += this.vx
    this.y += this.vy
  }
}

const mouse = { x: 200, y: 100 }
const follower = new ElasticFollow({
  target: mouse,
  stiffness: 0.08,
  damping: 0.75
})

canvas.addEventListener('mousemove', (e) => {
  mouse.x = e.offsetX
  mouse.y = e.offsetY
})

弹性跟随演示

弹性跟随效果(移动鼠标)

弹性动画应用

弹性按钮

class ElasticButton {
  constructor(element, options = {}) {
    this.element = element
    this.scale = 1
    this.targetScale = 1
    this.velocity = 0
    this.stiffness = options.stiffness || 0.3
    this.damping = options.damping || 0.6
    
    this.element.addEventListener('mousedown', () => this.press())
    this.element.addEventListener('mouseup', () => this.release())
    this.element.addEventListener('mouseleave', () => this.release())
    
    this.animate()
  }
  
  press() {
    this.targetScale = 0.9
  }
  
  release() {
    this.targetScale = 1
  }
  
  animate() {
    const force = (this.targetScale - this.scale) * this.stiffness
    this.velocity = (this.velocity + force) * this.damping
    this.scale += this.velocity
    
    this.element.style.transform = `scale(${this.scale})`
    
    requestAnimationFrame(() => this.animate())
  }
}

弹性滚动

class ElasticScroll {
  constructor(container, options = {}) {
    this.container = container
    this.scrollTop = 0
    this.targetScrollTop = 0
    this.velocity = 0
    this.stiffness = options.stiffness || 0.1
    this.damping = options.damping || 0.7
    
    container.addEventListener('scroll', () => {
      this.targetScrollTop = container.scrollTop
    })
    
    this.animate()
  }
  
  animate() {
    const force = (this.targetScrollTop - this.scrollTop) * this.stiffness
    this.velocity = (this.velocity + force) * this.damping
    this.scrollTop += this.velocity
    
    // 使用 scrollTop 做其他事情
    
    requestAnimationFrame(() => this.animate())
  }
}

弹性动画 vs 缓动动画

特性弹性动画缓动动画
物理模型弹簧物理数学曲线
超调可以超过目标不会超过目标
可中断可以随时改变目标需要重新开始
参数刚度、阻尼、质量缓动函数、时长
适用场景交互反馈、拖拽UI过渡、动画

性能优化

减少计算

class OptimizedSpring {
  constructor() {
    this.position = 0
    this.target = 0
    this.velocity = 0
    this.stiffness = 0.1
    this.damping = 0.8
    this.settled = true
  }
  
  update() {
    if (this.settled) return this.position
    
    const force = (this.target - this.position) * this.stiffness
    this.velocity = (this.velocity + force) * this.damping
    this.position += this.velocity
    
    if (Math.abs(this.velocity) < 0.001 && Math.abs(this.target - this.position) < 0.001) {
      this.position = this.target
      this.settled = true
    }
    
    return this.position
  }
  
  setTarget(target) {
    this.target = target
    this.settled = false
  }
}