【译】来,用 SVG & CSS 给你画一朵真实的云

3,521 阅读9分钟

原文:Drawing Realistic Clouds with SVG and CSS

这是作者最终实现的效果:

......哦,不,应该是这张:
在线查看效果:cloud demo


正文开始啦~

希腊神话讲述了一个关于宙斯创造云女神涅斐勒的故事。和其他希腊神话一样,这个故事极其怪异并且有点限制级。下面的表述则是一个比较简短且含蓄的版本(限制级,非战斗人员请撤离)。

涅斐勒(云神),据说是宙斯按照自己美丽妻子的形象创造的。传说有个凡人遇见了涅斐勒,一见钟情爱上了她并且在一起了,后来她们一起睡了一个觉,然后奇怪的事情发生了,一朵云生下了一个半人半马的小孩,传说这就半人马的祖先。

很不可思议对吗?就我个人而言,我搞不懂。但庆幸的是,浏览器中创建云的过程要简单得多,也没有那么不可描述。

这是@袁川画的云demo

最近,我发现开发者 @袁川 已经用代码实现了仿真的云烟。对我来说,在浏览器中实现一直是个神话。

通过简单扫一下这个demo里面的代码,我们可以现象,逼真而又独特的云朵是可以通过使用cssbox-shadow和一个包含两个元素的SVG过滤器<filter>去实现。

我们想要的仿真效果是通过feTurbulencefeDisplacementMap之间的微妙混合来实现的。SVG过滤器功能强大、复杂,并且提供了令人非常兴奋的功能(还包括奥斯卡获奖算法)。然而,在它的底层,它们的复杂性可能有点吓人!

译者:奥斯卡获奖算法,这么牛逼?无知限制了我的想象。

虽然SVG的物理特性超出了本文的范畴,但是在MDNw3.org上有大量的文档。一个免费的关于feTurbulencefeDisplacement非常有用的页面(同时被作为这本惊奇的书的一个章节)

对于本文,我们专注学习如何使用SVG的过滤器实现惊人的效果。我们不需要深入研究其底层的算法,正如艺术家不需要了解涂漆的分子结构也能绘出令人惊叹的风景一样。

先从一些基础开始

CSS的box-shadow属性有五个值需要搞懂:

box-shadow: <offsetX> <offsetY> <blurRadius> <spreadRadius> <color>;

让我们把这些值调高(可能比任何理智的开发人员调得都要高):

#cloud-square {
  background: turquoise;
  box-shadow: 200px 200px 50px 0px #000;
  width: 180px;
  height: 180px;
}

#cloud-circle {
  background: coral;
  border-radius: 50%;
  box-shadow: 200px 200px 50px 0px #000;
  width: 180px;
  height: 180px;
}

会得到下面的效果图:

你曾经应该也玩过或者看过影子木偶对吗?像下面这样:

译者:看见这个,想起小时候自己也玩过, 两个拇指叉在一块能摆出鸽子的投影。

就像一只手改变形状可以改变投影一样,我们改变HTML的“源形状”也可以使渲染在浏览器中的投影变形。box-shadow复制了原始尺寸和border-radius上的“渐变”特性,SVG过滤器则同时应用于元素及其阴影。

<svg width="0" height="0"> 
  <filter id="filter">
    <feTurbulence type="fractalNoise" baseFrequency=".01" numOctaves="10" />
    <feDisplacementMap in="SourceGraphic" scale="10" />
  </filter>
</svg>

这是我们目前的SVG代码,它不会被渲染,因为我们还没有定义任何可见的东西。它唯一的目的就是保存我们为SourceGraphic(也就是我们的<div>)提供的过滤器。

我们借助SVG过滤器的ID,通过添加CSS规则将HTML元素(#cloud-circle)和SVG过滤器进行关联:

#cloud-circle {
  filter: url(#filter);
  box-shadow: 200px 200px 50px 0px #fff;
}

嗯!很棒,就是这样

在线查看demo

别急!我们只是了解了皮毛,还有很多好东西要看。

尝试使用feDisplacementMap的scale属性

使用这一属性进行一些非科学试验可以产生显著的效果。现在,我们保持feTurbulence的值不变,简单调整feDisplacementMapscale属性值。

随着scale的增加(以30为增量),我们的源<div>变得扭曲,投射的阴影反应出天空中云出现的随机形式。

<feDisplacementMap in="SourceGraphic" scale="180"/>

在线查看demo

好了,我们有进展了!让我们稍微改变颜色,以形成更具说服力的云。

body {
  background: linear-gradient(165deg, #527785 0%, #7FB4C7 100%);
}

#cloud-circle {
    width: 180px;
    height: 180px;
    background: #000;
    border-radius: 50%;
    filter: url(#filter);
    box-shadow: 200px 200px 50px 0px #fff;
}

现在,我们越来越接近真实的云朵效果了!

修改box-shadow的模糊度

下面一套图片展示了box-shadow属性的模糊度作用的效果,这里,我们以10px递增模糊值:

随着模糊值增加,云朵也变得更加柔和。

为了增加一点积云的效果,我们可以稍微扩宽源<div>的宽度:

#cloud-circle {
  width: 500px; 
  height: 275px;
  background: #000;
  border-radius: 50%;
  filter: url(#filter);
  box-shadow: 200px 200px 60px 0px #fff;
}

很好,我们的源元素开始碍事了😫

等等,我们扩宽了源元素的宽度,但它现在遮挡在我们云层(白色阴影)的上方。让我们在更远的位置重新投影,这样我们的云就不会再被源图像遮挡了(你可以想象成把你的手往远离墙的方向移动,这样它就不会挡住你的影子木偶的视线了)。

这点我们通过CSS定位可以很好地实现。<body>是父元素,默认是静态定位的,我们给源<div>添加绝对定位。最初地,这也会重新定位我们的阴影,因此我们还需要增加阴影和元素之间的距离。

#cloud-circle {
  width: 500px;
  height: 275px;
  background: #000;
  border-radius: 50%;
  filter: url(#filter);
  box-shadow: 400px 400px 60px 0px #fff; /* 增加投影位移 */
  position: absolute;
  top: -320px;
  left: -320px;
}

是的,我们已经实现了一个极具说服力的云:这里查看

浏览器上展示的已经是一个相当完美的云了,但我不确定......这片云真的能公正地描述云女神涅斐勒吗?我相信我们还可以做得更好!

通过层次传达深度

这是我们想要的效果:

从这张照片中云层的深度、纹理和丰富性来看,宙斯一定是读过艺术专业的。至少,他一定读过《通用设计法则》,这本书阐述了一个强大而又普通的概念:

照明偏差在深度和自然度的解释中起着重要作用,设计师可以通过多种方式操纵照明偏差,利用明暗区域之间的对比度来改变深度的外观。

这段话给了我们一个提示,我们可以将不同形状、大小和颜色的图层堆叠在一起,可以实现像参考图片中那样具有高保真度的云。我们要做的也只是多次调用SVG过滤器。

使用三个SVG过滤器绘制前中后三朵云:

<svg width="0" height="0">
    <!-- 后层 -->
    <filter id="filter-back">
      <feTurbulence type="fractalNoise" baseFrequency="0.012" numOctaves="4" />
      <feDisplacementMap  in="SourceGraphic" scale="170" />
    </filter>
    <!-- 中层 -->
    <filter id="filter-mid">
      <feTurbulence type="fractalNoise" baseFrequency="0.012" numOctaves="2" />
      <feDisplacementMap  in="SourceGraphic" scale="150" />
    </filter>
    <!-- 前层 -->
    <filter id="filter-front">
      <feTurbulence type="fractalNoise" baseFrequency="0.012" numOctaves="2" />
      <feDisplacementMap  in="SourceGraphic" scale="100" />
    </filter>
</svg>

通过分层的应用,我们有机会去探索feTurbulence并认识它的多样性。我们选择了较为平滑的类型:fractalNoise,对于numOctaves的值最高只调到了6。

上面这些意味着什么?我们来看一下baseFrequency这个属性,下面几张图片是不同baseFrequency值下的效果:

值越低,图像就越圆,越模糊。

从效果看,介于0.005~0.01的值比较符合我们想要的积云效果。

用numOctaves添加细节

增加numOctaves值允许我们以更细的粒度去渲染图像,这个过程需要大量的计算,因此需要注意:高值会严重影响性能。

numOctaves的值设置得越高,云的效果就越精细。

幸运的是我们不需要为达到精细的效果而设置太高的值,介于4~5就够了。

最后的效果

查看效果

用seed属性进行无限变形

关于seed属性有很多可以说,但就我们的目的而言,seed的作用可以归结为:不同的值,不同的形状

柏林噪音函数使用这个值作为其随机数生成器的起点。选择不包含此属性则将seed值默认为0;当包含时,无论我们设置何值,都不需要担心会影响性能。

不同的seed值,对应生成不同的形状。

上面的GIF代表了seed作用的效果。请记住,每个云都是分层的复合云(虽然我调整了每一层的属性,但我保持了它们的seed值一致)。

仔细观察上面这张参考图片,我将3个云层<div>堆砌在一个基础的div上,通过反复试验不同的seed值,最终得到了与图中比较相似的形状。如下图:

在线查看demo

天空的极限

显然,认为我们用<div>在浏览器上绘制的云比涅斐勒高级是很荒谬的。

但是,我们能够梳理出CSS和SVG过滤器的神秘感越多,我们就越有能力去创造在视觉上令人惊叹的东西,并且高度保真于雷神的创作。那么,我们可以做进一步的试验了!

在这篇文章中,我们刚刚涉足了一个充满力量和复杂性的知识海洋,SVG过滤器通常看起来复杂地难以理解。

不过,就像A Single Div ProjectSmith's绘画技术中的例子一样,有趣和实验性的方法总会给人惊艳的效果。

我希望这篇文章能让你对web上进行摄影写实开发感到兴奋,欢迎下方评论交流你的想法~