前言
在Web开发中,动画效果已经成为提升用户体验的重要组成部分。然而,实现流畅的动画并不是一件简单的事情。今天我们来聊聊JavaScript中一个专门为动画而生的API------requestAnimationFrame,它将帮助你创建更加流畅、高效的动画效果。
什么是requestAnimationFrame?
简单来说,requestAnimationFrame是浏览器提供的一个专门用于动画的API,它告诉浏览器你希望执行一个动画,并请求浏览器在下次重绘之前调用指定的函数来更新动画。
通俗易懂的比喻
想象一下,你和浏览器之间有这样的对话:
- 你(开发者):"浏览器大哥,我有个动画想让你帮忙执行。"
- 浏览器:"好的,不过我现在有点忙,等我准备好绘制下一帧画面的时候再叫你,这样可以吗?"
- 你:"没问题!"
这就是requestAnimationFrame的工作方式------它不会在固定的时间间隔执行,而是在浏览器准备绘制下一帧时执行,这样就能保证动画与浏览器的绘制节奏同步。
为什么需要requestAnimationFrame?
在requestAnimationFrame出现之前,我们通常使用setTimeout或setInterval来实现动画:
javascript
// 传统的setInterval方式
setInterval(function() {
// 更新动画
}, 16); // 大约60fps
这种方式存在几个问题:
- 时机不准确:浏览器可能因为各种原因延迟执行
- 资源浪费:即使页面被隐藏或最小化,动画仍在后台运行
- 性能问题:可能导致过度绘制或丢帧
使用方法
基本语法
javascript
let animationId;
function animate() {
// 动画逻辑代码
animationId = requestAnimationFrame(animate);
}
// 启动动画
animationId = requestAnimationFrame(animate);
// 停止动画
cancelAnimationFrame(animationId);
简单示例:往复移动一个小方块
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>往复移动一个小方块</title>
</head>
<style>
#box {
width: 50px;
height: 50px;
background-color: red;
position: absolute;
left: 0;
top: 100px;
}
</style>
<body>
<div id="box"></div>
<button onclick="startAnimation()">开始动画</button>
<button onclick="stopAnimation()">停止动画</button>
</body>
<script>
const box = document.getElementById('box');
let animationId;
let position = 0;
let driect = 'right';
function animate() {
if (driect === 'right') {
position += 2;
if (position >= 400) {
driect = 'left';
}
} else {
position -= 2;
if (position <= 0) {
driect = 'right';
}
}
box.style.left = position + 'px';
animationId = requestAnimationFrame(animate);
}
// 开始动画
function startAnimation() {
if (!animationId) {
animationId = requestAnimationFrame(animate);
}
}
// 停止动画
function stopAnimation() {
if (animationId) {
cancelAnimationFrame(animationId);
animationId = null;
}
}
</script>
</html>
小球弹跳下落动画
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>复杂动画示例-小球弹跳下落</title>
</head>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
.demo-container {
background-color: white;
border-radius: 10px;
padding: 30px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
margin-bottom: 30px;
}
.ball {
width: 50px;
height: 50px;
background: #e74c3c;
border-radius: 50%;
position: relative;
margin: 20px auto;
}
.controls {
display: flex;
gap: 10px;
margin: 20px 0;
justify-content: center;
}
button {
padding: 10px 20px;
background: #3498db;
border: none;
color: white;
border-radius: 5px;
cursor: pointer;
}
</style>
<body>
<div class="demo-container">
<h2>自由落体 + 弹跳</h2>
<div class="ball" id="ball"></div>
<div class="controls">
<button id="startBtn">开始动画</button>
<button id="resetBtn">重置</button>
</div>
</div>
</body>
<script>
// 创建动画类
class BallAnimationClass {
constructor() {
this.position = 0; // 当前位置
this.velocity = 0; // 当前速度
this.gravity = 0.5; // 重力加速度
this.damping = 0.8; // 能力损失系数
this.isRuning = false;
this.animationId = null;
this.ball = document.getElementById('ball');
}
start() {
if (this.isRuning) return;
this.isRuning = true;
this.animate();
}
reset() {
this.isRuning = false;
if (this.animationId) {
cancelAnimationFrame(this.animationId);
}
this.position = 0;
this.velocity = 0;
this.ball.style.transform = `translateY(${this.position}px)`;
}
animate() {
if (!this.isRuning) return;
// 应用重力
this.velocity += this.gravity;
// 改变位置
this.position += this.velocity;
// 碰撞检测(地面在300px位置)
if (this.position > 300) {
this.position = 300;
// 按0.8的系数减少下落高度
this.velocity = -this.velocity * this.damping;
if (Math.abs(this.velocity) < 0.5) {
this.velocity = 0;
this.isRuning = false;
}
}
this.ball.style.transform = `translateY(${this.position}px)`;
this.animationId = requestAnimationFrame(() => this.animate());
}
}
const ballAnimation = new BallAnimationClass();
document.getElementById('startBtn').addEventListener('click', () => {
ballAnimation.start();
})
document.getElementById('resetBtn').addEventListener('click', () => {
ballAnimation.reset();
})
</script>
</html>
数字增加动画
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>数字计数动画</title>
</head>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
max-width: 600px;
margin: 50px auto;
padding: 20px;
text-align: center;
background: #f5f7fa;
}
h1 {
color: #2c3e50;
margin-bottom: 30px;
}
.counter {
font-size: 4rem;
font-weight: 600;
color: #3498db;
margin: 30px 0;
transition: color 0.3s;
}
.controls {
margin: 20px 0;
}
button {
padding: 10px 20px;
margin: 0 10px;
font-size: 1rem;
background: #3498db;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
transition: background 0.3s;
}
button:hover {
background: #2980b9;
}
button:disabled {
background: #bdc3c7;
cursor: not-allowed;
}
.status {
margin-top: 20px;
color: #7f8c8d;
}
</style>
<body>
<h1>数字计数动画示例</h1>
<div class="counter" id="counter">0</div>
<div class="controls">
<button id="startBtn">开始计数</button>
<button id="pauseBtn" disabled>暂停</button>
<button id="resetBtn">重置</button>
</div>
<div class="status" id="status">状态:等待开始</div>
</body>
<script>
// 创建动画类
class NumberCounter {
constructor(element, targetValue = 1000, duration = 2000) {
this.element = element;
this.targetValue = targetValue;
this.duration = duration;
this.currentValue = 0;
this.startTime = null;
this.animationId = null;
this.isRuning = false;
}
start() {
if (this.isRuning) return;
this.isRuning = true;
this.startTime = performance.now();
this.animate();
document.getElementById('status').textContent = "状态:计数中...";
document.getElementById('startBtn').disabled = true;
document.getElementById('pauseBtn').disabled = false;
}
pause() {
this.isRuning = false;
if (this.animationId) {
cancelAnimationFrame(this.animationId);
}
document.getElementById('status').textContent = "状态:已暂停";
document.getElementById('startBtn').disabled = false;
document.getElementById('pauseBtn').disabled = true;
}
reset() {
this.pause();
this.currentValue = 0;
this.updateDisplay();
document.getElementById('status').textContent = "状态:已重置";
}
animate() {
if (!this.isRuning) return;
const currentTime = performance.now();
// 计算从点击【开始计数】过去了多久
const elapsed = currentTime - this.startTime;
// 计算在duration中占据的比例
const progress = Math.min(elapsed / this.duration, 1);
// 缓动函数(y = 1- (1-x)^3):开始较慢,中间加速,结束时又变慢
const easedProgress = 1 - Math.pow((1 - progress), 3);
// 计算当前值
this.currentValue = Math.round(this.targetValue * easedProgress);
this.updateDisplay();
if (progress < 1) {
this.animationId = requestAnimationFrame(() => this.animate());
} else {
this.isRuning = false;
document.getElementById('status').textContent = "状态:完成!";
document.getElementById('startBtn').disabled = false;
document.getElementById('pauseBtn').disabled = true;
}
}
updateDisplay() {
this.element.textContent = this.currentValue.toLocaleString();
// 添加简单的颜色变化
const percent = this.currentValue / this.targetValue;
const hue = 210 - percent * 60;
this.element.style.color = `hsl(${hue}, 70%, 50%)`;
}
}
const counter = new NumberCounter(
document.getElementById('counter'), 1234567, 3000
);
// 开始动画
document.getElementById('startBtn').addEventListener('click', () => {
counter.start();
})
// 动画暂停
document.getElementById('pauseBtn').addEventListener('click', () => {
counter.pause();
})
// 动画重置
document.getElementById('resetBtn').addEventListener('click', () => {
counter.reset();
})
</script>
</html>