☰
手绘质感波浪与水流:SVG 贝塞尔曲线动态形变与路径插值动画实战
2026/10/5 5:50:44 网站建设 项目流程

手绘质感波浪与水流:SVG 贝塞尔曲线动态形变与路径插值动画实战

在设计手账页面的底部装饰、卡片分割线,或是营造秋天溪流、落叶漂流的静谧意境时,我们常常需要用到「波浪」与「水流」这一核心视觉元素。

但在常规的网页开发中,绝大多数的波浪效果都极其生硬:

  • 找一张现成的波浪 SVG 切图,设置width: 200%,然后用 CSS 写一个死板的translateX(-50%)无限循环位移;
  • 这种所谓的「水流」,一眼就能看出是一条机械传送带在眼前水平横移,波峰和波谷的形状永远固定不变,完全失去了真实液体那种自由形变、起伏张弛的灵气。

在真实的水彩画或自然流水中,波浪的生命力来自于形态本身的动态呼吸(Morphing):

  • 随着时间的推移,波峰的高度(振幅)在不断发生微弱的伸缩;
  • 波浪在前进的同时,曲线的曲率半径在柔和地呼吸形变;
  • 多层水流之间存在相位差,相互交叠出透光的水彩层叠感。

要实现这种真正有生命力的手绘波浪,我们不能仅靠外层的平移,而必须直接对SVG<path>内部的贝塞尔曲线控制点(Bézier Control Points)执行动态数学插值计算。

今天这篇文章,我将带大家用纯原生 JavaScript 与 SVG 路径语法,手写一个高性能的手绘质感动态波浪生成引擎。


一、贝塞尔波浪的数学骨架:二次贝塞尔平滑流

在 SVG 路径语言(Path Data)中,构造一条连续起伏的平滑波浪,最高效的指令组合是Q(二次贝塞尔曲线)配合T(平滑延续贝塞尔曲线):

  • Q x1 y1, x y:从当前起点出发,以 $(x1, y1)$ 为控制点,画一条平滑曲线到达终点 $(x, y)$;
  • T x y:自动以上一个控制点相对终点的对称点作为新控制点,画出下一个相反方向的波峰或波谷,确保在拐点处斜率完全连续,没有任何断裂或尖角。

一条包含一个完整波峰和一个完整波谷的基础正弦波段,可以抽象为:
$$y(x, t) = A(t) \cdot \sin(\omega x + \phi(t)) + \text{BaseY}$$

其中,振幅 $A(t)$ 与相位 $\phi(t)$ 随着时间连续微幅振荡,驱动贝塞尔控制点在二维平面上不断呼吸形变。

+-------------------------------------------------------------+ | 贝塞尔动态波浪控制点模型 | +-------------------------------------------------------------+ | 控制点 P1 (向上抬升) | | • | | / \ | | 起点 / \ 拐点 (斜率平滑连续) | | (0, Y) • | | \ / | | \ / | | • | | 控制点 P2 (向下深陷) | +-------------------------------------------------------------+

二、代码实战:纯前端 SVG 动态波浪流渲染引擎

我们不使用任何重型动画库,使用原生requestAnimationFrame直接对 SVG 路径字符串执行毫秒级计算更新:

<!-- wave-stage.html --> <div class="autumn-stream-card"> <svg class="stream-svg" viewBox="0 0 800 240" preserveAspectRatio="none"> <!-- 后层水流:较深、较慢的秋日暖茶色波浪 --> <path id="backWave" fill="rgba(212, 163, 115, 0.35)" /> <!-- 前层水流:清澈、温润的落叶金浅层水波 --> <path id="frontWave" fill="rgba(233, 180, 76, 0.45)" /> </svg> </div>
// cozy-wave-engine.js:基于贝塞尔控制点形变的波浪引擎 export class CozyWaveEngine { constructor() { this.frontPath = document.querySelector('#frontWave'); this.backPath = document.querySelector('#backWave'); this.width = 800; this.height = 240; this.baseY = 120; // 水位基础高度 this.step = 0; this.isRunning = false; } start() { this.isRunning = true; this.renderLoop(); } stop() { this.isRunning = false; } renderLoop() { if (!this.isRunning) return; // 时间步长推进 this.step += 0.025; // 1. 计算前层水波的动态形变路径 const frontD = this.generateWavePath({ amplitude: 24, // 基础波幅 amplitudeDrift: 6, // 波幅动态伸缩呼吸量 speed: this.step, segments: 4 // 4段连续波峰波谷 }); // 2. 计算后层水波(具有不同的相位与速度,营造景深错落感) const backD = this.generateWavePath({ amplitude: 18, amplitudeDrift: 4, speed: this.step * 0.65 + 2.0, // 较慢速度与初始相位差 segments: 3 }); // 直接注入 SVG path 属性 if (this.frontPath) this.frontPath.setAttribute('d', frontD); if (this.backPath) this.backPath.setAttribute('d', backD); requestAnimationFrame(() => this.renderLoop()); } // 核心:基于二次贝塞尔曲线动态拼装闭合路径 generateWavePath(config) { const { amplitude, amplitudeDrift, speed, segments } = config; const currentAmp = amplitude + Math.sin(speed * 0.8) * amplitudeDrift; const segmentWidth = this.width / segments; let pathD = `M 0,${this.baseY}`; // 循环构造平滑起伏的二次贝塞尔曲线段 for (let i = 0; i < segments; i++) { const startX = i * segmentWidth; const endX = (i + 1) * segmentWidth; const midX = (startX + endX) / 2; // 控制点垂直坐标随正弦函数上下柔和浮动 const waveDirection = i % 2 === 0 ? 1 : -1; const controlY = this.baseY + Math.sin(speed + i * 1.5) * currentAmp * waveDirection; // 使用 Q 命令生成平滑贝塞尔拱起 pathD += ` Q ${midX},${controlY.toFixed(1)} ${endX},${this.baseY}`; } // 将路径封闭到底部形成完整着色面 pathD += ` L ${this.width},${this.height} L 0,${this.height} Z`; return pathD; } }
/* wave-stage.css:波浪容器与手账融合 */ .autumn-stream-card { position: relative; width: 100%; max-width: 680px; height: 180px; background: #FAF7F0; /* 纸浆底色 */ border-radius: 12px; overflow: hidden; border: 1.5px solid #E8DFD1; box-shadow: 0 8px 24px rgba(74, 62, 61, 0.05); } .stream-svg { position: absolute; bottom: 0; left: 0; width: 100%; height: 100%; }

三、视觉进阶:水彩混色与和纸纹理的透叠

上面的代码实现了真正的流体形变。为了让它具备深秋手账的艺术水彩质感,我们需要在 CSS 中加入混合模式(Blend Mode):

.stream-svg path { /* 核心:正片叠底混合,两层水波交叠处会自然加深,宛如透明水彩重叠晕染 */ mix-blend-mode: multiply; transition: opacity 0.3s ease; }

双层水波以不同的速度和相位徐徐起伏,在相交重叠的区域,颜色自然演变为沉稳的深琥珀色,分开的区域则恢复为轻柔通透的浅金色,整个画面宛如阳光洒在清浅的小溪里,灵动而治愈。


四、工程避坑与性能调优

  1. 避免拼接过多的小段多边形:有些开发者误以为点越多越真,把一段波浪切成几百个点,导致每帧生成几千字符的字符串传递给 DOM,这会引起严重的垃圾回收(GC)内存抖动。在贝塞尔曲线中,利用极佳的数学切线特性,整幅画面只需要4 到 5 个控制点就能呈现出极致圆润的流体弧度,CPU 占用率不到 1%。
  2. 移动端后台定时休眠:在页面处于不可见状态时(document.visibilityState === 'hidden'),务必调用engine.stop()停止 RAF 循环,页面重新回到前台时再调用engine.start(),把宝贵的电池与 GPU 资源百分之百节省下来。

五、写在最后

在冰冷的几何世界里,直线代表着人造的效率与规则,而曲线则代表着大自然的包容与生命。

当一段代码能让屏幕底部的金色水波如同一幅慢慢洇开的水彩画一样自由起伏、温润流淌时,技术就完成了从功能工具向审美意象的优雅跨越。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询