动画时间线

学习Canvas动画时间线技术,掌握复杂动画序列的管理和编排方法。动画时间线用于管理复杂的动画序列,协调多个动画的播放顺序和时序。

什么是动画时间线

动画时间线是一个管理多个动画的工具,可以控制动画的播放顺序、同步和组合。

时间线的作用

功能说明
序列控制按顺序播放动画
并行控制同时播放多个动画
时间控制暂停、继续、跳转
事件触发在特定时间触发回调

基础时间线类

class Timeline {
  constructor() {
    this.tracks = []
    this.currentTime = 0
    this.duration = 0
    this.playing = false
    this.speed = 1
  }
  
  add(track) {
    this.tracks.push(track)
    this.duration = Math.max(this.duration, track.endTime)
    return this
  }
  
  play() {
    this.playing = true
    this.lastTime = performance.now()
    this.tick()
    return this
  }
  
  pause() {
    this.playing = false
    return this
  }
  
  seek(time) {
    this.currentTime = Math.max(0, Math.min(time, this.duration))
    this.updateTracks()
    return this
  }
  
  tick() {
    if (!this.playing) return
    
    const now = performance.now()
    const delta = (now - this.lastTime) * this.speed
    this.lastTime = now
    
    this.currentTime += delta
    
    if (this.currentTime >= this.duration) {
      this.currentTime = 0
    }
    
    this.updateTracks()
    
    requestAnimationFrame(() => this.tick())
  }
  
  updateTracks() {
    this.tracks.forEach(track => {
      track.update(this.currentTime)
    })
  }
}

动画轨道

class AnimationTrack {
  constructor(options = {}) {
    this.target = options.target
    this.property = options.property
    this.keyframes = []
    this.startTime = options.startTime || 0
    this.endTime = 0
  }
  
  addKeyframe(time, value, easing = 'linear') {
    this.keyframes.push({ time, value, easing })
    this.keyframes.sort((a, b) => a.time - b.time)
    this.endTime = Math.max(this.endTime, time + this.startTime)
    return this
  }
  
  update(currentTime) {
    const localTime = currentTime - this.startTime
    
    if (localTime < 0 || localTime > this.endTime - this.startTime) return
    
    let prev = this.keyframes[0]
    let next = this.keyframes[0]
    
    for (let i = 0; i < this.keyframes.length - 1; i++) {
      if (localTime >= this.keyframes[i].time && localTime <= this.keyframes[i + 1].time) {
        prev = this.keyframes[i]
        next = this.keyframes[i + 1]
        break
      }
    }
    
    if (prev === next) {
      this.target[this.property] = prev.value
      return
    }
    
    const progress = (localTime - prev.time) / (next.time - prev.time)
    const easedProgress = this.applyEasing(progress, prev.easing)
    
    this.target[this.property] = this.lerp(prev.value, next.value, easedProgress)
  }
  
  lerp(a, b, t) {
    return a + (b - a) * t
  }
  
  applyEasing(t, easing) {
    switch (easing) {
      case 'easeIn': return t * t
      case 'easeOut': return t * (2 - t)
      case 'easeInOut': return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t
      default: return t
    }
  }
}

时间线演示

动画时间线

关键帧动画

class KeyframeAnimation {
  constructor(options = {}) {
    this.target = options.target
    this.keyframes = options.keyframes || []
    this.duration = options.duration || 1000
    this.loop = options.loop || false
    this.easing = options.easing || (t => t)
    this.onUpdate = options.onUpdate
    this.onComplete = options.onComplete
    
    this.currentTime = 0
    this.playing = false
  }
  
  play() {
    this.playing = true
    this.startTime = performance.now()
    this.tick()
    return this
  }
  
  pause() {
    this.playing = false
    return this
  }
  
  tick() {
    if (!this.playing) return
    
    const elapsed = performance.now() - this.startTime
    this.currentTime = elapsed % this.duration
    
    const progress = this.currentTime / this.duration
    const easedProgress = this.easing(progress)
    
    this.applyKeyframes(easedProgress)
    
    if (this.onUpdate) {
      this.onUpdate(this.target, progress)
    }
    
    if (!this.loop && elapsed >= this.duration) {
      this.playing = false
      if (this.onComplete) {
        this.onComplete()
      }
      return
    }
    
    requestAnimationFrame(() => this.tick())
  }
  
  applyKeyframes(progress) {
    if (this.keyframes.length === 0) return
    
    let prev = this.keyframes[0]
    let next = this.keyframes[this.keyframes.length - 1]
    
    for (let i = 0; i < this.keyframes.length - 1; i++) {
      if (progress >= this.keyframes[i].time && progress <= this.keyframes[i + 1].time) {
        prev = this.keyframes[i]
        next = this.keyframes[i + 1]
        break
      }
    }
    
    const localProgress = (progress - prev.time) / (next.time - prev.time)
    
    Object.keys(prev.properties).forEach(prop => {
      const from = prev.properties[prop]
      const to = next.properties[prop]
      this.target[prop] = from + (to - from) * localProgress
    })
  }
}

const ball = { x: 50, y: 100, radius: 20 }
new KeyframeAnimation({
  target: ball,
  keyframes: [
    { time: 0, properties: { x: 50, radius: 20 } },
    { time: 0.5, properties: { x: 350, radius: 30 } },
    { time: 1, properties: { x: 50, radius: 20 } }
  ],
  duration: 2000,
  loop: true
}).play()

动画序列

class AnimationSequence {
  constructor() {
    this.animations = []
    this.currentIndex = 0
    this.playing = false
  }
  
  add(animation) {
    this.animations.push(animation)
    return this
  }
  
  play() {
    this.playing = true
    this.currentIndex = 0
    this.playNext()
    return this
  }
  
  playNext() {
    if (!this.playing || this.currentIndex >= this.animations.length) {
      this.playing = false
      return
    }
    
    const animation = this.animations[this.currentIndex]
    animation.onComplete = () => {
      this.currentIndex++
      this.playNext()
    }
    
    animation.play()
  }
  
  pause() {
    this.playing = false
    if (this.animations[this.currentIndex]) {
      this.animations[this.currentIndex].pause()
    }
    return this
  }
  
  stop() {
    this.playing = false
    this.currentIndex = 0
    return this
  }
}

并行动画

class ParallelAnimation {
  constructor() {
    this.animations = []
    this.playing = false
    this.completed = 0
  }
  
  add(animation) {
    this.animations.push(animation)
    return this
  }
  
  play() {
    this.playing = true
    this.completed = 0
    
    this.animations.forEach(animation => {
      const originalOnComplete = animation.onComplete
      animation.onComplete = () => {
        this.completed++
        if (originalOnComplete) originalOnComplete()
        if (this.completed === this.animations.length) {
          this.playing = false
        }
      }
      animation.play()
    })
    
    return this
  }
  
  pause() {
    this.playing = false
    this.animations.forEach(a => a.pause())
    return this
  }
}

动画组

class AnimationGroup {
  constructor(options = {}) {
    this.animations = []
    this.startTime = options.startTime || 0
    this.duration = options.duration || 0
    this.loop = options.loop || false
  }
  
  add(animation, delay = 0) {
    animation.delay = delay
    this.animations.push(animation)
    this.duration = Math.max(this.duration, delay + animation.duration)
    return this
  }
  
  update(currentTime) {
    const localTime = currentTime - this.startTime
    
    this.animations.forEach(animation => {
      const animTime = localTime - animation.delay
      
      if (animTime >= 0 && animTime < animation.duration) {
        animation.update(animTime)
      }
    })
  }
}

时间线控制器

class TimelineController {
  constructor() {
    this.timeline = new Timeline()
    this.ui = null
  }
  
  createUI(container) {
    const wrapper = document.createElement('div')
    wrapper.className = 'timeline-ui'
    
    const controls = document.createElement('div')
    controls.className = 'timeline-controls'
    
    const playBtn = document.createElement('button')
    playBtn.textContent = '播放'
    playBtn.onclick = () => this.togglePlay()
    
    const progress = document.createElement('input')
    progress.type = 'range'
    progress.min = 0
    progress.max = 100
    progress.value = 0
    progress.oninput = (e) => {
      const time = (e.target.value / 100) * this.timeline.duration
      this.timeline.seek(time)
    }
    
    controls.appendChild(playBtn)
    controls.appendChild(progress)
    wrapper.appendChild(controls)
    
    container.appendChild(wrapper)
    
    this.ui = { playBtn, progress }
    return wrapper
  }
  
  togglePlay() {
    if (this.timeline.playing) {
      this.timeline.pause()
      this.ui.playBtn.textContent = '播放'
    } else {
      this.timeline.play()
      this.ui.playBtn.textContent = '暂停'
    }
  }
  
  updateUI() {
    if (this.ui && this.timeline.duration > 0) {
      const progress = (this.timeline.currentTime / this.timeline.duration) * 100
      this.ui.progress.value = progress
    }
  }
}

缓动时间线

class EasingTimeline {
  constructor() {
    this.segments = []
    this.currentTime = 0
    this.duration = 0
  }
  
  addSegment(duration, easing, update) {
    const startTime = this.duration
    this.segments.push({
      startTime,
      duration,
      endTime: startTime + duration,
      easing,
      update
    })
    this.duration += duration
    return this
  }
  
  update(dt) {
    this.currentTime += dt
    
    if (this.currentTime >= this.duration) {
      this.currentTime = this.currentTime % this.duration
    }
    
    this.segments.forEach(segment => {
      if (this.currentTime >= segment.startTime && this.currentTime < segment.endTime) {
        const localTime = this.currentTime - segment.startTime
        const progress = localTime / segment.duration
        const easedProgress = segment.easing(progress)
        segment.update(easedProgress, localTime)
      }
    })
  }
}

const timeline = new EasingTimeline()
  .addSegment(1000, t => t * t, (p) => {
    console.log('第一阶段:', p)
  })
  .addSegment(500, t => t * (2 - t), (p) => {
    console.log('第二阶段:', p)
  })
  .addSegment(800, t => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t, (p) => {
    console.log('第三阶段:', p)
  })

事件系统

class EventTimeline extends Timeline {
  constructor() {
    super()
    this.events = []
    this.triggeredEvents = new Set()
  }
  
  addEvent(time, callback) {
    this.events.push({ time, callback, id: Math.random() })
    this.events.sort((a, b) => a.time - b.time)
    return this
  }
  
  updateTracks() {
    super.updateTracks()
    this.checkEvents()
  }
  
  checkEvents() {
    this.events.forEach(event => {
      if (this.currentTime >= event.time && !this.triggeredEvents.has(event.id)) {
        event.callback()
        this.triggeredEvents.add(event.id)
      }
    })
  }
  
  seek(time) {
    super.seek(time)
    this.triggeredEvents.clear()
  }
}

动画状态机

class AnimationStateMachine {
  constructor() {
    this.states = new Map()
    this.currentState = null
    this.transitions = []
  }
  
  addState(name, animation) {
    this.states.set(name, animation)
    return this
  }
  
  addTransition(from, to, condition) {
    this.transitions.push({ from, to, condition })
    return this
  }
  
  setState(name) {
    if (this.currentState) {
      const currentAnim = this.states.get(this.currentState)
      if (currentAnim && currentAnim.pause) {
        currentAnim.pause()
      }
    }
    
    this.currentState = name
    const animation = this.states.get(name)
    
    if (animation && animation.play) {
      animation.play()
    }
    
    return this
  }
  
  update() {
    if (!this.currentState) return
    
    this.transitions.forEach(transition => {
      if (transition.from === this.currentState && transition.condition()) {
        this.setState(transition.to)
      }
    })
  }
}

const fsm = new AnimationStateMachine()
  .addState('idle', idleAnimation)
  .addState('walk', walkAnimation)
  .addState('run', runAnimation)
  .addTransition('idle', 'walk', () => isMoving)
  .addTransition('walk', 'run', () => isRunning)
  .addTransition('run', 'walk', () => !isRunning)
  .addTransition('walk', 'idle', () => !isMoving)

fsm.setState('idle')