帧率控制

掌握Canvas帧率控制技术,学习帧率限制、FPS计算和自适应帧率方法。帧率控制是确保动画流畅性和性能平衡的关键技术,本章将介绍各种帧率控制方法。

什么是帧率控制

帧率控制是指控制动画每秒渲染的帧数,以实现流畅的视觉效果和合理的资源消耗。

为什么需要帧率控制

原因说明
性能平衡降低帧率减少CPU/GPU负载
设备差异适应不同性能的设备
电池续航移动设备上节省电量
动画一致性确保动画速度稳定

目标帧率

场景推荐帧率说明
游戏60 FPS流畅的游戏体验
普通动画30 FPS平衡性能和效果
后台动画15 FPS节省资源
简单过渡24 FPS电影级流畅度

帧率限制

基本帧率限制

const targetFPS = 30
const frameInterval = 1000 / targetFPS
let lastTime = 0

function animate(currentTime) {
  const elapsed = currentTime - lastTime
  
  if (elapsed >= frameInterval) {
    lastTime = currentTime - (elapsed % frameInterval)
    
    update()
    draw()
  }
  
  requestAnimationFrame(animate)
}

requestAnimationFrame(animate)

帧率限制器类

class FrameRateLimiter {
  constructor(targetFPS = 60) {
    this.targetFPS = targetFPS
    this.frameInterval = 1000 / targetFPS
    this.lastTime = 0
  }
  
  shouldRender(currentTime) {
    const elapsed = currentTime - this.lastTime
    
    if (elapsed >= this.frameInterval) {
      this.lastTime = currentTime - (elapsed % this.frameInterval)
      return true
    }
    
    return false
  }
  
  setFPS(fps) {
    this.targetFPS = fps
    this.frameInterval = 1000 / fps
  }
}

const limiter = new FrameRateLimiter(30)

function animate(currentTime) {
  if (limiter.shouldRender(currentTime)) {
    update()
    draw()
  }
  requestAnimationFrame(animate)
}

帧率限制演示

帧率限制对比

FPS计算

基本FPS计算

let frameCount = 0
let lastFpsTime = 0
let currentFPS = 0

function calculateFPS(currentTime) {
  frameCount++
  
  if (currentTime - lastFpsTime >= 1000) {
    currentFPS = frameCount
    frameCount = 0
    lastFpsTime = currentTime
  }
  
  return currentFPS
}

FPS监视器类

class FPSMonitor {
  constructor() {
    this.frames = []
    this.lastTime = 0
    this.fps = 0
    this.averageFPS = 0
    this.minFPS = Infinity
    this.maxFPS = 0
  }
  
  update(currentTime) {
    if (this.lastTime) {
      const delta = currentTime - this.lastTime
      this.frames.push(delta)
      
      if (this.frames.length > 60) {
        this.frames.shift()
      }
      
      this.calculateStats()
    }
    
    this.lastTime = currentTime
  }
  
  calculateStats() {
    if (this.frames.length === 0) return
    
    const total = this.frames.reduce((a, b) => a + b, 0)
    this.averageFPS = Math.round(1000 / (total / this.frames.length))
    this.fps = Math.round(1000 / this.frames[this.frames.length - 1])
    
    const fpsValues = this.frames.map(f => 1000 / f)
    this.minFPS = Math.round(Math.min(...fpsValues))
    this.maxFPS = Math.round(Math.max(...fpsValues))
  }
  
  getStats() {
    return {
      current: this.fps,
      average: this.averageFPS,
      min: this.minFPS,
      max: this.maxFPS
    }
  }
}

FPS显示演示

FPS监视器

自适应帧率

基于性能调整

class AdaptiveFrameRate {
  constructor(minFPS = 15, maxFPS = 60) {
    this.minFPS = minFPS
    this.maxFPS = maxFPS
    this.currentFPS = maxFPS
    this.frameTimes = []
    this.adjustmentInterval = 2000
    this.lastAdjustment = 0
  }
  
  update(currentTime) {
    const frameTime = currentTime - this.lastAdjustment
    this.frameTimes.push(frameTime)
    
    if (this.frameTimes.length > 60) {
      this.frameTimes.shift()
    }
    
    if (currentTime - this.lastAdjustment > this.adjustmentInterval) {
      this.adjustFPS()
      this.lastAdjustment = currentTime
    }
  }
  
  adjustFPS() {
    if (this.frameTimes.length < 10) return
    
    const avgFrameTime = this.frameTimes.reduce((a, b) => a + b, 0) / this.frameTimes.length
    const actualFPS = 1000 / avgFrameTime
    
    if (actualFPS < this.currentFPS * 0.8) {
      this.currentFPS = Math.max(this.minFPS, this.currentFPS - 5)
    } else if (actualFPS > this.currentFPS * 1.1) {
      this.currentFPS = Math.min(this.maxFPS, this.currentFPS + 5)
    }
  }
  
  getFrameInterval() {
    return 1000 / this.currentFPS
  }
}

动态质量调整

class DynamicQuality {
  constructor() {
    this.quality = 1.0
    this.targetFrameTime = 16.67
    this.frameTimes = []
  }
  
  update(frameTime) {
    this.frameTimes.push(frameTime)
    
    if (this.frameTimes.length > 30) {
      this.frameTimes.shift()
    }
    
    this.adjustQuality()
  }
  
  adjustQuality() {
    const avgFrameTime = this.frameTimes.reduce((a, b) => a + b, 0) / this.frameTimes.length
    
    if (avgFrameTime > this.targetFrameTime * 1.5) {
      this.quality = Math.max(0.25, this.quality - 0.1)
    } else if (avgFrameTime < this.targetFrameTime * 0.8) {
      this.quality = Math.min(1.0, this.quality + 0.05)
    }
  }
  
  getRenderScale() {
    return this.quality
  }
  
  shouldSkipFrame() {
    return this.quality < 0.5 && Math.random() > this.quality * 2
  }
}

自适应帧率演示

自适应帧率(模拟负载变化)

垂直同步

什么是垂直同步

垂直同步(V-Sync)是将帧率与显示器刷新率同步的技术,可以防止画面撕裂。

模拟垂直同步

const DISPLAY_REFRESH_RATE = 60
const VSYNC_INTERVAL = 1000 / DISPLAY_REFRESH_RATE

let lastVSyncTime = 0

function vSyncAnimate(currentTime) {
  const timeSinceLastVSync = currentTime - lastVSyncTime
  
  if (timeSinceLastVSync >= VSYNC_INTERVAL) {
    lastVSyncTime = currentTime
    
    update()
    draw()
  }
  
  requestAnimationFrame(vSyncAnimate)
}

帧率与动画速度

帧率无关动画

确保动画速度不受帧率影响:

let lastTime = 0
const speed = 200  // 像素/秒

function animate(currentTime) {
  const deltaTime = (currentTime - lastTime) / 1000
  lastTime = currentTime
  
  // 限制最大deltaTime避免跳帧
  const clampedDelta = Math.min(deltaTime, 0.1)
  
  // 基于时间更新位置
  x += speed * clampedDelta
  
  draw()
  requestAnimationFrame(animate)
}

插值渲染

在低帧率时使用插值保持流畅:

let previousState = { x: 0 }
let currentState = { x: 0 }
let accumulator = 0
const FIXED_TIMESTEP = 1000 / 60

function animate(currentTime) {
  accumulator += currentTime - lastTime
  lastTime = currentTime
  
  while (accumulator >= FIXED_TIMESTEP) {
    previousState = { ...currentState }
    updateState(currentState, FIXED_TIMESTEP)
    accumulator -= FIXED_TIMESTEP
  }
  
  // 插值渲染
  const alpha = accumulator / FIXED_TIMESTEP
  const interpolatedState = {
    x: previousState.x + (currentState.x - previousState.x) * alpha
  }
  
  render(interpolatedState)
  requestAnimationFrame(animate)
}

双缓冲

什么是双缓冲

双缓冲使用两个缓冲区交替渲染,避免画面撕裂。

class DoubleBuffer {
  constructor(width, height) {
    this.frontBuffer = document.createElement('canvas')
    this.backBuffer = document.createElement('canvas')
    
    this.frontBuffer.width = width
    this.frontBuffer.height = height
    this.backBuffer.width = width
    this.backBuffer.height = height
    
    this.frontCtx = this.frontBuffer.getContext('2d')
    this.backCtx = this.backBuffer.getContext('2d')
  }
  
  getBackContext() {
    return this.backCtx
  }
  
  swap(mainCtx, x = 0, y = 0) {
    mainCtx.drawImage(this.backBuffer, x, y)
    
    const temp = this.frontBuffer
    this.frontBuffer = this.backBuffer
    this.backBuffer = temp
    
    const tempCtx = this.frontCtx
    this.frontCtx = this.backCtx
    this.backCtx = tempCtx
  }
  
  clear() {
    this.backCtx.clearRect(0, 0, this.backBuffer.width, this.backBuffer.height)
  }
}

实际应用

游戏帧率控制

class GameLoop {
  constructor(options = {}) {
    this.targetFPS = options.targetFPS || 60
    this.frameInterval = 1000 / this.targetFPS
    this.lastTime = 0
    this.accumulator = 0
    this.isRunning = false
    
    this.onUpdate = options.update || (() => {})
    this.onRender = options.render || (() => {})
  }
  
  start() {
    this.isRunning = true
    this.lastTime = performance.now()
    this.loop(this.lastTime)
  }
  
  stop() {
    this.isRunning = false
  }
  
  loop(currentTime) {
    if (!this.isRunning) return
    
    const frameTime = currentTime - this.lastTime
    this.lastTime = currentTime
    this.accumulator += frameTime
    
    while (this.accumulator >= this.frameInterval) {
      this.onUpdate(this.frameInterval)
      this.accumulator -= this.frameInterval
    }
    
    this.onRender(this.accumulator / this.frameInterval)
    
    requestAnimationFrame(t => this.loop(t))
  }
  
  setFPS(fps) {
    this.targetFPS = fps
    this.frameInterval = 1000 / fps
  }
}

const game = new GameLoop({
  targetFPS: 60,
  update: (dt) => {
    // 更新逻辑
  },
  render: (alpha) => {
    // 渲染逻辑
  }
})

game.start()