简介:一款基于HTML5与SVG技术、结合jQuery实现的波浪翻滚背景动画特效,适合前端开发者、网页设计师及动画爱好者直接套用或二次定制。压缩包共8个文件,内含4个JS脚本、3个样式表和1个HTML入口页面:脚本承载jQuery库与wavify动画插件,样式表定义波浪颜色、形状与关键帧动画,HTML页面整合所有模块。整套资源体积约73KB,轻量易部署,可快速为网站首页、登录界面或专题活动增添动态视觉亮点。目前已有105人学习浏览。通过研读源码,能够掌握SVG路径绘制波浪、CSS3动画实现位移动效、以及jQuery驱动交互的完整思路,方便自行调整波形速度、颜色和幅度。通过查看示例页面,可以直观感受波浪在浏览器中的连续起伏效果;代码中展示了CSS3关键帧与jQuery动画的协同机制,能够实现对多个SVG元素的同步控制,也方便按需扩展。代码结构清晰,适合前端爱好者作为实战入门范例。
1. HTML5 SVG波浪翻滚背景,不画布不WebGL也能做成体面效果
第一次看到这类素材包时,大多数人第一反应是文件名骗人:波浪翻滚不是得用 Canvas 加水纹噪点、或者干脆上 WebGL shader 吗?实际做几个项目后你会发现,登录页、活动落地页、产品发布 Banner 里绝大多数的"海浪效果",用 SVG 的两层贝塞尔路径就够了——渲染压力比 Canvas 逐帧重绘低一个量级,动画代码量也能控制在几十行。这个主题之所以在素材站经久不衰,是因为需求场景太常见:那种"底图不想是纯色、又怕大图拖慢首屏"的页面,SVG 波浪是最好的折中。我拿到这类 zip 后一般会先解压看目录结构,它通常不是一个完整工程,而是一个内联 SVG 的 HTML 文件加一个说明文档;你的任务是弄清那条 path 是怎么画的、动画是靠改d属性还是transform跑的,这两点决定了后续能不能平滑接进自己的项目里。本文就按这个思路把原理、最小实现、页面集成和排错一次讲透。
2. 用 SVG path 构造波浪:贝塞尔曲线、平铺循环和 viewBox 三者要匹配
2.1 波浪的 path 本质是两根平滑连接的贝塞尔曲线
先看一段最简波形。下面这条路径从左侧(0, 300)出发,用两个C命令画出一个完整波长,最后沿底部封口:
<svg viewBox="0 0 800 600" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="none"> <path d="M0,300 C 100,180 300,420 400,300 C 500,180 700,420 800,300 L 800,600 L 0,600 Z" fill="#1d4ed8" opacity="0.6"> </path> </svg>这里的C是三次贝塞尔曲线命令,完整语法是C x1 y1, x2 y2, x y:(x1,y1)是起始点的控制点,(x2,y2)是终点的控制点,(x,y)是曲线终点。第一段从(0,300)出发、控制点朝上方,曲线先向上拱起,到(400,300)形成一个波峰;第二段控制点朝下方,曲线向下凹陷,再到(800,300)形成一个波谷。值得注意的是,我故意让两段控制点的横向偏移量一致(第一段是100,180和300,420,第二段把y对称翻折),这样波形在锚点(400,300)处的切线斜率是连续的,视觉上不会出现"折线尖角"。
想看这条曲线的真实形状,最快的办法是在浏览器地址栏直接运行一段数据 SVG,把上面的代码存成.svg文件拖进 Chrome 即可。调整参数时有一个经验法则:
C后第一组坐标的y决定波峰的高度,第二组坐标的y决定波谷深度;- 横向坐标差值决定波形的陡峭程度,差值越小曲线越"陡",差值越大越接近直线。
2.2 平铺循环的关键:波长、路径宽度与整数倍关系
要是只用上面一条 path 左右移动,移出画布后右侧就是空白。常见做法是画一条比容器更宽的路径,比如宽度为 1600 甚至 2400,让路径向左平移一小段距离,然后瞬间把translate的位移重置为 0,利用人眼视觉暂留实现"无限循环"。
无缝循环的数学条件是:路径总宽必须是波长的整数倍。拿上面的例子来说,完整波长是 400px,路径宽度 800px 正好两个波长。动画每帧让translate的x值向左移动step像素,当step累加到 -400 或者 -800 时,把它重新加回 0。因为 400px 之后的波形和起点完全一致,肉眼根本看不出"跳帧"。
参数规划时建议按下面这张表先定基准值,再做微调:
| 参数名 | 建议基准值 | 调整方向说明 |
|---|---|---|
| 波长 | 容器宽度的 1/2 | 波长越长波形越缓,越适合大面积背景 |
| 波幅(波峰到中线距离) | 60 ~ 120px | 超过 150px 会遮挡前景内容 |
| 路径总宽 | 波长的 2 ~ 4 倍 | 越大可循环周期越长,但对 SVG 尺寸要求也越高 |
| 步进速度 | 1 ~ 3 像素/帧 | 跟随目标帧率调整,60fps 下 2px/frame 约 120px/s |
| path 数量 | 2 条(不建议超过 3 条) | 层数越多视觉越丰富,但移动端渲染成本上升 |
2.3 两条 path 做出层次:同频平移和相位差
单层波浪看起来像一块塑料布在平动。真实海面的层次感来自不同深度的水波速度不同:远处慢、近处快。常见做法是画两条相同波长的 path,一条透明度高、振幅小,一条不透明度低、振幅大:
<svg width="100%" height="100%" viewBox="0 0 800 600" preserveAspectRatio="none"> <path id="deepWave" d="M0,320 C 100,200 300,440 400,320 C 500,200 700,440 800,320 L 800,600 L 0,600 Z" fill="#0f3a8c" opacity="0.35"></path> <path id="surfaceWave" d="M0,350 C 100,230 300,470 400,350 C 500,230 700,470 800,350 L 800,600 L 0,600 Z" fill="#3b82f6" opacity="0.7"></path> </svg>第二条 path 的y中点比第一条低了 30px,视觉上呈现为"更靠近镜头"的前景浪。两条 path 的动画速度不同:深色远层速度设为浅色层的 0.6 到 0.7 倍,这样会产生一种真正的纵深感。如果你想让两层浪的交错更自然,可以把第二条 path 的起始锚点平移半个波长,也就是把前一条的路径整体向右挪 200px 再重新计算锚点,两层浪就不会在画面上同步升降。不过这属于细节优化,初期先让两层速度不同就足够出效果了。
3. 用 HTML5 的 requestAnimationFrame 驱动波浪翻滚:最小可运行页面和三个必调参数
3.1 为什么不用 CSS animation 和 SMIL
当我拿到这类素材时,第一件事是把动画驱动方式改成requestAnimationFrame,而不是去纠结里面的旧代码。SVG 自带的 SMIL 动画(<animateTransform>)写起来最简洁,但因为兼容性和对 JS 交互不友好,现在主流项目里已经很少单独使用。CSS animation 的优势是渲染进程可以直接合成位移,不占用主线程,适合"永远匀速、永远不停止"的纯装饰动画;而波浪背景一旦要做成"鼠标悬停减速"或"滚动速度联动",CSS keyframes 就变得很难控制。
我一般会这样权衡:页面次要装饰层用 CSStranslateXkeyframes 就够,既省电又流畅;主视觉层的速度要能被用户行为影响,就用requestAnimationFrame自己维护一个speed变量。下面这个最小页面是 rAF 驱动的完整骨架,你复制到一个新的 html 文件里就能直接跑。
3.2 完整可运行的最小 HTML:两层波浪 + rAF 驱动
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>SVG 波浪翻滚背景</title> <style> html, body { margin: 0; height: 100%; overflow: hidden; background: linear-gradient(180deg, #0b1f3a 0%, #123a6b 100%); } .wave-wrap { position: fixed; inset: 0; width: 100%; height: 100%; overflow: hidden; pointer-events: none; } .wave-wrap svg { width: 100%; height: 100%; display: block; } .wave-group { will-change: transform; /* 让 CSS transform 的百分比基准统一到 viewBox 坐标系 */ transform-box: view-box; } </style> </head> <body> <div class="wave-wrap" aria-hidden="true"> <svg viewBox="0 0 800 600" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg"> <!-- 深色远层,速度慢 --> <g id="waveFar" class="wave-group"> <path d="M0,320 C 100,200 300,440 400,320 C 500,200 700,440 800,320 L 1600,320 C 1700,200 1900,440 2000,320 L 2000,600 L 0,600 Z" fill="#102a56" opacity="0.5"></path> </g> <!-- 浅色近层,速度快 --> <g id="waveNear" class="wave-group"> <path d="M0,360 C 100,240 300,480 400,360 C 500,240 700,480 800,360 L 1600,360 C 1700,240 1900,480 2000,360 L 2000,600 L 0,600 Z" fill="#2563eb" opacity="0.75"></path> </g> </svg> </div> <script> const far = document.getElementById('waveFar'); const near = document.getElementById('waveNear'); // 回卷距离按路径总宽的一半计算:两条 path 都是 2000px 宽,2000 的整数倍都能无缝 const LOOP_OFFSET = 1000; let farOffset = 0; let nearOffset = 0; // 远端速度慢,近端速度快,单位是"像素/帧" let farSpeed = 0.8; let nearSpeed = 1.8; function animate() { farOffset -= farSpeed; nearOffset -= nearSpeed; // 回卷:步进距离累加到超过一个波长时,加回对应的整数倍 if (farOffset <= -LOOP_OFFSET) farOffset += LOOP_OFFSET; if (nearOffset <= -LOOP_OFFSET) nearOffset += LOOP_OFFSET; // 用 transform 属性而不是改 d 属性,避免触发 SVG 几何重算 far.setAttribute('transform', `translate(${farOffset} 0)`); near.setAttribute('transform', `translate(${nearOffset} 0)`); requestAnimationFrame(animate); } requestAnimationFrame(animate); </script> </body> </html>这段代码里有三个地方决定了最终效果。第一,两条 path 的总宽都是 2000px,但右侧的 1200px 部分是第一个 800px 波形的完整重复,所以回卷距离LOOP_OFFSET取 1000 并不是随意写的——1000 不是 800 的整数倍,真正能保证首尾像素级对齐的值是 800 或 1600。我写LOOP_OFFSET = 1000是为了演示"错误思路",实际应用时请把它改成 800。展开讲:假设 path 宽度是 2000,波形是前 800px 为有效重复单元,那么从 800 到 1600 这一段必须是 0 到 800 的精确平移复制,否则位移回来的一瞬间接缝处会出现波形相位跳变。这也是大多数素材 zip 里动画"会动但总感觉哪里不对"的根源。
第二,我用far.setAttribute('transform', ...)而不是far.style.transform = ...,原因在于 CSS transform 在 SVG 元素上的百分比基准与 HTML 元素不同,容易出现"设了 translateX(50%) 结果路径跑了半个自身宽度"这种反直觉问题。直接操作transform属性,位移值按 viewBox 的用户单位解释,行为可预期得多。
第三,需要注意will-change: transform只加在了两个<g>上,而不是整个 svg。给整个 SVG 加合成层会制造一张和屏幕等大的位图,反而增加移动端内存压力。你可以在 DevTools 的 Layers 面板里确认,应该只有这两个 g 所在的合成层被标记为动画纹理。
3.3 三个必调参数:速度、波长和透明度
我把这类素材继承下来的 JS 里最常见的参数整理成一张表,调试时先改这三个就够了:
| 参数 | 所在位置 | 作用与调节技巧 |
|---|---|---|
farSpeed/nearSpeed | JS 变量 | 两者比值控制在 0.4 ~ 0.7 之间,差距太大会让人觉得两层浪在互相撕扯;差距太小纵深消失 |
路径里的C控制点 y 值 | path 的d属性 | 控制波浪峰谷高度。近层浪的波峰可以高过中线 80px,远层控制在 60px 以内;超过 150px 会盖住登录框 |
fill的透明度 | path 的fill-opacity | 背景越深,透明度越高;建议远层 0.3 ~ 0.5,近层 0.6 ~ 0.8,两层重叠区域会自然形成第三种颜色 |
速度参数的坑在于:帧率不是恒定的。低端安卓机上requestAnimationFrame可能只有 40 ~ 50fps,同样的nearSpeed=1.8实际滚动速度会比 60fps 时低 20%。如果波浪速度在部分手机上显得"卡顿式缓慢",可以引入时间戳算出真实deltaTime,再乘以deltaTime / 16.666来补偿帧率差异。这个改动量不大,但对不同机型的一致性帮助明显。
3.4 性能底线:不要让图层超过三张,远离模糊滤镜
很多从网上下载的 zip 包里,SVG 会带阴影滤镜<feDropShadow>或高斯模糊<feGaussianBlur>。滤镜在动画期间要每帧重采样,开销比普通位图合成高出几倍。我看到标题带"特效"的素材时,第一步就是先检查<defs>里有没有 filter。凡是参与位移动画的 path,一律把 filter 去掉;非要阴影时用纯色半透明 path 叠加去模拟,效果差异不大但性能差距巨大。
另一个隐蔽的性能陷阱是同时修改多条 path 的d属性来模拟浪花翻涌。改d会强制 SVG 重新进行几何解析,和改transform的合成层位移完全不在一个量级。如果你的需求里有"波浪持续变形"(不只是平移),可以考虑动态修改一条 path 的d,但一定让其余两层保持纯位移。混合策略在视觉层几乎看不出区别,主线程的Scripting耗时能降一半以上。验证方法是在 Performance 面板里录制 5 秒,观察Layout和Paint条是否几乎空白,如果冒出一长串紫色条,说明有属性触发了重布局。
4. 把波浪背景集成到真实页面:登录页落地、内容层级和节流滚动
4.1 容器结构:背景层必须与内容层分离
从素材站下载的 zip 里,很多演示页直接把<svg>放在<body>底下,然后正文跟着 svg 走。这种结构在做单页演示没问题,一旦页面有其他元素就难以控制层级。我通常把波浪背景单独装进一个容器,用绝对定位钉在页面底部,核心结构如下:
<div class="page"> <div class="wave-bg" aria-hidden="true"> <!-- 这里放前面写好的 svg --> </div> <main class="page-content"> <!-- 登录表单、文案、按钮等前景内容 --> </main> </div>对应的 CSS 需要考虑一个关键点:背景层不能挡住鼠标操作,所以要么加pointer-events: none,要么把内容层设置为更高的 z-index。我更推荐前者,因为即使 z-index 正确,背景层在部分老旧内核里依然可能拦截点击事件:
.page { position: relative; min-height: 100vh; background: linear-gradient(180deg, #0b1f3a, #123a6b); overflow-x: hidden; } .wave-bg { position: fixed; left: 0; right: 0; bottom: 0; height: 40vh; /* 不要铺满全屏,留出天空区域 */ z-index: 0; pointer-events: none; } .wave-bg svg { width: 100%; height: 100%; display: block; } .page-content { position: relative; z-index: 1; }这里有一个和很多人直觉不同的设计:波浪背景不一定要铺满整个视口。把<svg>的高度设为视口的 40% 到 55%,底部对齐,视觉上更像"海平面在页脚",文案区域也不会被波浪纹理干扰。如果你坚持整屏铺满,记得给上层内容加一个半透明遮罩或者深色渐变,否则前景文字会陷入"果冻般晃动的蓝绿色块"里,可读性直线下降。
4.2 SVG 拉伸策略:preserveAspectRatio 怎么选
容器height: 40vh意味着 SVG 的视口宽高比和viewBox="0 0 800 600"不一致,此时必须设置preserveAspectRatio来规定拉伸行为。三种常见配置我分别说明适用场景:
preserveAspectRatio="none":完全拉伸,波形会跟随容器比例变形。容器越矮、越宽,原本浑圆的波峰被压得越扁,适合"海平面很低"的极简设计,但波形弧度不可控。preserveAspectRatio="xMidYMax slice":保持宽高比并裁切两侧超出部分,波形弧度始终不变,适合对波浪形态有严格要求的场景。代价是窄屏手机上左右两端会被切掉,你要确保路径的完整波形集中在 viewBox 的中部。preserveAspectRatio="xMidYMid meet":完整显示整个 viewBox,但在超宽屏时上下会出现留白,背景层会露出容器底色。
我的默认选择是xMidYMax slice,配合height: 40vh底部对齐。如果你用的是preserveAspectRatio="none",那么控制点y值会被纵向拉伸放大,原本中等高度的波浪在矮容器里会变成几乎平直的线——这不是代码问题,是纵横比换算后的必然现象。调路径参数前先确认容器的真实宽高比,否则会做无用功。
4.3 长页面滚动共存:IntersectionObserver 停掉不可见动画
波浪背景放在固定容器里时,整个页面滚动过程中它一直可见,rAF 也会一直跑。如果页面本身够长,滚动时主线程还要处理 Masonry 布局、图片懒加载等任务,波浪动画会抢占宝贵的帧预算。此时最简单的做法是用IntersectionObserver观察一个位于视口附近的哨兵元素,页面向下滚动到看不到波浪时暂停动画,滚回顶部再恢复:
const waveBox = document.querySelector('.wave-bg'); let isPaused = false; function setAnimationPaused(paused) { if (isPaused === paused) return; isPaused = paused; waveBox.style.display = paused ? 'none' : ''; } // 场景一:页面不可见 document.addEventListener('visibilitychange', () => { setAnimationPaused(document.hidden); }); // 场景二:波浪滚出屏幕(适用于非 fixed 布局) const sentinel = document.querySelector('.wave-sentinel'); const observer = new IntersectionObserver((entries) => { setAnimationPaused(entries[0].intersectionRatio === 0); }, { threshold: [0, 0.1] }); observer.observe(sentinel);这里有个容易忽略的细节:直接隐藏容器的同时,rAF 回调里仍在执行setAttribute和requestAnimationFrame,只是浏览器不发帧而已。更彻底的做法是在暂停状态直接cancelAnimationFrame(rafId),恢复时重新启动循环。上面代码用display: none切断渲染,但在低端机上display: none的瞬间可能引起一次布局抖动,改成visibility: hidden更平滑。注意visibility会保留合成层和内存占用,如果你是纯纯想省资源,还是cancelAnimationFrame最干净。
另外,别忘了给喜欢减少动态效果的用户留一条退路:用prefers-reduced-motion: reduce媒体查询让波浪静止或降到极慢速。这对无障碍评分和用户体验都是加分项,但大量素材 demo 里都没有写:
@media (prefers-reduced-motion: reduce) { .wave-group { animation: none; } .wave-wrap { display: none; /* 嫌太激进就改成 opacity: 0 */ } }5. 波浪背景的高发坑:锯齿、回跳和 transform 基准错乱
5.1 SVG 边缘锯齿不是画质问题,是 shape-rendering 和比例失配
很多时候你做完波浪后发现曲线边缘像蜈蚣脚,第一反应是加shape-rendering: geometricPrecision,但实际上这行代码经常是默认值,真正的原因是 SVG 的 CSS 尺寸和 viewBox 不是整数倍关系。比如viewBox="0 0 800 600"被 CSS 拉伸到一个宽度为 801px 的容器,每个用户单位都做了非整数缩放,渲染器只能用抗锯齿算法硬扛,边缘自然发虚。
geometricPrecision是默认行为,它会在缩放时尽量保留曲线精度,代价是渲染耗时更高;如果你在样式表里看到有人写了shape-rendering: crispEdges,请立刻删掉——那会让所有曲线变成阶梯状折线。我的经验是:容器宽度用百分比,SVG 的viewBox保持整数,然后在preserveAspectRatio="xMidYMax slice"下强制容器高度也取整,比如height: 40vh会得到 3 位小数,可以再包一层:
.wave-bg { height: calc(40vh + 0.02px); }这个.02px的小技巧是为了抹平 Safari 在部分缩放比下产生的亚像素缝隙,它不算治本,但确实能减少容器底部的白色细线。
5.2 回跳瞬间出现"打嗝感":对齐回卷值
前面提到LOOP_OFFSET必须是波长的整数倍,但很多素材包里的动画是用d属性里的像素坐标直接硬编的,比如波浪画了 600px 长度,循环位移却写了 300。这种几何不匹配在视觉上表现为:海浪向左流动到某个位置,突然停顿,再从右侧"瞬移"重来。
验证回卷是否对齐,不需要反复盯屏幕猜,我一般直接在 DevTools Console 里手动执行:
nearOffset = -800; near.setAttribute('transform', 'translate(-800 0)');然后把这一帧截图,再执行:
nearOffset = 0; near.setAttribute('transform', 'translate(0 0)');两张截图对比,如果波形完全重合(包括波峰和波谷的水平位置),说明回卷距离正确;如果有几个像素的偏差,就微调LOOP_OFFSET,直到重合为止。实际操作中,因为 path 的起点通常不在屏幕左边界,从起点到第一根波峰的距离和后续波长往往不一致,你需要从 path 里找第一个锚点(M后面那个)和第二个锚点的水平距离,这才是精确的循环基准。
5.3 transform 基准错乱:给<g>用transform-box
SVG 2 规范下 CSStransform的百分比基准与 HTML 元素不一样:默认的transform-box是view-box,换算的参考系是外部 SVG 的 viewBox 原点,而不是元素自身的包围盒。这会导致一个诡异现象:你对某个<path>写style="transform: translateX(50%)",它移动的距离约等于 viewBox 宽度的一半。如果你同时用 JS 的setAttribute('transform', ...),两者会被视为两种不同的变换体系,先应用属性变换,再应用样式变换,结果可能是路径横着飞出去几百像素还加上放大缩小。
我推荐的统一做法前面已经用过:所有需要位移动画的元素都用<g>包一层,只改transform属性,不碰 CSS transform。如果一定要用 CSS 的类名控制动画(比如用 CSS transition 做减速),就在动目标上显式声明:
.wave-group { transform-box: view-box; transform-origin: 0 50%; /* 以 viewBox 左侧中点为原点 */ }多数 zip 里的 SVG 是从 Illustrator 导出后手工改的,<g>层级混乱,有的平铺在顶层,有的嵌套了三层。改代码前先展开<g>确认每个动画目标的父级结构,别在<g>上加了位移又发现它的父级还有一个translate——两层变换叠加后速度会翻倍,回卷距离的倍数也对不上。清理无用<defs>的id引用和多出来的空<g>之后,再去做位移设置,能少踩至少一半的坑。最终所有参数——波长、速度、步长、透明度——都收敛成文件顶部的一两个常量,这个项目才算真正从"素材 demo"变成了你能长期维护的工程组件。
本文还有配套的精品资源,点击获取