用 css 和 svg 绘制一朵“真实”的云

用 css 和 svg 绘制一朵“真实”的云

本文根据文章 Drawing Realistic Clouds with SVG and CSS,仅记录自己学习过程。

最终实现效果:

首先,CSS的box-shadow属性有五个值:

1
box-shadow: <offsetX> <offsetY> <blurRadius> <spreadRadius> <color>;
描述
h-shadow 必需。水平阴影的位置。允许负值。
v-shadow 必需。垂直阴影的位置。允许负值。
blur 可选。模糊距离。
spread 可选。阴影的尺寸。
color 可选。阴影的颜色。
inset 可选。将外部阴影 (outset) 改为内部阴影。

我们把这些值调高,就会得到类似影子木偶的效果:

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

1
2
3
4
5
6
<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过滤器进行关联:

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

差不多就是这样:

尝试使用feDisplacementMap的scale属性

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
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>的宽度:

1
2
3
4
5
6
7
8
#cloud-circle {
width: 500px;
height: 275px;
background: #000;
border-radius: 50%;
filter: url(#filter);
box-shadow: 200px 200px 60px 0px #fff;
}

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

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

1
2
3
4
5
6
7
8
9
10
11
#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过滤器绘制前中后三朵云:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<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就够了。

最后的效果