深入学习requestAnimationFrame API,掌握现代Canvas动画的核心技术。requestAnimationFrame是浏览器专门为动画设计的API,是创建流畅Canvas动画的首选方法。
requestAnimationFrame(简称rAF)是浏览器提供的动画专用API,它会在浏览器下一次重绘之前调用指定的回调函数。
const requestId = requestAnimationFrame(callback)
// callback: 回调函数,接收一个高精度时间戳参数
// 返回值: 请求ID,用于取消动画
function animate(timestamp) {
// timestamp: 高精度时间戳(毫秒)
update(timestamp)
draw()
requestAnimationFrame(animate)
}
requestAnimationFrame(animate)
| 特性 | requestAnimationFrame | setInterval |
|---|---|---|
| 帧率 | 自动匹配显示器刷新率 | 固定间隔 |
| 页面隐藏 | 自动暂停 | 继续运行 |
| 时间精度 | 高精度时间戳 | 依赖系统时间 |
| 性能 | 浏览器优化 | 可能浪费资源 |
| 回调时机 | 重绘前 | 固定间隔 |
const canvas = document.getElementById('canvas')
const ctx = canvas.getContext('2d')
let x = 0
function animate() {
ctx.clearRect(0, 0, canvas.width, canvas.height)
ctx.fillStyle = '#3498db'
ctx.beginPath()
ctx.arc(x, 100, 30, 0, Math.PI * 2)
ctx.fill()
x += 2
if (x > canvas.width + 30) x = -30
requestAnimationFrame(animate)
}
requestAnimationFrame(animate)
回调函数接收一个DOMHighResTimeStamp参数,表示从页面加载开始经过的毫秒数。
function animate(timestamp) {
console.log(`当前时间: ${timestamp.toFixed(2)}ms`)
requestAnimationFrame(animate)
}
requestAnimationFrame(animate)
let lastTime = 0
function animate(currentTime) {
const deltaTime = currentTime - lastTime
lastTime = currentTime
console.log(`帧间隔: ${deltaTime.toFixed(2)}ms`)
requestAnimationFrame(animate)
}
requestAnimationFrame(animate)
let lastTime = 0
let x = 0
const speed = 200 // 像素/秒
function animate(currentTime) {
if (lastTime === 0) lastTime = currentTime
const deltaTime = (currentTime - lastTime) / 1000 // 转换为秒
lastTime = currentTime
// 基于时间更新位置
x += speed * deltaTime
if (x > canvas.width) x = 0
draw()
requestAnimationFrame(animate)
}
使用cancelAnimationFrame停止动画:
let animationId = null
function start() {
if (!animationId) {
animationId = requestAnimationFrame(animate)
}
}
function stop() {
if (animationId) {
cancelAnimationFrame(animationId)
animationId = null
}
}
function animate(timestamp) {
update()
draw()
animationId = requestAnimationFrame(animate)
}
const animation = {
animationId: null,
isPaused: false,
lastTime: 0,
x: 0,
start() {
this.isPaused = false
this.lastTime = 0
this.animate(performance.now())
},
pause() {
this.isPaused = true
if (this.animationId) {
cancelAnimationFrame(this.animationId)
this.animationId = null
}
},
resume() {
if (this.isPaused) {
this.isPaused = false
this.lastTime = 0
this.animationId = requestAnimationFrame(ts => this.animate(ts))
}
},
animate(currentTime) {
if (this.isPaused) return
if (this.lastTime === 0) this.lastTime = currentTime
const deltaTime = currentTime - this.lastTime
this.lastTime = currentTime
this.update(deltaTime)
this.draw()
this.animationId = requestAnimationFrame(ts => this.animate(ts))
}
}
class AnimationManager {
constructor() {
this.animations = new Map()
this.animationId = null
}
add(id, updateFn) {
this.animations.set(id, updateFn)
if (!this.animationId) {
this.start()
}
}
remove(id) {
this.animations.delete(id)
if (this.animations.size === 0) {
this.stop()
}
}
start() {
const loop = (timestamp) => {
this.animations.forEach(updateFn => updateFn(timestamp))
this.animationId = requestAnimationFrame(loop)
}
this.animationId = requestAnimationFrame(loop)
}
stop() {
if (this.animationId) {
cancelAnimationFrame(this.animationId)
this.animationId = null
}
}
}
const manager = new AnimationManager()
manager.add('ball1', (timestamp) => {
// 更新球1
})
manager.add('ball2', (timestamp) => {
// 更新球2
})
requestAnimationFrame在页面不可见时会自动暂停,节省资源。
// 监听页面可见性变化
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
console.log('页面隐藏,动画自动暂停')
} else {
console.log('页面显示,动画自动恢复')
}
})
let animationId = null
function animate(timestamp) {
update(timestamp)
draw()
animationId = requestAnimationFrame(animate)
}
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
if (animationId) {
cancelAnimationFrame(animationId)
animationId = null
}
} else {
if (!animationId) {
animationId = requestAnimationFrame(animate)
}
}
})
// 只清除需要更新的区域
function animate() {
const oldX = ball.x
const oldY = ball.y
update()
// 只清除旧位置
ctx.clearRect(oldX - radius, oldY - radius, radius * 2, radius * 2)
// 清除新位置(如果位置变化)
ctx.clearRect(ball.x - radius, ball.y - radius, radius * 2, radius * 2)
draw()
requestAnimationFrame(animate)
}
const offscreenCanvas = document.createElement('canvas')
offscreenCanvas.width = 200
offscreenCanvas.height = 200
const offCtx = offscreenCanvas.getContext('2d')
// 预渲染静态内容
function prerender() {
offCtx.fillStyle = '#3498db'
offCtx.fillRect(0, 0, 200, 200)
// ... 复杂绘制
}
function animate() {
ctx.clearRect(0, 0, canvas.width, canvas.height)
// 直接绘制缓存内容
ctx.drawImage(offscreenCanvas, 0, 0)
// 只更新动态内容
drawDynamicContent()
requestAnimationFrame(animate)
}
let lastTime = 0
const targetFPS = 30
const frameInterval = 1000 / targetFPS
function animate(currentTime) {
const elapsed = currentTime - lastTime
if (elapsed >= frameInterval) {
lastTime = currentTime - (elapsed % frameInterval)
update()
draw()
}
requestAnimationFrame(animate)
}
requestAnimationFrame(animate)
(function() {
let lastTime = 0
const vendors = ['webkit', 'moz', 'ms', 'o']
for (let i = 0; i < vendors.length && !window.requestAnimationFrame; i++) {
window.requestAnimationFrame = window[vendors[i] + 'RequestAnimationFrame']
window.cancelAnimationFrame = window[vendors[i] + 'CancelAnimationFrame'] ||
window[vendors[i] + 'CancelRequestAnimationFrame']
}
if (!window.requestAnimationFrame) {
window.requestAnimationFrame = function(callback) {
const currentTime = Date.now()
const timeToCall = Math.max(0, 16 - (currentTime - lastTime))
const id = setTimeout(() => callback(currentTime + timeToCall), timeToCall)
lastTime = currentTime + timeToCall
return id
}
}
if (!window.cancelAnimationFrame) {
window.cancelAnimationFrame = function(id) {
clearTimeout(id)
}
}
})()
class AnimationTimer {
constructor(duration, onUpdate, onComplete) {
this.duration = duration
this.onUpdate = onUpdate
this.onComplete = onComplete
this.startTime = null
this.animationId = null
}
start() {
this.startTime = performance.now()
this.tick()
}
stop() {
if (this.animationId) {
cancelAnimationFrame(this.animationId)
this.animationId = null
}
}
tick() {
const elapsed = performance.now() - this.startTime
const progress = Math.min(elapsed / this.duration, 1)
this.onUpdate(progress)
if (progress < 1) {
this.animationId = requestAnimationFrame(() => this.tick())
} else {
this.onComplete && this.onComplete()
}
}
}
const timer = new AnimationTimer(
1000,
progress => console.log(`进度: ${(progress * 100).toFixed(0)}%`),
() => console.log('完成')
)
timer.start()
function smoothScroll(targetY, duration = 500) {
const startY = window.scrollY
const distance = targetY - startY
const startTime = performance.now()
function scroll(currentTime) {
const elapsed = currentTime - startTime
const progress = Math.min(elapsed / duration, 1)
// 使用缓动函数
const eased = progress * (2 - progress)
window.scrollTo(0, startY + distance * eased)
if (progress < 1) {
requestAnimationFrame(scroll)
}
}
requestAnimationFrame(scroll)
}
smoothScroll(1000)