尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

SVG波浪背景动画实战:贝塞尔曲线与requestAnimationFrame高效实现

SVG波浪背景动画实战:贝塞尔曲线与requestAnimationFrame高效实现 简介一款基于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 viewBox0 0 800 600 xmlnshttp://www.w3.org/2000/svg preserveAspectRationone path dM0,300 C 100,180 300,420 400,300 C 500,180 700,420 800,300 L 800,600 L 0,600 Z fill#1d4ed8 opacity0.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/spath 数量2 条不建议超过 3 条层数越多视觉越丰富但移动端渲染成本上升2.3 两条 path 做出层次同频平移和相位差单层波浪看起来像一块塑料布在平动。真实海面的层次感来自不同深度的水波速度不同远处慢、近处快。常见做法是画两条相同波长的 path一条透明度高、振幅小一条不透明度低、振幅大svg width100% height100% viewBox0 0 800 600 preserveAspectRationone path iddeepWave dM0,320 C 100,200 300,440 400,320 C 500,200 700,440 800,320 L 800,600 L 0,600 Z fill#0f3a8c opacity0.35/path path idsurfaceWave dM0,350 C 100,230 300,470 400,350 C 500,230 700,470 800,350 L 800,600 L 0,600 Z fill#3b82f6 opacity0.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 langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 titleSVG 波浪翻滚背景/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 classwave-wrap aria-hiddentrue svg viewBox0 0 800 600 preserveAspectRationone xmlnshttp://www.w3.org/2000/svg !-- 深色远层速度慢 -- g idwaveFar classwave-group path dM0,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 opacity0.5/path /g !-- 浅色近层速度快 -- g idwaveNear classwave-group path dM0,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 opacity0.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/nearSpeedJS 变量两者比值控制在 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同样的nearSpeed1.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 classpage div classwave-bg aria-hiddentrue !-- 这里放前面写好的 svg -- /div main classpage-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 的视口宽高比和viewBox0 0 800 600不一致此时必须设置preserveAspectRatio来规定拉伸行为。三种常见配置我分别说明适用场景preserveAspectRationone完全拉伸波形会跟随容器比例变形。容器越矮、越宽原本浑圆的波峰被压得越扁适合海平面很低的极简设计但波形弧度不可控。preserveAspectRatioxMidYMax slice保持宽高比并裁切两侧超出部分波形弧度始终不变适合对波浪形态有严格要求的场景。代价是窄屏手机上左右两端会被切掉你要确保路径的完整波形集中在 viewBox 的中部。preserveAspectRatioxMidYMid meet完整显示整个 viewBox但在超宽屏时上下会出现留白背景层会露出容器底色。我的默认选择是xMidYMax slice配合height: 40vh底部对齐。如果你用的是preserveAspectRationone那么控制点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 不是整数倍关系。比如viewBox0 0 800 600被 CSS 拉伸到一个宽度为 801px 的容器每个用户单位都做了非整数缩放渲染器只能用抗锯齿算法硬扛边缘自然发虚。geometricPrecision是默认行为它会在缩放时尽量保留曲线精度代价是渲染耗时更高如果你在样式表里看到有人写了shape-rendering: crispEdges请立刻删掉——那会让所有曲线变成阶梯状折线。我的经验是容器宽度用百分比SVG 的viewBox保持整数然后在preserveAspectRatioxMidYMax 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-boxSVG 2 规范下 CSStransform的百分比基准与 HTML 元素不一样默认的transform-box是view-box换算的参考系是外部 SVG 的 viewBox 原点而不是元素自身的包围盒。这会导致一个诡异现象你对某个path写styletransform: 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变成了你能长期维护的工程组件。本文还有配套的精品资源点击获取
返回列表