尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
原生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 charsetUTF-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 idfish-container/div script srcfish-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); } }波浪传播原理每个点受弹簧力作用趋向基准位置相邻点通过力传递形成波动鼠标交互施加额外作用力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. 高级技巧与扩展思路性能监控添加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}); } };响应式设计根据设备性能动态调整鱼的数量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)); } }视觉效果增强添加水花溅起效果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动画特效包含了面向对象设计、物理模拟、用户交互等核心要素。通过模块化的代码结构你可以轻松扩展更多功能如添加不同鱼种、实现食物追逐行为或增加更复杂的水面效果。
RELATED

相关推荐

OWASP ZAP:开源Web应用安全扫描工具实战指南

OWASP ZAP:开源Web应用安全扫描工具实战指南

1. 项目概述:为什么我们需要一个“主动的”安全伙伴在Web应用开发与测试的日常里,安全常常是一个“事后诸葛亮”的角色。功能跑通了,界面好看了,性能达标了,最后才想起来:“安全测试做了吗?” 结…

📅 2026/9/27 1:55:17
3 种 IRT 模型对比:Rasch vs 2PL vs 3PL 在知识追踪中的选择与性能差异

3 种 IRT 模型对比:Rasch vs 2PL vs 3PL 在知识追踪中的选择与性能差异

3 种 IRT 模型对比:Rasch vs 2PL vs 3PL 在知识追踪中的选择与性能差异当教育科技团队需要构建自适应学习系统时,项目反应理论(IRT)模型的选择往往成为关键决策点。面对Rasch、2PL和3PL这三种经典模型,开发者常陷入两难…

📅 2026/9/27 11:37:12
Adam vs AdamW 实战对比:在 BERT 微调任务中验证 0.01 权重衰减效果

Adam vs AdamW 实战对比:在 BERT 微调任务中验证 0.01 权重衰减效果

AdamW vs Adam:BERT微调任务中权重衰减的实战验证与深度解析在自然语言处理领域,优化器的选择往往决定着模型微调的成败。当我们面对BERT这样的基础模型时,Adam和AdamW这两个名字总会频繁出现在技术讨论和论文实现中。但究竟哪种优化器更适合…

📅 2026/10/5 21:02:18
MORE NEWS

更多资讯

📰

特种作业考试实操没底?卡片电工证在哪里查询?网上查询一步搞定!

特种作业考试实操没底?卡片电工证在哪里查询?网上查询一步搞定! 实操考试心里没底怕挂科,卡片电工证在哪里查询?网上查询一步搞定,别再被信息差耽误考试机会。作为驻马店市政公用工程从业者,很多人在准备电工证考试时,最怕的就是操作流程不熟悉、考试…

📰

引路教育电工证上岗必须要这个证吗

电工证实操考试心里没底怕挂科?电子证书怎么拿更稳妥? 实操考试心里没底怕挂科,这是很多准备考电工证的朋友最真实的状态。特别是第一次接触特种作业,面对高压设备和复杂的操作流程,难免紧张。而如今,很多单位已经全面启用 电子证书…

📰

韶关市电工证办理对学历不高的人来说真的难吗?上岗必备怎么搞定

韶关市电工证办理对学历不高的人来说真的难吗?上岗必备怎么搞定 学历不高怕报不上名?韶关市电工证办理其实不难,关键是你怎么操作。很多人一听说要办电工证,第一反应就是“我学历不够,肯定报不了”。但其实,只要你满足基本条件, 上岗必备…

📰

郑州电工证考点有哪些良心建议实操考试心里没底怕挂科

郑州电工证考点有哪些良心建议实操考试心里没底怕挂科 实操考试心里没底怕挂科,这几乎是每个准备考电工证的学员都有的感受。尤其是对于郑州地区的电工学员来说,考点分布、考试难度、报名流程这些都直接影响着最终能否顺利拿到证。作为在特种作业操作证领域…

📰

低压电工证要三年一次郑州报考避坑指南

低压电工证要三年一次郑州报考避坑指南 工地太忙根本没时间复习考试,低压电工证要三年一次,很多老电工都愁得不行,尤其在郑州,报考流程复杂、费用不透明、复审时间卡得严,稍不注意就错过了关键节点。别急,今天咱就聊聊低压电工证三年一次这事,怎么报、…

📰

贵州云岩电工证怎么考?郑州报考避坑指南全解析

贵州云岩电工证怎么考?郑州报考避坑指南全解析 急需特种作业证才能上岗找工作,尤其是电工证,可以说是很多从业者进入行业的“门槛证”。尤其是在贵州云岩地区,电工证不仅关乎能否顺利入职,更与薪资水平、职业晋升息息相关。如果你在考虑考取贵州云岩电工…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬