如何使用JavaScript实现2D垂直流体物理效果及渲染
复刻Fresh首页垂直流体动画的实现方案与零基础学习路径
先明确效果本质
你要做的这个效果根本不需要啃复杂的计算流体力学,Fresh首页的流体是视觉优化的层叠垂直波动效果,不是物理精度拉满的真实水流模拟,2D Canvas就能实现,WebGL只是后续性能优化的选项,完全没必要一开始就上高难度技术栈。
分阶段学习路径(零基础友好)
- 第一阶段(1-2周,补必备基础):先摸透Canvas 2D的核心API,不用全学,重点练
requestAnimationFrame动画循环逻辑、贝塞尔/直线路径绘制、线性/径向渐变填充、透明度控制这几个点,能独立写个随时间动的平滑曲线就算过关。别一开始冲WebGL,着色器、缓冲区这些概念对没接触过图形学的人门槛太高,先把2D版本的逻辑跑通再考虑性能迁移。 - 第二阶段(1周,吃透核心原理):核心逻辑就是不同参数的正弦波叠加+垂直方向扰动传播,属于用简单数学模拟视觉效果的典型方案,和真实流体物理关系不大。先从单条波动线写起:给正弦波设置随机初始相位、不同波高、不同传播速度,叠3-5层半透明同色系的波浪,动起来就有流体的层次感了。
- 第三阶段(2周,补交互与质感):学一下简单的一维高度场实现逻辑,做到鼠标移动/点击、页面滚动时,接触点产生垂直方向的波纹,向两侧扩散同时自然衰减,把这个扰动叠加到之前的波浪曲线上,再加一点边缘柔化、碎波细节,还原度就能到90%以上。
- 第四阶段(可选,性能优化):如果要适配低性能移动端、或者要加更多粒子/细节,再把逻辑迁移到WebGL,用片元着色器计算波动值,性能会比Canvas 2D高一个数量级。
可直接落地的实现步骤
- 初始化流体层配置:提前定义4-6个独立的流体层,每个层单独配参数,错落开才真实:
- 基础垂直位置:越靠上层的波浪y坐标越小,颜色越浅、透明度越低
- 波振幅:也就是垂直方向波动的最大高度,从上层到下层取3-15像素不等,不要统一值
- 传播速度:每层速度差控制在0.2-0.5之间,同速移动的波浪会像死板的背景板
- 填充色:选同色系不同透明度的颜色,从浅到高层层叠加
- 写基础动画循环:每一帧给每个层的正弦波相位累加对应速度的偏移,逐x坐标计算当前点的y轴位置,用路径把所有点连起来,底部封到画布下边缘,填充对应颜色即可。跑通这一步就已经有80%的目标效果了。
- 加交互扰动:维护一个和画布宽度等长的一维数组当高度场,每次交互事件触发时,把对应x坐标位置的高度场值加一个垂直方向的偏移量;每一帧遍历高度场,让每个点的扰动向左右相邻点传播,同时乘一个0.9左右的阻尼系数让扰动自然消失,最后把高度场的偏移值叠加到之前算的正弦波y坐标上,就能实现碰一下就出波纹的真实感。
- 细节调优:给最上层的波浪叠一点高频的小振幅碎波,不要全是光滑的大正弦曲线;层和层之间加轻微的模糊过渡,不要留硬边,质感会提升很多。
新手避坑提醒
- 别一开始就去找纳维-斯托克斯方程、全量CFD流体模拟的代码,那些是做烟雾、真实水流泼溅效果的,计算量大性能差,和你要的简洁垂直流体效果完全不匹配,属于纯纯的过度设计。
- 动画循环里不要频繁创建新对象、不要做DOM查询操作,尽量复用变量,不然帧率会掉得很明显。
- 不要用固定时间步长之外的逻辑改动画速度,不同刷新率的屏幕上速度差会非常大。
下面是最小可运行的核心逻辑片段,存成本地html打开就能看到基础效果,后续可以在这个基础上加扰动、调参数:
<canvas id="fluid" style="display:block;width:100vw;height:100vh;margin:0"></canvas> <script> const canvas = document.getElementById('fluid') const ctx = canvas.getContext('2d') // 适配高DPI屏幕 const dpr = window.devicePixelRatio || 1 function resize() { canvas.width = window.innerWidth * dpr canvas.height = window.innerHeight * dpr ctx.scale(dpr, dpr) } resize() window.addEventListener('resize', resize) // 初始化5层流体配置 const layers = Array.from({length:5}, (_,i)=>({ baseY: window.innerHeight * 0.55 + i*22, amp: 6 + i*2.5, speed: 0.018 + i*0.007, phase: Math.random() * Math.PI * 2, fill: `rgba(0, 124, 196, ${0.12 + i*0.11})` })) function render() { ctx.clearRect(0, 0, window.innerWidth, window.innerHeight) layers.forEach(layer => { layer.phase += layer.speed ctx.beginPath() ctx.moveTo(0, window.innerHeight) // 逐段绘制波浪线 for(let x=0; x<=window.innerWidth; x+=8) { const y = layer.baseY + Math.sin(x*0.009 + layer.phase) * layer.amp ctx.lineTo(x, y) } ctx.lineTo(window.innerWidth, window.innerHeight) ctx.closePath() ctx.fillStyle = layer.fill ctx.fill() }) requestAnimationFrame(render) } render() </script>
内容的提问来源于stack exchange,提问作者Ab. C.
相关产品推荐
相关产品推荐

