掌握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)
}
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
}
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
}
}
}
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()