二维波动方程)
在杭州西湖群山深处的九溪十八涧有一处名唤“烟树”的幽静潭水。清晨山谷空灵游人未至一潭碧水澄澈如镜倒映着四周青翠欲滴的重重山影与微茫晨雾。忽有一片微风吹落的青绿竹叶从树梢悄然飘落叶尖轻轻点破了如镜的水面。那一瞬间水面之上绽放出的物理律动令人屏息凝神以落叶触水的那一点为圆心一圈极其纤细、纯净的同心圆形水波纹Concentric Ripples徐徐向外推移扩散每一道波峰在阳光的穿透下泛起一道晶莹剔透的流光折射当波纹逐渐扩散至池塘边缘的花岗岩石壁时水波并未戛然而止而是顺着石壁优雅地反弹折射Wave Reflection与后续涌来的新波纹在水中央交汇激荡出一幅波光粼粼、明暗交织的空间波动干涉画卷Wave Interference Pattern。水波荡漾、聚散有时在短短数秒钟的能量衰减之后整潭清泉又重新归于至深至静的澄澈。多年之后当我在前端工程中为一套现代设计系统编写交互点击水波纹反馈Material Ripple Effect Surface Shimmer时我总会想起九溪十八涧那一汪被落叶点破的清泉。在很多粗糙的前端实现中所谓的水波纹往往充斥着严重的“塑料质感”开发者仅仅是在按钮内部插入了一个div用 CSS 执行transform: scale(0) ➔ scale(4)并淡出透明度这个圆圈在边界处生硬消失既没有波峰与波谷的高光折射也没有波浪相互交叠时的物理干涉与水面法线起伏这种动画就像一张被强行放大的死贴图充满了廉价与浮躁。将经典偏微分“二维离散波动方程2D Discrete Wave Equation”引入前端 Canvas 渲染管线我们就能用几十行纯粹的物理数学算法在用户指尖触碰屏幕的瞬间唤醒如九溪清泉般波光粼粼、真实泛波的顶级水纹微交互体验。二维物理波动方程的离散网格推导在连续介质力学中理想水面的垂直位移高度场 $u(x, y, t)$ 遵循经典二维双曲型偏微分波动方程$$\frac{\partial^2 u}{\partial t^2} c^2 \left( \frac{\partial^2 u}{\partial x^2} \frac{\partial^2 u}{\partial y^2} \right) c^2 \nabla^2 u$$其中 $c$ 为波在水介质中的传播相速度$\nabla^2$ 为拉普拉斯算子。为了在计算机离散像素网格Grid中以极高帧率实时求解我们采用中心有限差分法Finite Difference Method将时间与空间离散化[当前网格节点 (x, y) 的受力由上下左右 4 邻域决定] (x, y-1) │ │ (x-1, y) ───── (x, y) ───── (x1, y) │ │ (x, y1)设当前帧水面高度缓存为 $H_{current}$上一帧高度为 $H_{prev}$下一帧高度为 $H_{next}$。离散波动方程的递推代数解为$$H_{next}(x, y) \frac{H_{current}(x-1, y) H_{current}(x1, y) H_{current}(x, y-1) H_{current}(x, y1)}{2} - H_{prev}(x, y)$$最后引入水体粘滞能量耗散阻尼因子 $\alpha \approx 0.96$模拟水体能量衰减$$H_{next}(x, y) H_{next}(x, y) \cdot \alpha$$随后交换双缓冲区指针$H_{prev} \leftarrow H_{current}, \quad H_{current} \leftarrow H_{next}$纯 TypeScript 极速二维水波纹物理求解器// ripple-wave-solver.ts export class RippleWaveSimulation { private width: number; private height: number; private buffer1: Float32Array; private buffer2: Float32Array; private currentBuffer: Float32Array; private previousBuffer: Float32Array; public damping 0.965; // 水面阻尼系数 constructor(width: number, height: number) { this.width width; this.height height; const size width * height; this.buffer1 new Float32Array(size); this.buffer2 new Float32Array(size); this.currentBuffer this.buffer1; this.previousBuffer this.buffer2; } // 1. 用户点击/触碰水面注入能量 (如同竹叶点水) public dropAt(cx: number, cy: number, radius 4, strength 255) { const minX Math.max(1, cx - radius); const maxX Math.min(this.width - 2, cx radius); const minY Math.max(1, cy - radius); const maxY Math.min(this.height - 2, cy radius); for (let y minY; y maxY; y) { for (let x minX; x maxX; x) { if (Math.hypot(x - cx, y - cy) radius) { this.currentBuffer[y * this.width x] strength; } } } } // 2. 核心单帧波动方程离散积分步进 public step() { const w this.width; const h this.height; const curr this.currentBuffer; const prev this.previousBuffer; for (let y 1; y h - 1; y) { const rowOffset y * w; for (let x 1; x w - 1; x) { const idx rowOffset x; // 核心四邻域拉普拉斯平滑求平均 const wave ( curr[idx - 1] curr[idx 1] curr[idx - w] curr[idx w] ) * 0.5 - prev[idx]; // 应用水体粘滞阻尼衰减 prev[idx] wave * this.damping; } } // 交换双缓冲区 this.previousBuffer curr; this.currentBuffer prev; } public getHeightField(): Float32Array { return this.currentBuffer; } }Canvas 光影折射与水波渲染舞台// ripple-canvas-stage.ts export class RippleCanvasStage { private canvas: HTMLCanvasElement; private ctx: CanvasRenderingContext2D; private waveSim: RippleWaveSimulation; private simWidth 160; // 降采样网格计算保证极致性能 private simHeight 120; private imageData: ImageData; constructor(canvas: HTMLCanvasElement) { this.canvas canvas; this.ctx canvas.getContext(2d)!; this.waveSim new RippleWaveSimulation(this.simWidth, this.simHeight); this.imageData this.ctx.createImageData(this.simWidth, this.simHeight); // 监听鼠标触碰水面 this.canvas.addEventListener(pointerdown, (e) { const rect this.canvas.getBoundingClientRect(); const scaleX this.simWidth / rect.width; const scaleY this.simHeight / rect.height; const gx Math.floor((e.clientX - rect.left) * scaleX); const gy Math.floor((e.clientY - rect.top) * scaleY); this.waveSim.dropAt(gx, gy, 4, 320); }); } public renderLoop () { // 物理步进 this.waveSim.step(); const heights this.waveSim.getHeightField(); const pixels this.imageData.data; const w this.simWidth; const h this.simHeight; // 根据高度场梯度计算法线与光影折射 for (let y 1; y h - 1; y) { for (let x 1; x w - 1; x) { const idx y * w x; const pIdx idx * 4; // 计算 X 和 Y 方向的高度梯度 (斜率法线) const dx heights[idx 1] - heights[idx - 1]; const dy heights[idx w] - heights[idx - w]; // 模拟高光水面折射着色 const light Math.max(0, Math.min(255, 15 (dx dy) * 1.8)); pixels[pIdx] 15; // R (深渊幽蓝) pixels[pIdx 1] 23 light * 0.4; // G pixels[pIdx 2] 42 light * 0.8; // B (水波流光) pixels[pIdx 3] 255; // Alpha } } // 将低分辨率高度图双线性平滑放大渲染至高清 Canvas const tempCanvas document.createElement(canvas); tempCanvas.width this.simWidth; tempCanvas.height this.simHeight; tempCanvas.getContext(2d)!.putImageData(this.imageData, 0, 0); this.ctx.imageSmoothingEnabled true; this.ctx.drawImage(tempCanvas, 0, 0, this.canvas.width, this.canvas.height); requestAnimationFrame(this.renderLoop); }; }总结水波荡漾的至纯之美在于流体能量在时空网格中遵循波动微分方程自然传导的生生不息。看透二维波动方程离散差分的数学本质用纯算法在像素矩阵中模拟水滴入潭后的扩散、折射与衰减我们彻底告别了生硬的塑料缩放动效让用户的每一次点击都能在屏幕深处激荡出如九溪清泉般灵动、深邃且波光粼粼的东方写意诗篇。