原生JS Canvas 小鱼游动特效:350行代码实现3条鱼与水面交互(附源码)

📅 2026/7/6 22:18:48 👁️ 阅读次数 📝 编程学习
原生JS Canvas 小鱼游动特效:350行代码实现3条鱼与水面交互(附源码)

原生JavaScript实现Canvas小鱼游动特效:从零构建高性能交互式动画

在网页设计中,动态效果往往能显著提升用户体验。本文将带你从零开始,用原生JavaScript和Canvas API实现一个高性能的小鱼游动特效,包含3条智能游动的鱼儿与水面波浪的交互效果。不同于依赖jQuery等库的传统实现,我们将专注于原生代码的性能优化与模块化设计。

1. Canvas动画基础与环境搭建

Canvas是HTML5提供的绘图API,它通过JavaScript直接在网页上绘制图形。相比DOM操作,Canvas更适合实现复杂的动画效果,因为它避免了频繁的重排和重绘。

首先创建基础HTML结构:

<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>小鱼游动特效</title> <style> #fish-container { width: 100%; height: 200px; position: fixed; bottom: 0; left: 0; z-index: -1; opacity: 0.4; } </style> </head> <body> <div id="fish-container"></div> <script src="fish-animation.js"></script> </body> </html>

关键CSS设置说明:

  • position: fixed使动画固定在窗口底部
  • z-index: -1确保内容显示在动画上方
  • opacity控制透明度实现水纹效果

2. 核心动画架构设计

我们采用面向对象的方式组织代码,主要包含三个核心类:

class FishAnimation { constructor(containerId) { this.container = document.getElementById(containerId); this.canvas = document.createElement('canvas'); this.context = this.canvas.getContext('2d'); this.container.appendChild(this.canvas); // 初始化参数 this.fishes = []; this.surfacePoints = []; this.animationId = null; this.lastTimestamp = 0; // 绑定事件 window.addEventListener('resize', this.handleResize.bind(this)); this.canvas.addEventListener('mousemove', this.handleMouseMove.bind(this)); this.init(); } init() { this.setCanvasSize(); this.createSurface(); this.createFishes(); this.startAnimation(); } // 其他方法将在后续章节实现... }

2.1 性能优化关键参数

参数名默认值说明
FISH_COUNT3鱼的数量
POINT_INTERVAL5水面波浪点间隔(像素)
WAVE_DAMPING0.9波浪阻尼系数
FISH_SPEED2-5鱼游动速度范围
MOUSE_INFLUENCE0.02鼠标影响系数

3. 水面波浪效果实现

水面效果基于弹簧质点模型,每个点受相邻点影响形成波浪传播:

class SurfacePoint { constructor(animation, x) { this.animation = animation; this.x = x; this.baseHeight = animation.height * 0.5; this.height = this.baseHeight; this.velocity = 0; this.force = 0; } update() { // 弹簧力计算 const springForce = 0.03 * (this.baseHeight - this.height); this.velocity += springForce; this.velocity *= 0.9; // 阻尼 this.height += this.velocity; } interact(y, velocity) { const direction = (this.height - y) >= 0 ? -1 : 1; this.velocity += direction * Math.abs(velocity) * 0.01; } draw(ctx) { ctx.lineTo(this.x, this.animation.height - this.height); } }

波浪传播原理:

  1. 每个点受弹簧力作用趋向基准位置
  2. 相邻点通过力传递形成波动
  3. 鼠标交互施加额外作用力

4. 鱼类行为模拟

每条鱼都有自己的运动逻辑和外观特征:

class Fish { constructor(animation) { this.animation = animation; this.reset(); this.tailAngle = 0; this.finAngle = 0; } reset() { this.x = Math.random() < 0.5 ? -50 : this.animation.width + 50; this.y = Math.random() * this.animation.height; this.speed = 2 + Math.random() * 3; this.direction = this.x < 0 ? 1 : -1; this.amplitude = 10 + Math.random() * 20; this.frequency = 0.05 + Math.random() * 0.1; } update() { this.x += this.speed * this.direction; // 正弦游动路径 const progress = (this.x / this.animation.width) * Math.PI; this.y += Math.sin(progress * this.frequency) * this.amplitude; // 边界检测 if ((this.direction > 0 && this.x > this.animation.width + 50) || (this.direction < 0 && this.x < -50)) { this.reset(); } // 尾部摆动 this.tailAngle = Math.sin(Date.now() * 0.005) * 0.5; this.finAngle = Math.sin(Date.now() * 0.003) * 0.3; } draw(ctx) { ctx.save(); ctx.translate(this.x, this.y); ctx.rotate(Math.PI + Math.atan2( Math.sin(Date.now() * 0.005) * 2, this.direction )); // 鱼身绘制 ctx.beginPath(); ctx.moveTo(-20, 0); ctx.bezierCurveTo(-15, 10, 10, 8, 30, 0); ctx.bezierCurveTo(10, -8, -15, -10, -20, 0); ctx.fillStyle = 'hsl(200, 70%, 50%)'; ctx.fill(); // 鱼尾绘制 ctx.save(); ctx.translate(30, 0); ctx.scale(0.9 + 0.1 * Math.sin(this.tailAngle), 1); ctx.beginPath(); ctx.moveTo(0, 0); ctx.quadraticCurveTo(5, 10, 15, 8); ctx.quadraticCurveTo(10, 0, 15, -8); ctx.quadraticCurveTo(5, -10, 0, 0); ctx.fill(); ctx.restore(); ctx.restore(); } }

5. 动画循环与性能优化

使用requestAnimationFrame实现高效动画循环:

class FishAnimation { // ...之前代码... startAnimation() { const animate = (timestamp) => { // 计算时间差实现帧率无关动画 const deltaTime = timestamp - this.lastTimestamp; this.lastTimestamp = timestamp; this.update(deltaTime); this.draw(); this.animationId = requestAnimationFrame(animate); }; this.animationId = requestAnimationFrame(animate); } update(deltaTime) { const scale = deltaTime / 16; // 标准化到60fps // 更新水面点 this.surfacePoints.forEach(point => point.update()); // 传播波浪力 for (let i = 1; i < this.surfacePoints.length; i++) { const prev = this.surfacePoints[i-1]; const curr = this.surfacePoints[i]; const force = 0.3 * (prev.height - curr.height); prev.velocity -= force * scale; curr.velocity += force * scale; } // 更新鱼位置 this.fishes.forEach(fish => fish.update()); } draw() { const ctx = this.context; ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); // 绘制鱼 this.fishes.forEach(fish => fish.draw(ctx)); // 绘制水面 ctx.save(); ctx.globalCompositeOperation = 'xor'; ctx.fillStyle = 'hsl(200, 50%, 70%)'; ctx.beginPath(); ctx.moveTo(0, this.canvas.height); this.surfacePoints.forEach(point => point.draw(ctx)); ctx.lineTo(this.canvas.width, this.canvas.height); ctx.closePath(); ctx.fill(); ctx.restore(); } handleResize() { this.setCanvasSize(); this.createSurface(); } handleMouseMove(e) { const rect = this.canvas.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; // 找到最近的水面点施加影响 const index = Math.round(x / this.pointInterval); if (index > 0 && index < this.surfacePoints.length) { this.surfacePoints[index].interact(y, e.movementY); } } }

性能提示:在动画循环中避免频繁创建对象,尽量复用变量。使用transform代替直接坐标计算可以提升渲染性能。

6. 完整实现与参数调优

将所有部分组合起来并添加配置参数:

// 初始化动画 const animation = new FishAnimation('fish-container', { fishCount: 3, pointInterval: 5, waveDamping: 0.9, mouseInfluence: 0.02 }); // 可调参数示例 class FishAnimation { constructor(containerId, options = {}) { this.config = { fishCount: options.fishCount || 3, pointInterval: options.pointInterval || 5, waveDamping: options.waveDamping || 0.9, mouseInfluence: options.mouseInfluence || 0.02, waveSpeed: options.waveSpeed || 0.3 }; // ...其余初始化代码... } createSurface() { const count = Math.round(this.canvas.width / this.config.pointInterval); this.pointInterval = this.canvas.width / (count - 1); this.surfacePoints = []; for (let i = 0; i < count; i++) { const point = new SurfacePoint(this, i * this.pointInterval); if (i > 0) { point.prev = this.surfacePoints[i-1]; this.surfacePoints[i-1].next = point; } this.surfacePoints.push(point); } } createFishes() { this.fishes = []; for (let i = 0; i < this.config.fishCount; i++) { this.fishes.push(new Fish(this)); } } // ...其余方法... }

7. 高级技巧与扩展思路

  1. 性能监控:添加FPS计数器检测性能
let fps = 0; let lastFpsUpdate = 0; let frameCount = 0; const updateFps = (timestamp) => { frameCount++; if (timestamp - lastFpsUpdate >= 1000) { fps = frameCount; frameCount = 0; lastFpsUpdate = timestamp; console.log(`FPS: ${fps}`); } };
  1. 响应式设计:根据设备性能动态调整鱼的数量
handleResize() { this.setCanvasSize(); this.createSurface(); // 根据屏幕尺寸调整鱼数量 const area = this.canvas.width * this.canvas.height; const targetCount = Math.min( 5, Math.floor(area / (250 * 250) * this.config.fishCount) ); while (this.fishes.length > targetCount) { this.fishes.pop(); } while (this.fishes.length < targetCount) { this.fishes.push(new Fish(this)); } }
  1. 视觉效果增强:添加水花溅起效果
class Splash { constructor(x, y, power) { this.particles = []; for (let i = 0; i < 10; i++) { this.particles.push({ x, y, vx: (Math.random() - 0.5) * power, vy: -Math.random() * power * 0.5, size: 1 + Math.random() * 3, life: 30 + Math.random() * 20 }); } } update() { this.particles.forEach(p => { p.x += p.vx; p.y += p.vy; p.vy += 0.1; // 重力 p.life--; }); return this.particles.some(p => p.life > 0); } draw(ctx) { ctx.save(); ctx.fillStyle = 'hsla(200, 100%, 70%, 0.7)'; this.particles.forEach(p => { if (p.life > 0) { ctx.globalAlpha = p.life / 50; ctx.beginPath(); ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2); ctx.fill(); } }); ctx.restore(); } }

这个350行左右的实现完整呈现了一个高性能的Canvas动画特效,包含了面向对象设计、物理模拟、用户交互等核心要素。通过模块化的代码结构,你可以轻松扩展更多功能,如添加不同鱼种、实现食物追逐行为或增加更复杂的水面效果。