1. 为什么我劝你别再用SVG和Canvas做波浪了
前阵子帮一个做数据可视化的朋友改大屏项目,他那个页面顶部有一条流动的波浪装饰带,用的是Canvas逐帧重绘,跑在低配一体机上直接掉到20帧,风扇呼呼转。我看了下代码,光那条波浪就占了主线程将近40%的计算时间。后来我花了不到二十分钟,用纯CSS把同样的效果重写了一遍,帧率稳在60,CPU占用几乎可以忽略。这件事让我意识到,很多前端开发者对CSS的动画能力还是低估了,尤其是涉及连续形变、循环流动这类效果时,第一反应总是去搬Canvas或者引一个动画库,结果把简单问题复杂化了。
纯CSS实现动态波浪动画这个主题,说白了就是利用CSS的渐变、伪元素、关键帧和变换属性,在不依赖任何JavaScript和外部资源的前提下,做出视觉上连续流动的波浪效果。它能用在哪里?页面顶部装饰条、加载动画、卡片背景、按钮悬停反馈、数据大屏的边框动效,甚至可以用在邮件模板里——因为邮件客户端对JavaScript的支持极差,但CSS动画的兼容性反而好得多。适合谁看?有基础CSS知识、想做动效但不想引入重型依赖的前端开发者,以及需要给静态页面增加一点“呼吸感”的UI设计师。
我下面要讲的,不是那种复制粘贴就完事的代码片段合集,而是把波浪动画拆成几个可组合的模块,讲清楚每个参数为什么这么设、换一种写法会有什么后果、在哪些浏览器上会翻车。你跟着走一遍,以后遇到类似的连续形变需求,脑子里能直接映射出对应的CSS方案,而不是条件反射地去开Canvas。
2. 波浪动画的底层逻辑:先想清楚“动”的是什么
2.1 波浪的本质是重复的曲线,不是一条线在跑
很多人第一次做波浪,思路是“画一条曲线,让它横向移动”。这个思路本身没错,但实现起来容易走偏。如果你真的去用border-radius拼一条曲线然后平移,会发现曲线两端会露出破绽,而且移动过程中形状是刚性的,看起来像一根绳子在滑动,没有波浪那种“起伏在传递”的感觉。
正确的思路是:波浪是多个相同波形在水平方向上无缝拼接,通过整体平移实现视觉上的流动。也就是说,你看到的“波浪在动”,其实是波形本身没变,只是整个图案在横向位移。这跟跑步机的原理一样——人没动,履带在动,但看起来像是人在往前跑。
理解这一点之后,实现路径就清晰了:先造出一个可重复的波形单元,然后让它横向循环平移。CSS里能造波形的手段有几个,我逐个说下取舍。
2.2 三种造波手段的对比:渐变、圆角、裁剪
第一种是径向渐变。用radial-gradient画圆,然后只取圆的一部分露出来,通过多个圆错位排列形成波浪。这种方式的优点是波形圆润自然,缺点是控制精度差,想要调整波峰高度和波长需要反复试参数,而且多个渐变叠加时性能开销会上升。
第二种是border-radius拼圆。用两个半圆或者椭圆错位叠在一起,形成波峰波谷。这种方式直观,但同样存在调整困难的问题,而且当波长变化时,圆角值需要重新计算,不够灵活。
第三种是CSS裁剪配合重复渐变。这是我最推荐的方式,也是后面实战部分主要采用的方案。核心思路是用repeating-linear-gradient或者repeating-radial-gradient生成一个周期性的图案,然后用clip-path或者mask裁出波浪形状。这种方式的好处是波长、波高、相位都可以通过参数精确控制,而且改一个变量就能整体调整。
我实测下来,第三种方案在Chrome、Firefox、Safari上的表现最一致,性能也最好。下面重点讲这种。
2.3 为什么不用SVG:兼容性和性能的双重考量
有人会问,SVG的path画波浪不是更直接吗?确实,SVG在描述复杂路径上更直观,但有两个问题。一是SVG动画如果走SMIL,兼容性已经不被推荐了;如果走CSS动画操作d属性,支持度又参差不齐。二是SVG作为独立元素嵌入时,在某些邮件客户端和低版本WebView里渲染会出问题。
纯CSS方案的优势在于,它最终落到的是浏览器的合成层,动画走的是GPU加速路径,不触发重排重绘。只要你不去动画化width、height、top、left这些几何属性,性能就不会差。我后面会具体讲哪些属性可以放心动画化,哪些是雷区。
3. 核心参数拆解:波长、波高、相位、速度
3.1 波长和波高怎么用CSS变量控制
先定义一个波浪单元。假设我们要做一个波长200px、波高40px的波浪,用repeating-radial-gradient来生成。这里的关键是理解渐变尺寸和波浪尺寸的换算关系。
一个完整的波浪周期,在视觉上包含一个波峰和一个波谷。用径向渐变模拟时,通常用两个圆错位来实现。但更简洁的做法是用radial-gradient配合background-size来定义周期。
我习惯把参数抽成CSS自定义属性:
.wave { --wave-length: 200px; --wave-height: 40px; --wave-color: #4a90d9; --wave-speed: 3s; }--wave-length控制一个完整波峰波谷的水平跨度,--wave-height控制波峰到波谷的垂直距离。这两个值决定了波浪的“胖瘦”和“陡缓”。波长越短、波高越大,波浪看起来越急促;波长越长、波高越小,越平缓。
这里有个经验值:波高一般取波长的15%到25%之间,视觉上比较舒服。比如波长200px,波高取30到50px都合理。如果波高超过波长的40%,波浪会显得很尖锐,像锯齿;如果低于10%,又太平,看不出起伏。
3.2 相位偏移:让波浪看起来是“流”而不是“跳”
相位这个概念在CSS里不像在信号处理里那么直观,但你可以理解为“波形起始点的水平偏移量”。如果两个波浪单元的相位差刚好等于半个波长,那么一个的波峰对着另一个的波谷,叠在一起就是一条直线。如果相位差是四分之一波长,叠在一起会形成更复杂的干涉图案。
在纯CSS波浪里,相位主要用来做两件事:一是让多层波浪错开,产生层次感;二是配合动画关键帧,让平移看起来是连续的。
举个例子,如果你只做一层波浪,从translateX(0)动画到translateX(-200px),然后瞬间跳回translateX(0),会看到明显的跳跃。解决办法是让波浪的宽度是波长的整数倍,并且动画的起点和终点相差整数个波长。这样当动画循环时,视觉上是无缝的。
@keyframes wave-flow { from { transform: translateX(0); } to { transform: translateX(calc(var(--wave-length) * -2)); } }这里移动两个波长,是因为波浪容器本身宽度覆盖了至少两个周期,移动两个波长后画面和初始状态完全一致,循环时看不出接缝。
3.3 动画速度与缓动函数的选择
速度用animation-duration控制,这个没什么好说的。重点说缓动函数。波浪流动应该是匀速的,所以用linear。如果你用了ease-in-out,波浪会一会儿快一会儿慢,看起来像在喘气,非常不自然。
但有一种情况例外:如果你做的是“波浪涌入”效果,比如页面加载时波浪从底部涌上来然后停住,那可以用ease-out让涌入过程有减速感。但纯粹的循环流动,必须用linear。
速度值怎么定?我一般让波浪在3到6秒内移动一个完整周期。太快了像水流湍急,太慢了像糖浆。如果是背景装饰,4秒左右比较适中。如果是加载动画,可以快到2秒,传递“正在处理”的感觉。
4. 从零手写一个可复用的波浪组件
4.1 HTML结构:越简单越好
纯CSS波浪的HTML结构可以极简,一个容器加一个伪元素就够了:
<div class="wave-container"></div>所有视觉效果都通过.wave-container的::before和::after伪元素来实现。这样做的好处是不增加额外的DOM节点,对性能友好,而且组件化时只需要控制一个类名。
如果你需要多层波浪叠加,可以用两个伪元素分别做一层,或者在外面再包一层容器。我倾向于用伪元素,因为伪元素不参与文档流,不会影响布局。
4.2 用repeating-radial-gradient生成波形
这是核心步骤。先看代码:
.wave-container { position: relative; width: 100%; height: 120px; overflow: hidden; background: #f0f6ff; } .wave-container::before { content: ""; position: absolute; bottom: 0; left: 0; width: 200%; height: var(--wave-height, 40px); background: repeating-radial-gradient( circle at 50% 100%, transparent 0, transparent var(--wave-length, 200px), var(--wave-color, #4a90d9) var(--wave-length, 200px), var(--wave-color, #4a90d9) calc(var(--wave-length, 200px) * 2) ); animation: wave-flow var(--wave-speed, 4s) linear infinite; }这段代码的逻辑是:在容器底部创建一个高度为波高的条带,条带内用重复径向渐变画出周期性的圆弧。circle at 50% 100%表示圆心在条带底部中央,这样渐变出来的圆弧向上凸起,形成波峰。
但这里有个问题:repeating-radial-gradient画出来的是同心圆环,直接用在矩形条带上,边缘会被裁切,形成的是半圆形的波峰,波谷是平的。这其实已经是一种波浪了,只是形状比较“胖”。如果你想要更接近正弦波的形状,需要调整渐变的停止点,让圆弧的过渡更平滑。
我试过的一个改进方案是用两个径向渐变叠加,一个负责波峰,一个负责波谷,通过background-position错位。但这样代码复杂度上升,而且调整起来不直观。对于大多数装饰性场景,单层径向渐变已经够用了。
4.3 用clip-path裁出精确波形
如果你对波形精度有要求,比如要做数据可视化里的水位图,那clip-path是更好的选择。思路是:先画一个矩形,然后用clip-path裁出波浪形状。
.wave-container::after { content: ""; position: absolute; bottom: 0; left: 0; width: 200%; height: 100%; background: var(--wave-color, #4a90d9); clip-path: polygon( 0% 60%, 10% 50%, 20% 40%, 30% 50%, 40% 60%, 50% 70%, 60% 60%, 70% 50%, 80% 40%, 90% 50%, 100% 60%, 100% 100%, 0% 100% ); animation: wave-flow var(--wave-speed, 4s) linear infinite; }clip-path的polygon用一系列点来定义波浪的上边缘。点的数量越多,波形越平滑。但点太多会导致CSS体积膨胀,而且动画时浏览器需要重新计算裁剪路径,性能会下降。
我的经验是:对于宽度200px的波浪单元,用8到12个点就够了。再多的话,视觉提升不明显,但性能开销线性增长。而且clip-path动画在低端安卓机上可能会有锯齿,因为裁剪边缘的抗锯齿处理不如渐变。
4.4 动画关键帧与无缝循环的数学关系
无缝循环的条件是:动画位移量必须是波长的整数倍。假设波浪单元宽度是W,波长是L,那么W必须是L的整数倍。动画从translateX(0)到translateX(-nL),其中n是正整数。
为什么?因为当位移了nL之后,图案的每一个点都移动到了原来另一个同相位点的位置,整个画面和初始状态完全重合。如果位移量不是波长的整数倍,循环时就会看到跳变。
@keyframes wave-flow { from { transform: translateX(0); } to { transform: translateX(calc(var(--wave-length) * -2)); } }这里移动两个波长,是因为波浪容器宽度设为200%,覆盖了两个周期。移动两个波长后,画面完全重合。如果你把容器宽度设为300%,移动三个波长也可以,但没必要,因为超出视口的部分用户看不到。
5. 多层波浪叠加:做出有纵深感的动态水面
5.1 用伪元素分层,控制透明度和速度差
单层波浪看起来比较单薄,像一条纸带。要做出水面的纵深感,需要叠两到三层波浪,每层的颜色、透明度、速度、相位都不同。
.wave-container::before { /* 后层波浪 */ background: rgba(74, 144, 217, 0.3); animation-duration: 6s; transform: translateY(-10px); } .wave-container::after { /* 前层波浪 */ background: rgba(74, 144, 217, 0.6); animation-duration: 4s; transform: translateY(0); }后层波浪颜色浅、速度慢、位置偏高,前层波浪颜色深、速度快、位置偏低。这样视觉上后层像是远处的波浪,前层像是近处的波浪,形成空间层次。
速度差很关键。如果两层速度一样,看起来就是一块板在平移。速度差在1.5到2倍之间比较自然。后层慢、前层快,符合近大远小的透视直觉。
5.2 颜色搭配:从单色到渐变波浪
单色波浪做装饰够用,但如果要融入品牌色或者配合页面主题,渐变波浪会更出彩。CSS的linear-gradient可以作为background叠在波浪上,但要注意渐变的方向和波浪的流动方向要协调。
.wave-container::after { background: linear-gradient( 90deg, #4a90d9 0%, #6bb3f0 50%, #4a90d9 100% ); background-size: 200% 100%; animation: wave-flow 4s linear infinite, gradient-shift 8s linear infinite; } @keyframes gradient-shift { from { background-position: 0% 0%; } to { background-position: 200% 0%; } }这里同时动画化了transform和background-position。注意,background-position的动画会触发重绘,性能不如纯transform。如果页面性能吃紧,建议只保留transform动画,渐变用静态的。
我实测下来,在中等配置的笔记本上,两层波浪加渐变背景,CPU占用在5%以下,可以接受。但在低端手机上,建议砍掉渐变动画,只保留位移。
5.3 速度差与相位差的组合效果
相位差通过animation-delay的负值来实现。比如让第二层波浪比第一层提前半个周期:
.wave-container::after { animation-delay: calc(var(--wave-speed) / -2); }负的animation-delay表示动画从中间某个状态开始,而不是从头开始。这样两层波浪的波峰不会同时到达同一个位置,视觉上更自然。
速度差和相位差可以组合出很多变化。我常用的一个配方是:后层速度6s、延迟-3s,前层速度4s、延迟-1s。这样两层波浪的波峰交错出现,像真实水面一样有随机感。
6. 性能优化与兼容性避坑指南
6.1 哪些CSS属性可以放心动画化
在波浪动画里,只有transform和opacity是可以放心动画化的,因为它们可以由GPU合成器独立处理,不触发重排和重绘。translateX、translateY、scale、rotate都属于transform的范畴,性能最好。
background-position的动画会触发重绘,因为浏览器需要重新计算背景图案的绘制位置。在简单场景下问题不大,但如果波浪层数多、页面元素复杂,就可能引起掉帧。
clip-path的动画性能介于两者之间。现代浏览器对clip-path的动画有优化,但不如transform彻底。如果要用clip-path做波浪,建议把动画加在transform上,而不是直接动画化clip-path的路径点。
绝对不要动画化width、height、top、left、margin这些几何属性,它们会触发重排,性能极差。
6.2 移动端低端机的降级策略
低端安卓机对复杂CSS动画的支持有限,尤其是多层叠加加渐变的情况。我的降级策略是:
第一,用@media (prefers-reduced-motion: reduce)检测用户是否开启了减少动画偏好,如果开启了,直接停掉动画,只显示静态波浪。
@media (prefers-reduced-motion: reduce) { .wave-container::before, .wave-container::after { animation: none; } }第二,用@supports检测浏览器是否支持clip-path和repeating-radial-gradient,不支持的话回退到简单的背景色块。
@supports not (clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%)) { .wave-container::after { clip-path: none; height: 20px; border-radius: 50% 50% 0 0; } }第三,在低端机上减少波浪层数。可以通过JavaScript检测设备性能,但纯CSS方案里,我倾向于用媒体查询根据屏幕宽度做粗略判断。小屏幕设备通常性能也弱一些,可以只保留一层波浪。
6.3 浏览器兼容性速查表
| 特性 | Chrome | Firefox | Safari | Edge | iOS Safari | Android WebView |
|---|---|---|---|---|---|---|
| repeating-radial-gradient | 支持 | 支持 | 支持 | 支持 | 支持 | 支持 |
| clip-path polygon | 支持 | 支持 | 支持 | 支持 | 支持 | 支持 |
| CSS自定义属性 | 支持 | 支持 | 支持 | 支持 | 支持 | 支持 |
| transform动画 | 支持 | 支持 | 支持 | 支持 | 支持 | 支持 |
| background-position动画 | 支持 | 支持 | 支持 | 支持 | 支持 | 支持 |
| prefers-reduced-motion | 支持 | 支持 | 支持 | 支持 | 支持 | 支持 |
从表里可以看出,核心特性在现代浏览器上都没有问题。唯一需要注意的是,某些老版本的Android WebView对clip-path的动画支持不完整,可能会出现裁剪边缘闪烁。如果目标用户里有大量老设备,建议用repeating-radial-gradient方案而不是clip-path方案。
7. 实战案例:给数据大屏做一个动态水位波浪
7.1 场景描述与效果目标
假设你要做一个数据大屏,中间有一个圆形的水位指示器,水位线是动态波浪。这个场景比单纯的装饰波浪要求更高:波浪要在圆形内部显示,水位高度要能通过CSS变量控制,波浪要持续流动。
这个需求用纯CSS完全可以实现,而且比Canvas方案更轻量。核心思路是用一个圆形容器,内部放一个矩形波浪层,波浪层的高度由水位变量控制,波浪本身用repeating-radial-gradient生成。
7.2 圆形裁剪与水位的动态控制
.water-level { position: relative; width: 200px; height: 200px; border-radius: 50%; overflow: hidden; background: #e8f4ff; border: 4px solid #4a90d9; } .water-level::before { content: ""; position: absolute; left: -50%; bottom: 0; width: 200%; height: var(--water-level, 60%); background: repeating-radial-gradient( circle at 50% 0%, transparent 0, transparent 30px, rgba(74, 144, 217, 0.7) 30px, rgba(74, 144, 217, 0.7) 60px ); animation: water-flow 3s linear infinite; } @keyframes water-flow { from { transform: translateX(0); } to { transform: translateX(-60px); } }这里波浪的波长是60px,所以动画移动60px就是一个完整周期。--water-level控制水位高度,改这个变量就能动态调整水位,配合CSS过渡还能做出水位上升的动画。
注意circle at 50% 0%,圆心在波浪层的顶部中央,这样渐变出来的圆弧向下凸起,形成波谷。因为水位是从底部往上算的,波浪层顶部就是水面,所以波谷向下凸起看起来像水面凹陷,波峰向上凸起看起来像水面隆起。这个方向要根据实际效果调整,我试过circle at 50% 100%,出来的效果是反的,水面看起来像倒过来的。
7.3 波浪层与容器的层叠关系
水位指示器通常还需要显示百分比数字,数字要浮在波浪上面。这时候层叠顺序就很重要:
.water-level .percentage { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 2; font-size: 32px; font-weight: bold; color: #2c5f8a; mix-blend-mode: difference; }mix-blend-mode: difference可以让数字在波浪经过时自动反色,保证可读性。但这个属性在部分浏览器上性能开销较大,如果页面已经卡顿,建议改用纯色数字加文字阴影。
波浪层的z-index默认是0,数字层设为2,容器背景在底层。这样波浪在数字下方流动,数字始终可见。
7.4 实测性能数据与调整记录
我在一台2019年的中端笔记本上做了测试,页面同时运行四个水位指示器,每个都有波浪动画。Chrome DevTools的性能面板显示:
- 纯
transform动画方案:帧率稳定60fps,CPU占用约3%到5% - 加上
background-position渐变动画:帧率降到45到50fps,CPU占用升到8%到12% - 改用
clip-path方案:帧率约55fps,CPU占用6%到8%
最终我选择了纯transform加静态渐变的方案,牺牲了一点色彩变化,换来了最好的性能。对于数据大屏这种需要长时间运行的场景,稳定性比花哨更重要。
8. 常见问题与排查技巧实录
8.1 波浪循环时出现跳变怎么办
这是最常见的问题。原因几乎总是位移量不是波长的整数倍。检查你的@keyframes里translateX的值,确保它是--wave-length的整数倍。另外,波浪容器的宽度也必须是波长的整数倍,否则容器边缘会截断波形,导致循环时接不上。
还有一个隐蔽的原因:如果波浪层用了border或者padding,实际内容宽度会变化,导致波长计算不准。建议波浪层不要加border和padding,所有尺寸通过width和height控制。
8.2 波浪边缘出现锯齿或白边怎么处理
锯齿通常出现在clip-path方案的裁剪边缘。解决办法是给波浪层加一个微小的transform: translateZ(0),强制开启GPU抗锯齿。或者把波浪层的背景色和容器背景色设为一致,这样即使有锯齿也不明显。
白边通常是因为波浪层和容器之间有1px的间隙。检查bottom值是否设为0,以及容器是否有overflow: hidden。如果容器有border-radius,波浪层可能被圆角裁切后露出背景色,这时候把波浪层的宽度稍微加大一点,超出容器边缘即可。
8.3 动画在Safari上不流畅的排查思路
Safari对CSS动画的合成层处理有自己的逻辑。如果发现Safari上波浪动画卡顿,首先检查是否触发了不必要的重绘。用Safari的Web Inspector打开“显示绘制矩形”和“显示合成层”,看看动画过程中是否有大面积重绘。
常见的触发重绘的原因包括:动画化了background-position、使用了box-shadow、波浪层有opacity变化。解决办法是把这些效果移到单独的图层上,或者干脆去掉。
另一个原因是Safari对will-change的处理比较激进。如果你加了will-change: transform,Safari会提前创建合成层,但如果元素太多,反而会消耗大量内存。建议只在最外层的波浪容器上加will-change,内部的伪元素不要加。
8.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 循环时跳变 | 位移量非波长整数倍 | 检查keyframes的translateX值 | 改为波长的整数倍 |
| 波浪不流动 | 动画未生效或duration为0 | 检查animation属性拼写 | 确保animation-name匹配 |
| 边缘锯齿 | clip-path抗锯齿不足 | 放大页面观察边缘 | 加translateZ(0)或改用渐变方案 |
| 移动端卡顿 | 多层叠加或渐变动画 | 用性能面板录制 | 减少层数,去掉background-position动画 |
| 波浪方向反了 | 渐变圆心位置不对 | 调整circle at的坐标 | 改50% 0%为50% 100% |
| 水位不更新 | CSS变量未传递 | 检查变量作用域 | 确保变量定义在容器上 |
9. 进阶玩法:用CSS Houdini做更复杂的波浪
9.1 Houdini Paint API能带来什么
CSS Houdini的Paint API允许你用JavaScript写一个绘制函数,浏览器在渲染时调用这个函数来绘制背景。对于波浪来说,这意味着你可以用数学公式精确控制波形,而不是靠渐变去近似。
registerPaint('wave', class { static get inputProperties() { return ['--wave-length', '--wave-height', '--wave-color']; } paint(ctx, size, props) { const length = props.get('--wave-length').value; const height = props.get('--wave-height').value; const color = props.get('--wave-color').toString(); ctx.fillStyle = color; ctx.beginPath(); ctx.moveTo(0, size.height); for (let x = 0; x <= size.width; x++) { const y = size.height - height * Math.sin((x / length) * Math.PI * 2); ctx.lineTo(x, y); } ctx.lineTo(size.width, size.height); ctx.closePath(); ctx.fill(); } });这段代码用正弦函数生成波浪路径,比渐变方案精确得多。但Houdini的兼容性目前还有限,Chrome和Edge支持较好,Firefox和Safari还在推进中。如果你的项目不需要考虑这些浏览器,Houdini方案是值得尝试的。
9.2 什么时候值得上Houdini
判断标准很简单:如果你需要波浪形状精确可控、需要动态调整波形参数、而且目标用户主要用Chrome系浏览器,那就上Houdini。否则,纯CSS渐变方案已经能满足90%的需求。
我个人的做法是:先用纯CSS方案实现基础效果,如果后期发现波形精度不够,再考虑用Houdini做渐进增强。这样即使Houdini不被支持,页面也不会崩。
9.3 渐进增强的代码组织方式
.wave-container { background: repeating-radial-gradient(/* 回退方案 */); } @supports (background: paint(wave)) { .wave-container { background: paint(wave); } }用@supports检测paint()函数的支持情况,支持的话就用Houdini绘制,不支持就回退到渐变方案。这样代码结构清晰,维护起来也方便。
10. 我踩过的坑和最后分享几个小技巧
第一个坑:不要用vh单位控制波浪高度。移动端浏览器的地址栏收起和展开会改变vh的值,导致波浪高度跳变。用px或者%更稳定。
第二个坑:波浪层的overflow不要设为visible。如果波浪层超出了容器边界,在部分浏览器上会撑开滚动条。始终给波浪容器加overflow: hidden。
第三个坑:动画的animation-fill-mode不要用forwards。波浪是无限循环动画,forwards会让动画在第一次结束后保持最终状态,破坏循环。用默认的none或者both都可以,但forwards绝对不行。
分享一个小技巧:如果你想让波浪在暗色背景上也有好的视觉效果,可以给波浪层加一个微弱的filter: drop-shadow,让波浪边缘有发光感。但drop-shadow的性能开销比box-shadow大,慎用。
再分享一个:波浪的波长和波高可以用clamp()函数做成响应式的,比如--wave-length: clamp(100px, 20vw, 300px),这样在不同屏幕宽度下波浪会自动调整大小,不需要写多个媒体查询。
最后,如果你要做的是页面顶部的装饰波浪,记得给波浪容器加一个和下方内容区相同的背景色,这样波浪和内容之间不会有突兀的分界线。这个细节很小,但视觉效果差别很大。