气泡浮力与流体动力学:Canvas 模拟水下气泡上升与破裂动效
在现代 Web 互动营销、水下科幻主题大屏以及先锋液体微交互设计中,“晶莹剔透的水下气泡(Underwater Bubbles)升腾与表面张力破裂动效”是一种能极大赋予界面生机、空灵与梦幻呼吸感的顶级视觉元素。
然而,许多初级前端在手写气泡动效时,往往只是简单地让圆形沿一条垂直直线匀速向上平移(y -= 2):
- 这种伪气泡看起来就像一颗颗死板的塑料小珠子在真空轨道中移动,缺乏水下流体的粘滞阻力刹车感;
- 真实世界中的水下气泡在上升时,由于背风侧周期性涡旋脱落(Vortex Shedding / 卡门涡街效应),必定会产生极其灵动的横向 S 形正弦左右摇曳(Wobbling Swirl);
- 当气泡升至水面的一瞬间,必须经历表面张力拉伸、并在毫秒内爆裂化作一团微小的晶莹飞溅水珠(Droplets Burst)!
将阿基米德浮力定律、斯托克斯流体阻力与涡街摆动微分方程融合进 Canvas 粒子动力学,我们能够以极轻的纯数学算力,在屏幕上呈现出宛如置身深海般逼真空灵的物理气泡升腾画卷。
气泡流体动力学的四大物理方程推导
设单个气泡为半径为 $r$ 的微小球体,其在水中的空间坐标为 $\mathbf{p} = (x, y)$,上升速度为 $\mathbf{v} = (v_x, v_y)$。
每一帧作用在气泡上的流体力学合力由四部分构成:
[气泡在水下受力平衡图] ▲ │ 阿基米德浮力 (F_buoyancy = 4/3 * pi * r^3 * rho_water * g) ┌───┴───┐ 流体涡街横向摆动力 ◀───────────────┤ 气泡 ├───────────────▶ 流体涡街横向摆动力 (F_wobble = A * sin(omega * t)) └───┬───┘ (周期性左右交替产生 S 形轨迹) │ ▼ 斯托克斯流体粘滞阻力 (F_drag = -6 * pi * eta * r * v)1. 向上阿基米德浮力(Buoyancy Force)
$$F_{buoyancy} = \rho_{water} \cdot V \cdot g = \frac{4}{3} \pi r^3 \cdot \rho_{water} \cdot g$$
气泡体积越大($r$ 越大),受到的向上浮力呈三次方程爆发式增长,上升初速度越快!
2. 斯托克斯流体粘滞阻力(Stokes' Drag Law)
对于在低雷诺数粘性流体中运动的球体,水流阻力与速度的一次方成正比,方向严格向下:
$$F_{drag} = -6 \pi \cdot \eta \cdot r \cdot v_y$$
当浮力与阻力完全平衡时,气泡达到终端收尾上升速度(Terminal Velocity)。
3. 涡街横向正弦摆动(Vortex Wobbling)
气泡在上升过程中,两侧不对称脱落的微旋涡对气泡施加横向周期性推力:
$$x(t) = x_0 + A_{wobble} \cdot \sin(\omega \cdot t + \phi_0)$$
其中摆动频率 $\omega$ 与气泡尺寸反相关(小气泡高频微颤,大气泡从容大摆)。
TypeScript 纯数学气泡物理引擎实现
// underwater-bubble-engine.ts export interface BubbleBurstDroplet { x: number; y: number; vx: number; vy: number; alpha: number; } export class FluidBubble { public x: number; public y: number; public baseRadius: number; public currentRadius: number; public vy: number = 0; public wobblePhase: number; public wobbleSpeed: number; public wobbleAmplitude: number; public isBursting: boolean = false; public burstDroplets: BubbleBurstDroplet[] = []; constructor(startX: number, startY: number, radius: number) { this.x = startX; this.y = startY; this.baseRadius = radius; this.currentRadius = radius; this.wobblePhase = Math.random() * Math.PI * 2; this.wobbleSpeed = (1.5 / radius) * 0.08 + 0.02; // 小气泡摆动更快 this.wobbleAmplitude = radius * 0.8; } // 1. 物理步进 public update(waterSurfaceY: number) { if (this.isBursting) { // 破裂微粒子物理衰减 for (let i = this.burstDroplets.length - 1; i >= 0; i--) { const d = this.burstDroplets[i]; d.x += d.vx; d.y += d.vy; d.vy += 0.15; // 微重力下坠 d.alpha -= 0.04; if (d.alpha <= 0) this.burstDroplets.splice(i, 1); } return; } // A. 浮力与阻力平衡推进 const terminalSpeed = -Math.min(6.5, this.baseRadius * 0.28 + 0.8); this.vy += (terminalSpeed - this.vy) * 0.1; this.y += this.vy; // B. 叠加涡街横向正弦摇曳 this.wobblePhase += this.wobbleSpeed; this.x += Math.cos(this.wobblePhase) * (this.wobbleAmplitude * 0.08); // C. 触达水面瞬间触发表面张力爆裂 if (this.y <= waterSurfaceY) { this.triggerBurst(); } } // 2. 触发气泡破裂微粒子飞溅 private triggerBurst() { this.isBursting = true; const dropletCount = Math.floor(this.baseRadius * 1.5); for (let i = 0; i < dropletCount; i++) { const angle = Math.random() * Math.PI * 2; const speed = Math.random() * 3.5 + 1.0; this.burstDroplets.push({ x: this.x, y: this.y, vx: Math.cos(angle) * speed, vy: Math.sin(angle) * speed - 1.5, // 向上喷射 alpha: 1.0, }); } } }Canvas 晶莹水下高光折射渲染
// bubble-canvas-stage.ts export class BubbleCanvasStage { private canvas: HTMLCanvasElement; private ctx: CanvasRenderingContext2D; private bubbles: FluidBubble[] = []; private waterSurfaceY: number = 60; constructor(canvas: HTMLCanvasElement) { this.canvas = canvas; this.ctx = canvas.getContext('2d')!; this.spawnBubbles(30); } private spawnBubbles(count: number) { for (let i = 0; i < count; i++) { this.bubbles.push(new FluidBubble( Math.random() * this.canvas.width, this.canvas.height + Math.random() * 400, Math.random() * 12 + 4 )); } } public renderLoop = () => { const w = this.canvas.width; const h = this.canvas.height; // 深海幽蓝渐变底板 const bgGrad = this.ctx.createLinearGradient(0, 0, 0, h); bgGrad.addColorStop(0, '#0f172a'); bgGrad.addColorStop(1, '#020617'); this.ctx.fillStyle = bgGrad; this.ctx.fillRect(0, 0, w, h); // 绘制水面微光线 this.ctx.strokeStyle = 'rgba(56, 189, 248, 0.4)'; this.ctx.lineWidth = 1.5; this.ctx.beginPath(); this.ctx.moveTo(0, this.waterSurfaceY); this.ctx.lineTo(w, this.waterSurfaceY); this.ctx.stroke(); for (let i = this.bubbles.length - 1; i >= 0; i--) { const b = this.bubbles[i]; b.update(this.waterSurfaceY); if (!b.isBursting) { // 绘制晶莹半透明气泡球体 this.ctx.beginPath(); this.ctx.arc(b.x, b.y, b.baseRadius, 0, 2 * Math.PI); this.ctx.fillStyle = 'rgba(56, 189, 248, 0.15)'; this.ctx.fill(); this.ctx.strokeStyle = 'rgba(255, 255, 255, 0.6)'; this.ctx.lineWidth = 1.2; this.ctx.stroke(); // 绘制右上角物理高光白点 (Specular Highlight) this.ctx.beginPath(); this.ctx.arc( b.x - b.baseRadius * 0.35, b.y - b.baseRadius * 0.35, b.baseRadius * 0.25, 0, 2 * Math.PI ); this.ctx.fillStyle = 'rgba(255, 255, 255, 0.85)'; this.ctx.fill(); } else { // 绘制破裂飞溅水滴 for (const d of b.burstDroplets) { this.ctx.beginPath(); this.ctx.arc(d.x, d.y, 1.5, 0, 2 * Math.PI); this.ctx.fillStyle = `rgba(56, 189, 248, ${d.alpha})`; this.ctx.fill(); } // 破裂完毕后在底部重生 if (b.burstDroplets.length === 0) { this.bubbles[i] = new FluidBubble( Math.random() * w, h + 40, Math.random() * 12 + 4 ); } } } requestAnimationFrame(this.renderLoop); }; }总结
气泡升腾的灵动美感,源于流体浮力与涡旋摆动在微观空间中自发涌现的自然秩序。将阿基米德浮力定律、斯托克斯阻力与表面张力破裂方程精确编织进 Canvas 渲染管线,我们在屏幕上为微交互动效赋予了如同深海气泡般纯净、剔透且充满生命张力的物理真实质感。