当前位置: 首页 > news >正文

原生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动画特效,包含了面向对象设计、物理模拟、用户交互等核心要素。通过模块化的代码结构,你可以轻松扩展更多功能,如添加不同鱼种、实现食物追逐行为或增加更复杂的水面效果。

http://www.jsqmd.com/news/1137386/

相关文章:

  • LlamaEdge实战指南:基于WasmEdge的LLM边缘部署与对话应用
  • Windows Server 2016本地提权漏洞原理、利用与防御实战解析
  • Windows提权技术全解析:从权限模型到实战攻防
  • ICM-42688-P与PIC18F57Q43在工业自动化中的高精度运动控制
  • Windows系统安全加固:彻底关闭445端口防御永恒之蓝攻击
  • PCF8591与STM32G0B1RE的工业级数据采集系统设计
  • 2026年3款多平台财务决策记录转文字工具对比:手机/平板/PC适配深度评测
  • 微信视频号评论采集:逆向工程与反爬虫实战指南
  • pytest描述性命名规范:让测试代码成为自解释的活文档
  • 指纹浏览器真的能防止关联封号吗?效果怎么样
  • 终极指南:Flashtool专业刷机工具完全使用手册
  • 电商系统测试实战指南:从核心模块到高阶技能
  • 嵌入式系统电源管理:TI TPS65263与PIC32MZ的集成方案
  • Cursor 2.0 + Streamlit 实战:AI 驱动的实时汇率转换应用开发
  • 基于async-http-client与HMAC-SHA256的HTTP请求签名实战指南
  • AI大模型技术祛魅:从核心能力到工程落地的实战剖析
  • 国产AI编程工具选型与合规部署指南
  • PostgreSQL 16 + pgAdmin 4 部署:5步配置外网访问与 PostGIS 插件
  • 6DoF IMU与微控制器的运动跟踪技术优化实践
  • Python timedelta 本质解析:时间长度标尺与业务超时健壮实践
  • 编程小白入门AI编程:从零上手Codex与GitHub Copilot实战指南
  • 基于DeepSeek构建本地AI编程助手:从原理到实践
  • PostgreSQL 动态SQL 3种实现方式对比:format函数 vs quote_ident vs 连接符
  • PloneFormGen邮件适配器深度解析:字符集、附件与安全加固
  • Docker部署AI-CRM系统:本地一键启动与功能验证指南
  • 中兴光猫权限解锁:从受限用户到完全掌控的技术探索之旅
  • UCI与sklearn数据集对比:3大维度解析5个经典数据集的适用场景
  • OpenCV adaptiveThreshold 实战:高斯与均值法对比,3组参数效果差异解析
  • fdisk 实战:MBR 与 GPT 分区表对比与 2TB 容量限制解析
  • 免费开源鼠标连点器MouseClick:三分钟掌握自动化点击技巧