以下是一个使用 requestAnimationFrame 实现的简单 canvas 动画:一个彩色小球在画布边界内反弹并留下渐变轨迹。
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>requestAnimationFrame 简单动画 - 弹跳小球</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: linear-gradient(145deg, #1a2a3a 0%, #0f1a24 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
font-family: 'Segoe UI', Roboto, system-ui, sans-serif;
}
.container {
background: rgba(0, 0, 0, 0.3);
border-radius: 2rem;
padding: 1rem;
box-shadow: 0 20px 35px rgba(0, 0, 0, 0.4);
}
canvas {
display: block;
margin: 0 auto;
border-radius: 1rem;
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.5);
cursor: pointer;
}
.info {
text-align: center;
margin-top: 1rem;
color: #eef4ff;
font-weight: 500;
text-shadow: 0 1px 2px black;
}
.info p {
font-size: 0.9rem;
background: rgba(0, 0, 0, 0.5);
display: inline-block;
padding: 0.3rem 1rem;
border-radius: 2rem;
backdrop-filter: blur(4px);
}
button {
margin-top: 0.8rem;
background: #ffb347;
border: none;
padding: 6px 18px;
border-radius: 40px;
font-weight: bold;
cursor: pointer;
transition: 0.2s;
color: #2c3e2f;
box-shadow: 0 2px 6px rgba(0,0,0,0.3);
}
button:hover {
background: #ff9f1c;
transform: scale(1.02);
}
</style>
</head>
<body>
<div>
<div class="container">
<canvas id="bouncingCanvas" width="800" height="500" style="width:800px; height:500px"></canvas>
<div class="info">
<p>⚡ 弹跳小球 | 渐变轨迹 | requestAnimationFrame 驱动</p>
<button id="resetBtn">🔄 重置位置 / 随机速度</button>
</div>
</div>
</div>
<script>
(function() {
// ----- 获取 canvas 元素和上下文 -----
const canvas = document.getElementById('bouncingCanvas');
const ctx = canvas.getContext('2d');
// ----- 动画参数 -----
let ball = {
x: canvas.width / 2,
y: canvas.height / 2,
radius: 24,
vx: 3.2, // 水平速度 (px/帧)
vy: 2.7, // 垂直速度
color: '#ffaa44'
};
// 辅助变量:轨迹淡入淡出效果 (半透明填充背景实现残影/轨迹)
// 为了避免无限拖尾,每帧用低透明度填充背景,制造运动模糊轨迹感
// ----- 辅助函数:重置小球随机位置/速度 (但不超出边界)-----
function resetBall() {
ball.x = Math.random() * (canvas.width - 2 * ball.radius) + ball.radius;
ball.y = Math.random() * (canvas.height - 2 * ball.radius) + ball.radius;
// 随机速度 (-4 ~ 4 之间,避免为零)
ball.vx = (Math.random() - 0.5) * 7;
ball.vy = (Math.random() - 0.5) * 7;
// 保证速度绝对值不太小,否则动画太慢
if (Math.abs(ball.vx) < 0.8) ball.vx += (ball.vx >= 0 ? 0.9 : -0.9);
if (Math.abs(ball.vy) < 0.8) ball.vy += (ball.vy >= 0 ? 0.9 : -0.9);
// 限制最大速度 6
ball.vx = Math.min(Math.max(ball.vx, -5.5), 5.5);
ball.vy = Math.min(Math.max(ball.vy, -5.5), 5.5);
// 动态改变一下颜色
ball.color = `hsl(${Math.random() * 360}, 70%, 60%)`;
}
// ----- 更新小球位置 + 边界碰撞 (弹性反弹)-----
function updatePosition() {
// 更新坐标
ball.x += ball.vx;
ball.y += ball.vy;
// 边界检测 (左右)
if (ball.x + ball.radius >= canvas.width) {
ball.x = canvas.width - ball.radius;
ball.vx = -ball.vx;
// 小彩蛋: 碰撞时微调颜色
ball.color = `hsl(${Math.random() * 360}, 75%, 58%)`;
} else if (ball.x - ball.radius <= 0) {
ball.x = ball.radius;
ball.vx = -ball.vx;
ball.color = `hsl(${Math.random() * 360}, 75%, 58%)`;
}
// 边界检测 (上下)
if (ball.y + ball.radius >= canvas.height) {
ball.y = canvas.height - ball.radius;
ball.vy = -ball.vy;
ball.color = `hsl(${Math.random() * 360}, 75%, 58%)`;
} else if (ball.y - ball.radius <= 0) {
ball.y = ball.radius;
ball.vy = -ball.vy;
ball.color = `hsl(${Math.random() * 360}, 75%, 58%)`;
}
}
// ----- 绘制画布 (背景 + 小球 + 光晕特效) -----
function draw() {
// 1. 拖尾/运动模糊效果: 用半透明黑填充全屏,让上一帧图像逐渐淡出
// 注意: 这种方式会形成"光轨"感觉,非常平滑
ctx.fillStyle = 'rgba(15, 26, 36, 0.27)'; // 透明度越低,轨迹越长
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 2. 绘制额外装饰网格线 (增加科技感,可选,但不影响核心动画)
ctx.save();
ctx.globalCompositeOperation = 'lighter';
ctx.beginPath();
ctx.strokeStyle = 'rgba(100, 180, 255, 0.2)';
ctx.lineWidth = 1;
for (let i = 0; i < canvas.width; i += 40) {
ctx.beginPath();
ctx.moveTo(i, 0);
ctx.lineTo(i, canvas.height);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(0, i % canvas.height);
ctx.lineTo(canvas.width, i % canvas.height);
ctx.stroke();
}
ctx.globalCompositeOperation = 'source-over';
// 3. 绘制小球 (带高光、阴影)
// 阴影 (增加立体感)
ctx.shadowColor = 'rgba(0,0,0,0.5)';
ctx.shadowBlur = 12;
ctx.shadowOffsetX = 3;
ctx.shadowOffsetY = 3;
// 径向渐变 让小球更生动
const grad = ctx.createRadialGradient(
ball.x - 5, ball.y - 5, 5,
ball.x, ball.y, ball.radius
);
grad.addColorStop(0, '#fff5c4');
grad.addColorStop(0.6, ball.color);
grad.addColorStop(1, `rgba(0,0,0,0.3)`);
ctx.beginPath();
ctx.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2);
ctx.fillStyle = grad;
ctx.fill();
// 绘制高光小白点
ctx.shadowBlur = 4;
ctx.beginPath();
ctx.arc(ball.x - 6, ball.y - 6, 6, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(255, 255, 245, 0.7)';
ctx.fill();
// 重置阴影
ctx.shadowBlur = 0;
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 0;
// 额外描边轮廓
ctx.beginPath();
ctx.arc(ball.x, ball.y, ball.radius - 1, 0, Math.PI * 2);
ctx.strokeStyle = 'rgba(255,210,90,0.7)';
ctx.lineWidth = 1.8;
ctx.stroke();
ctx.restore();
}
// ----- 主动画循环 (使用 requestAnimationFrame) -----
let animationId = null;
function animate() {
// 1. 更新物理(位置、碰撞)
updatePosition();
// 2. 绘制当前帧
draw();
// 3. 注册下一帧
animationId = requestAnimationFrame(animate);
}
// ----- 优雅的启动与重置控制 -----
function startAnimation() {
if (animationId) {
cancelAnimationFrame(animationId);
}
animate();
}
// 重置按钮交互 (重置位置和速度,同时改变颜色)
const resetBtn = document.getElementById('resetBtn');
resetBtn.addEventListener('click', () => {
resetBall();
// 可选:额外清屏+闪烁效果 (瞬间clear)
ctx.fillStyle = '#0f1a24';
ctx.fillRect(0, 0, canvas.width, canvas.height);
draw(); // 立即绘制重置后的位置让画面没有跳跃感
});
// 初始化随机速度一次(让每次刷新不同)
resetBall();
// 确保一开始画布背景
ctx.fillStyle = '#0f1a24';
ctx.fillRect(0, 0, canvas.width, canvas.height);
draw(); // 先画一帧避免白屏
startAnimation();
// 可选:页面可见性变化时,无需额外操作,但 requestAnimationFrame 自动暂停节省资源
// 防止页面关闭后动画继续 (不需要额外代码,浏览器已做优化)
// 窗口resize时 canvas尺寸固定为800x500, 无需处理
})();
</script>
</body>
</html>
动画说明
- 核心驱动 :
requestAnimationFrame循环调用animate()函数,每帧更新小球位置并重绘。 - 物理效果 :小球拥有速度
vx/vy,碰到边界时速度反向(弹性碰撞),同时随机改变颜色增加趣味。 - 视觉风格 :
- 半透明背景填充制造"拖尾/轨迹"效果,形成运动模糊感。
- 径向渐变 + 高光让小球更立体。
- 半透明网格线增强科技感。
- 交互:点击重置按钮可随机重置小球位置、速度以及颜色。
你可以直接复制代码到 .html 文件中并用浏览器打开,即可看到流畅的动画效果。