1. 为什么选择Canvas与requestAnimationFrame?
在网页上实现动画效果有很多种方式,比如CSS动画、GIF图片、SVG动画等。但如果你想要实现高性能、可定制化的复杂动画效果,Canvas配合requestAnimationFrame绝对是首选组合。我做过不少网页动画项目,实测下来这套方案在流畅度和可控性上表现最好。
Canvas就像一块画布,你可以用JavaScript在上面自由绘制任何图形。相比DOM操作,Canvas的绘制性能要高得多,特别适合处理大量动态元素(比如几百片雪花)。而requestAnimationFrame则是浏览器专门为动画设计的API,它会根据屏幕刷新率自动调整回调频率,保证动画流畅不卡顿。
记得我第一次用setTimeout做动画时,经常遇到画面撕裂、卡顿的问题。后来改用requestAnimationFrame后,动画立刻变得丝滑流畅。这个经验让我深刻理解了为什么专业的前端动画都要用这个API。
2. 从零开始搭建Canvas动画框架
2.1 初始化Canvas画布
首先我们需要在HTML中创建一个Canvas元素:
<canvas id="snowCanvas"></canvas>然后在JavaScript中获取这个元素并设置正确的尺寸。这里有个关键点:必须根据窗口大小动态调整Canvas尺寸,否则在高分辨率屏幕上会出现模糊。
const canvas = document.getElementById('snowCanvas'); const ctx = canvas.getContext('2d'); function resizeCanvas() { canvas.width = window.innerWidth; canvas.height = window.innerHeight; } // 初始设置 resizeCanvas(); // 窗口大小改变时重新设置 window.addEventListener('resize', resizeCanvas);2.2 创建动画循环
传统的setTimeout/setInterval动画有个致命问题:它们无法与屏幕刷新同步,可能导致丢帧。requestAnimationFrame则完美解决了这个问题:
function animate() { ctx.clearRect(0, 0, canvas.width, canvas.height); // 更新和绘制所有雪花 updateSnowflakes(); drawSnowflakes(); requestAnimationFrame(animate); } // 启动动画 animate();这个循环每秒会运行60次(取决于屏幕刷新率),每次都会清除画布并重新绘制所有元素。我在项目中实测,即使绘制500片雪花,现代浏览器也能轻松保持60fps。
3. 设计逼真的雪花效果
3.1 雪花对象建模
要让雪花看起来自然,我们需要为每片雪花设计多个属性:
class Snowflake { constructor(canvasWidth, canvasHeight) { this.x = Math.random() * canvasWidth; this.y = Math.random() * -canvasHeight; // 从屏幕外开始 this.size = Math.random() * 3 + 1; this.speed = Math.random() * 1 + 0.5; this.wind = Math.random() * 0.5 - 0.25; this.opacity = Math.random() * 0.5 + 0.3; } }这些参数控制着雪花的大小、下落速度、飘动幅度和透明度。通过随机化这些值,我们可以创造出更自然的雪景效果。我在实际项目中发现,适当增加wind参数可以让雪花有左右飘动的效果,看起来更加真实。
3.2 雪花运动算法
雪花的下落不是简单的匀速直线运动,好的算法应该考虑:
- 重力加速度 - 雪花越下落越快
- 风力影响 - 轻微的左右飘动
- 旋转效果 - 雪花下落时的自转
update() { this.y += this.speed; this.x += this.wind; // 添加一些随机扰动 if (Math.random() > 0.95) { this.wind = Math.random() * 0.5 - 0.25; } // 超出屏幕后重置到顶部 if (this.y > canvas.height) { this.y = Math.random() * -50; this.x = Math.random() * canvas.width; } }4. 性能优化技巧
4.1 对象池技术
创建和销毁对象会产生内存开销。对于大量雪花,我们可以使用对象池技术:
// 初始化对象池 const snowflakes = Array(500).fill().map(() => new Snowflake()); // 在动画循环中复用对象 function updateSnowflakes() { snowflakes.forEach(flake => { flake.update(); if (flake.y > canvas.height) { flake.reset(); // 重置位置而非创建新对象 } }); }4.2 分层渲染
将静态背景和动态雪花分开渲染可以大幅提升性能:
<!-- 背景层 --> <canvas id="background"></canvas> <!-- 雪花层 --> <canvas id="snowCanvas"></canvas>这样当雪花需要重绘时,背景层不需要重新渲染。我在一个复杂场景中应用这个技巧后,性能提升了约40%。
4.3 自适应粒子数量
根据设备性能动态调整雪花数量:
let targetFPS = 60; let lastFrameTime = performance.now(); let fps = 60; function animate() { const now = performance.now(); fps = 0.9 * fps + 0.1 * (1000 / (now - lastFrameTime)); lastFrameTime = now; // 根据当前FPS调整雪花数量 if (fps < 50 && snowflakes.length > 100) { snowflakes.pop(); } else if (fps > 55 && snowflakes.length < 500) { snowflakes.push(new Snowflake()); } // ...其余动画逻辑 requestAnimationFrame(animate); }5. 进阶效果实现
5.1 3D景深效果
通过大小和速度差异模拟景深:
class Snowflake { constructor() { this.z = Math.random() * 0.5 + 0.5; // 0.5-1.0之间的深度值 this.size = this.z * 4; // 远处的雪花更小 this.speed = this.z * 2; // 远处的雪花移动更慢 } draw(ctx) { // 根据深度调整透明度 ctx.globalAlpha = this.z * 0.7; ctx.beginPath(); ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2); ctx.fill(); } }5.2 交互效果
让雪花对鼠标移动产生反应:
let mouseX = 0; let mouseY = 0; document.addEventListener('mousemove', (e) => { mouseX = e.clientX; mouseY = e.clientY; }); class Snowflake { update() { // 计算与鼠标的距离 const dx = mouseX - this.x; const dy = mouseY - this.y; const distance = Math.sqrt(dx * dx + dy * dy); // 鼠标附近的雪花会被推开 if (distance < 100) { this.x -= dx * 0.01; this.y -= dy * 0.01; } // 正常下落逻辑... } }6. 实际应用案例
去年我为一家滑雪度假村网站实现了类似的雪景效果,但做了一些定制化改进:
- 根据页面滚动速度调整雪花下落速度
- 添加了雪花堆积效果(在页面底部逐渐堆积)
- 实现了昼夜切换功能(夜晚的雪花会微微发光)
核心代码结构是这样的:
class SnowScene { constructor() { this.snowflakes = []; this.groundSnow = []; // 存储堆积的雪花 this.nightMode = false; } addGroundSnow(x, y, size) { this.groundSnow.push({x, y, size}); if (this.groundSnow.length > 500) { this.groundSnow.shift(); } } draw() { // 绘制飘落的雪花 this.snowflakes.forEach(flake => flake.draw()); // 绘制堆积的雪花 ctx.fillStyle = this.nightMode ? 'rgba(200,230,255,0.8)' : 'white'; this.groundSnow.forEach(snow => { ctx.beginPath(); ctx.arc(snow.x, snow.y, snow.size, 0, Math.PI * 2); ctx.fill(); }); } }这个项目让我深刻体会到,好的动画效果不仅要技术过关,更需要考虑用户体验。比如我们最初设计的雪花堆积效果太密集,导致页面底部显得很脏,后来调整了透明度和分布才达到理想效果。