如何在 CSS 中创建波浪形和图案

Avatar of Temani Afif
Temani Afif

DigitalOcean 为您旅程的每个阶段提供云产品。 立即开始使用 200 美元的免费额度!

波浪可能是 CSS 中最难制作的形状之一。 我们总是尝试使用诸如 border-radius 和大量魔法数字之类的属性来近似它,直到我们得到一些感觉比较接近的东西。 而且这甚至是在我们开始处理更难的波浪图案之前。

“用 SVG 来做!” 你可能会说,你可能说得对,那是一个更好的方法。 但我们会看到 CSS 可以创建漂亮的波浪,并且它的代码不必过于复杂。 猜猜看? 我有一个 在线生成器 使其变得更加简单!

如果您使用生成器进行操作,您会发现它生成的 CSS 只有两个渐变和一个 CSS 遮罩属性——仅仅这两样东西,我们就可以创建任何类型的波浪形或图案。 更不用说我们还可以轻松控制波浪的大小和曲率。

一些值可能看起来像是“魔法数字”,但实际上它们背后有逻辑,我们将剖析代码并发现创建波浪的所有秘密。

本文是对 之前的一篇文章 的后续,在那篇文章中,我构建了各种不同的锯齿形、范围限定的、扇贝形的,是的,还有波浪形的边框。 我强烈建议查看那篇文章,因为它使用了我们将在本文中介绍的相同技术,但更详细。

波浪背后的数学

严格来说,波浪形背后没有一个神奇的公式。 任何具有上下曲线形状的形状都可以称为波浪,因此我们不会将自己局限于复杂的数学。 相反,我们将使用几何基础来重现波浪。

让我们从一个使用两个圆形形状的简单示例开始

Two gray circles.

我们有两个半径相同的圆形并排放置。 你看到那条红线了吗? 它覆盖了第一个圆形的上半部分和第二个圆形的下半部分。 现在想象一下,你取那条线并重复它。

A squiggly red line in the shape of waves.

我们已经看到了波浪。 现在让我们填充底部(或顶部)以获得以下内容

Red wave pattern.

瞧! 我们得到了一个波浪形,并且我们可以使用一个圆形半径变量来控制它。 这是我们可以制作的最简单的波浪之一,也是我在 之前的文章 中展示的波浪。

让我们通过取第一个插图并稍微移动圆形来增加一些复杂性

Two gray circles with two bisecting dashed lines indicating spacing.

我们仍然有两个半径相同的圆形,但它们不再水平对齐。 在这种情况下,红线不再覆盖每个圆形的一半面积,而是覆盖了一个较小的面积。 此区域受虚线红线限制。 那条线穿过两个圆形相遇的点。

现在取那条线并重复它,你就会得到另一个波浪,一个更平滑的波浪。

A red squiggly line.
A red wave pattern.

我想你明白了。 通过控制圆形的位置和大小,我们可以创建任何我们想要的波浪。 我们甚至可以为它们创建变量,我将分别称之为 PS

您可能已经注意到,在在线生成器中,我们使用两个输入来控制波浪。 它们映射到上述变量。 S 是“波浪的大小”,P 是“波浪的曲率”。

我将 P 定义为 P = m*S,其中 m 是您在更新波浪曲率时调整的变量。 这允许我们始终保持相同的曲率,即使我们更新 S。

m 可以是 02 之间的任何值。 0 将为我们提供第一个特殊情况,其中两个圆形水平对齐。 2 是一种最大值。 我们可以更大,但在经过一些测试后,我发现任何超过 2 的值都会产生不良的扁平形状。

别忘了我们圆形的半径! 这也可以使用 SP 来定义,如下所示

R = sqrt(P² + S²)/2
R = sqrt(m²*S² + S²)/2
R = S*sqrt(m² + 1)/2

m 等于 0(即 P 等于 0)时,我们将有 R = S/2

我们拥有将所有这些转换为 CSS 中的渐变所需的一切!

创建渐变

我们的波浪使用圆形,当谈到圆形时,我们谈论的是径向渐变。 由于两个圆形定义了我们的波浪,因此我们将在逻辑上使用两个径向渐变。

我们将从 P 等于 0 的特殊情况开始。 这是第一个渐变的插图

此渐变创建第一个曲率,同时填充整个底部区域——可以说是波浪的“水”。

.wave {
  --size: 50px;

  mask: radial-gradient(var(--size) at 50% 0%, #0000 99%, red 101%) 
    50% var(--size)/calc(4 * var(--size)) 100% repeat-x;
}

--size 变量定义径向渐变的半径和大小。 如果我们将它与 S 变量进行比较,那么它等于 S/2

现在让我们添加第二个渐变

第二个渐变不过是一个圆形,用于完成我们的波浪

radial-gradient(var(--size) at 50% var(--size), blue 99%, #0000 101%) 
  calc(50% - 2*var(--size)) 0/calc(4 * var(--size)) 100%

如果您查看 之前的文章,您会发现我只是在重复我在那里已经做的事情。

我遵循了两篇文章,但渐变配置并不相同。

这是因为我们可以使用不同的渐变配置来达到相同的结果。 如果您比较这两种配置,您会注意到对齐方式略有不同,但技巧是相同的。 如果您不熟悉渐变,这可能会令人困惑,但不用担心。 通过一些练习,您会习惯它们,并且您会发现不同的语法可以产生相同的结果。

以下是我们第一个波浪的完整代码

.wave {
  --size: 50px;

  mask:
    radial-gradient(var(--size) at 50% var(--size),#000 99%, #0000 101%) 
      calc(50% - 2*var(--size)) 0/calc(4 * var(--size)) 100%,
    radial-gradient(var(--size) at 50% 0px, #0000 99%, #000 101%) 
      50% var(--size)/calc(4 * var(--size)) 100% repeat-x;
}

现在让我们获取此代码并对其进行调整,以便我们引入一个变量,使之可以完全重复使用以创建我们想要的任何波浪。 正如我们在上一节中看到的,主要技巧是移动圆形,使它们不再对齐,因此让我们更新每个圆形的位置。 我们将第一个向上移动,第二个向下移动。

我们的代码将如下所示

.wave {
  --size: 50px;
  --p: 25px;

  mask:
    radial-gradient(var(--size) at 50% calc(var(--size) + var(--p)), #000 99%, #0000 101%) 
      calc(50% - 2*var(--size)) 0/calc(4 * var(--size)) 100%,
    radial-gradient(var(--size) at 50% calc(-1*var(--p)), #0000 99%, #000 101%) 
      50% var(--size) / calc(4 * var(--size)) 100% repeat-x;
}

我引入了一个新的 --p 变量,用于定义每个圆形的中心位置。 第一个渐变使用 50% calc(-1*var(--p)),因此其中心向上移动,而第二个使用 calc(var(--size) + var(--p)) 向下移动。

演示胜过千言万语

圆形既不对齐也不相互接触。 我们将它们隔开了一段距离,而没有改变它们的半径,因此我们失去了波浪。 但是我们可以通过使用前面用于计算新半径的相同数学方法来解决问题。 请记住 R = sqrt(P² + S²)/2。 在我们的案例中,--size 等于 S/2--p 也一样,它也等于 P/2,因为我们正在移动两个圆形。 因此,它们的中心点之间的距离为此值的 --p 的两倍

R = sqrt(var(--size) * var(--size) + var(--p) * var(--p))

这给了我们 55.9px 的结果。

我们的波浪回来了! 让我们将该等式插入我们的 CSS 中

.wave {
  --size: 50px;
  --p: 25px;
  --R: sqrt(var(--p) * var(--p) + var(--size)*var(--size));

  mask:
    radial-gradient(var(--R) at 50% calc(var(--size) + var(--p)), #000 99%, #0000 101%) 
      calc(50% - 2*var(--size)) 0 / calc(4 * var(--size)) 100%,
    radial-gradient(var(--R) at 50% calc(-1*var(--p)), #0000 99%, #000 101%) 
      50% var(--size)/calc(4 * var(--size)) 100% repeat-x;
}

上面的 CSS 看起来有效,但它不会起作用,因为我们不能将两个长度相乘,因此我们必须引入 m 变量,它将控制曲率,如上一节所述。

.wave {
  --size: 50px;
  --m: 0.5;
  --p: calc(var(--m)*var(--size));
  --R: calc(var(--size)*sqrt(var(--m)*var(--m) + 1));

  mask:
    radial-gradient(var(--R) at 50% calc(var(--size) + var(--p)), #000 99%, #0000 101%) 
      calc(50% - 2*var(--size)) 0 / calc(4 * var(--size)) 100%,
    radial-gradient(var(--R) at 50% calc(-1*var(--p)), #0000 99%, #000 101%) 
      50% var(--size)/calc(4 * var(--size)) 100% repeat-x;
}

这非常酷:只需要两个渐变就可以获得一个很酷的波浪,您可以使用 mask 属性将其应用于任何元素。 不再需要反复试验——您只需要更新两个变量,就可以开始了!

反转波浪

如果我们希望波浪朝另一个方向前进,我们填充“天空”而不是“水”会怎样。 信不信由你,我们只需要更新两个值

.wave {
  --size: 50px;
  --m: 0.5;
  --p: calc(var(--m)*var(--size));
  --R: calc(var(--size)*sqrt(var(--m)*var(--m) + 1));

  mask:
    radial-gradient(var(--R) at 50% calc(100% - (var(--size) + var(--p))), #000 99%, #0000 101%)
      calc(50% - 2 * var(--size)) 0/calc(4 * var(--size)) 100%,
    radial-gradient(var(--R) at 50% calc(100% + var(--p)), #0000 99%, #000 101%) 
      50% calc(100% - var(--size)) / calc(4 * var(--size)) 100% repeat-x;
}

我在那里所做的只是添加了一个等于 100% 的偏移量,上面突出显示。 这是结果

我们可以考虑使用关键字值更友好的语法,使其更容易

.wave {
  --size: 50px;
  --m: 0.5;
  --p: calc(var(--m)*var(--size));
  --R: calc(var(--size)*sqrt(var(--m)*var(--m) + 1));

  mask:
    radial-gradient(var(--R) at left 50% bottom calc(var(--size) + var(--p)), #000 99%, #0000 101%) 
      calc(50% - 2 * var(--size)) 0/calc(4 * var(--size)) 100%,
    radial-gradient(var(--R) at left 50% bottom calc(-1 * var(--p)), #0000 99%, #000 101%) 
      left 50% bottom var(--size) / calc(4 * var(--size)) 100% repeat-x;
}

我们使用 leftbottom 关键字来指定边和偏移量。 默认情况下,浏览器默认为 lefttop——这就是为什么我们使用 100% 将元素移动到底部。 实际上,我们将其从 top 移动了 100%,因此它实际上与说 bottom 相同。 比数学更容易阅读!

使用此更新的语法,我们只需将 bottom 替换为 top——反之亦然——即可更改波浪的方向。

如果您想要获得顶部和底部的波浪,我们将所有渐变组合到一个声明中

.wave {
  --size: 50px;
  --m: 0.5;
  --p: calc(var(--m)*var(--size));
  --R: calc(var(--size)*sqrt(var(--m)*var(--m) + 1));

  mask:
    /* Gradient 1 */
    radial-gradient(var(--R) at left 50% bottom calc(var(--size) + var(--p)), #000 99%, #0000 101%) 
      left calc(50% - 2*var(--size)) bottom 0 / calc(4 * var(--size)) 51% repeat-x,
    /* Gradient 2 */
    radial-gradient(var(--R) at left 50% bottom calc(-1 * var(--p)), #0000 99%, #000 101%) 
      left 50% bottom var(--size) / calc(4 * var(--size)) calc(51% - var(--size)) repeat-x,
    /* Gradient 3 */
    radial-gradient(var(--R) at left 50% top calc(var(--size) + var(--p)), #000 99%, #0000 101%) 
      left calc(50% - 2 * var(--size)) top 0 / calc(4 * var(--size)) 51% repeat-x,
    /* Gradient 4 */
    radial-gradient(var(--R) at left 50% top calc(-1 * var(--p)), #0000 99%, #000 101%) 
      left 50% top var(--size) / calc(4 * var(--size)) calc(51% - var(--size)) repeat-x;
}

如果您查看代码,您会发现除了组合所有渐变之外,我还将它们的高度从 100% 减少到 51%,以便它们都覆盖元素的一半。 是的,51%。 我们需要那一点额外的百分比才能进行少量重叠以避免间隙。

左右两侧呢?

这是你的家庭作业! 借鉴我们在顶部和底部所做的操作,尝试更新值以获得左右值。 别担心,这很容易,您只需要交换值即可。

如果您遇到问题,您可以随时使用 在线生成器 检查代码并可视化结果。

波浪线

之前,我们使用红线创建了第一个波浪,然后填充了元素的底部部分。那个波浪线呢?那也是一个波浪!如果我们可以用变量控制它的粗细以便重复使用,那就更好了。让我们来做吧!

我们不会从头开始,而是利用之前的代码并进行更新。首先要更新渐变的颜色停止点。两个渐变都从透明色到不透明色,反之亦然。为了模拟线条或边框,我们需要从透明开始,变为不透明,然后再次变回透明。

#0000 calc(99% - var(--b)), #000 calc(101% - var(--b)) 99%, #0000 101%

我想你已经猜到我们使用--b变量来控制线条的粗细了。让我们将其应用到我们的渐变中。

是的,结果远非波浪线。但仔细观察,我们可以看到一个渐变正确地创建了底部曲率。因此,我们真正需要做的就是修正第二个渐变。与其保持一个完整的圆形,不如像另一个渐变一样做一个部分圆形。

仍然差很远,但我们有了所需的两个曲率!如果你查看代码,你会发现我们有两个相同的渐变。唯一的区别是它们的位置。

.wave {
  --size: 50px;
  --b: 10px;
  --m: 0.5;
  --p: calc(var(--m)*var(--size));
  --R: calc(var(--size)*sqrt(var(--m)*var(--m) + 1));

  --_g: #0000 calc(99% - var(--b)), #000 calc(101% - var(--b)) 99%, #0000 101%;
  mask:
    radial-gradient(var(--R) at left 50% bottom calc(-1*var(--p)), var(--_g)) 
      calc(50% - 2*var(--size)) 0/calc(4*var(--size)) 100%,
    radial-gradient(var(--R) at left 50% top    calc(-1*var(--p)), var(--_g)) 
      50% var(--size)/calc(4*var(--size)) 100%;
}

现在我们需要调整大小和位置以获得最终形状。我们不再需要渐变占据整个高度,所以可以用这个替换100%

/* Size plus thickness */
calc(var(--size) + var(--b))

这个值没有数学逻辑依据。它只需要足够大以形成曲率。我们很快就会看到它对图案的影响。同时,让我们也更新位置以垂直居中渐变。

.wave {
  --size: 50px;
  --b: 10px;
  --m: 0.5;
  --p: calc(var(--m)*var(--size));
  --R: calc(var(--size)*sqrt(var(--m)*var(--m) + 1));

  --_g: #0000 calc(99% - var(--b)), #000 calc(101% - var(--b)) 99%, #0000 101%;  
  mask:
    radial-gradient(var(--R) at left 50% bottom calc(-1*var(--p)), var(--_g)) 
      calc(50% - 2*var(--size)) 50%/calc(4 * var(--size)) calc(var(--size) + var(--b)) no-repeat,
    radial-gradient(var(--R) at left 50% top calc(-1 * var(--p)), var(--_g)) 50%
      50%/calc(4 * var(--size)) calc(var(--size) + var(--b)) no-repeat;
}

仍然不太对。

一个渐变需要稍微向下移动,另一个需要稍微向上移动。两者都需要移动其高度的一半。

我们快完成了!我们需要对半径进行一个小修复以实现完美的重叠。两条线都需要偏移一半边框(--b)的粗细。

我们成功了!一个完美的波浪线,我们可以通过控制几个变量轻松调整。

.wave {
  --size: 50px;
  --b: 10px;
  --m: 0.5;
  --p: calc(var(--m)*var(--size));
  --R: calc(var(--size)*sqrt(var(--m)*var(--m) + 1) + var(--b)/2);

  --_g: #0000 calc(99% - var(--b)), #000 calc(101% - var(--b)) 99%, #0000 101%;
  mask:
    radial-gradient(var(--R) at left 50% bottom calc(-1 * var(--p)), var(--_g)) 
     calc(50% - 2*var(--size)) calc(50% - var(--size)/2 - var(--b)/2) / calc(4 * var(--size)) calc(var(--size) + var(--b)) repeat-x,
    radial-gradient(var(--R) at left 50% top calc(-1*var(--p)),var(--_g)) 
     50%  calc(50% + var(--size)/2 + var(--b)/2) / calc(4 * var(--size)) calc(var(--size) + var(--b)) repeat-x;
}

我知道这个逻辑需要一点时间才能理解。没关系,正如我所说,在CSS中创建波浪形状并不容易,更不用说它背后的复杂数学了。这就是为什么在线生成器 是一个救星——即使你没有完全理解它背后的逻辑,也可以轻松获得最终代码。

波浪图案

我们可以用我们刚刚创建的波浪线制作一个图案!

哦,不,图案的代码将更难以理解!

一点也不!我们已经有了代码。我们只需要从现有的代码中删除repeat-x,然后,瞧!🎉

一个漂亮的波浪图案。还记得我说的我们会重新讨论的方程式吗?

/* Size plus thickness */
calc(var(--size) + var(--b))

好吧,这就是控制图案中线条之间距离的因素。我们可以把它变成一个变量,但没有必要增加更多复杂性。在生成器中,我甚至没有为此使用变量。也许我以后会改变它。

这是同一个图案朝不同方向延伸。

我在演示中提供了代码,但我希望你能够分析它并理解我做了哪些更改来实现这一点。

简化代码

在所有之前的演示中,我们总是独立定义--size--p。但--p--size的函数,所以我们可以删除它并像下面一样更新我们的代码。

.wave {
  --size: 50px;
  --m: 0.5;
  --R: calc(var(--size) * sqrt(var(--m) * var(--m) + 1));

  mask:
    radial-gradient(var(--R) at 50% calc(var(--size) * (1 + var(--m))), #000 99%, #0000 101%) 
      calc(50% - 2*var(--size)) 0/calc(4 * var(--size)) 100%,
    radial-gradient(var(--R) at 50% calc(-1 * var(--size) * var(--m)), #0000 99%, #000 101%) 
      50% var(--size) / calc(4 * var(--size)) 100% repeat-x;
  }

如你所见,我们不再需要--p变量了。我用var(--m)*var(--size)替换了它,并相应地优化了一些数学运算。现在,如果我们想使用特定的波浪形状,我们也可以省略--m变量并用固定值替换它。例如,让我们尝试使用.8

.wave {
  --size: 50px;
  --R: calc(var(--size) * 1.28);

  mask:
    radial-gradient(var(--R) at 50% calc(1.8 * var(--size)), #000 99%, #0000 101%) 
      calc(50% - 2*var(--size)) 0/calc(4 * var(--size)) 100%,
    radial-gradient(var(--R) at 50% calc(-.8 * var(--size)), #0000 99%, #000 101%) 
      50% var(--size) / calc(4 * var(--size)) 100% repeat-x;
}

看看代码现在是不是更容易了?只需要一个变量来控制你的波浪,而且你也不再需要依赖sqrt()了!

你可以将相同的逻辑应用于我们看到的所有的演示,包括波浪线和图案。我从详细的数学解释和通用代码开始,但你可能会发现自己在实际使用场景中需要更简单的代码。我一直都在这样做。我很少使用通用代码,但我总是考虑一个简化的版本,尤其是在大多数情况下,我使用一些已知的值,这些值不需要存储为变量。(**剧透警告:**我将在最后分享一些例子!)

这种方法的局限性

从数学上讲,我们编写的代码应该可以为我们提供完美的波浪形状和图案,但在现实中,我们会遇到一些奇怪的结果。所以,是的,这种方法有其局限性。例如,在线生成器能够产生糟糕的结果,尤其是在波浪线方面。部分问题是由于值的特定组合导致结果变得混乱,例如与大小相比使用较大的边框粗细值。

对于其他情况,则是与某些舍入相关的问题,这会导致错位和波浪之间的间隙。

也就是说,我仍然认为我们介绍的方法仍然是一个好方法,因为它在大多数情况下都能产生平滑的波浪,并且我们可以通过尝试不同的值直到获得完美的结果来轻松避免不良结果。

总结

我希望在阅读完本文后,你将不再需要通过反复试验来构建波浪形状或图案。除了在线生成器,你还掌握了创建任何你想要的波浪背后的所有数学秘密!

本文到此结束,但现在你拥有了一个强大的工具来创建使用波浪形状的花哨设计。以下是一些启发你的灵感……

你呢?使用我的在线生成器(或者如果你已经记住了所有数学知识,可以手动编写代码),并向我展示你的作品!让我们在评论区收集一些好的作品。