视觉效果
- 单侧投影
- 单侧投影
- 邻边投影
- 双侧投影
- 不规则投影
- 染色效果
- 基于滤镜的方案
- 基于混合模式的方案
单侧投影
单侧投影
大多数人使用 box-shadow 的方法是,指定三个长度值和一个颜色值:
box-shadow: 2px 3px 4px rgba(0,0,0,.5);
接下来的几个步骤很好地(虽然在技术上还不够严谨)以图形化的方式讲解了投影是如何绘制的(参见下图)。
(1) 以该元素相同的尺寸1①和位置,画一个 rgba(0,0,0,.5) 的矩形。
(2) 把它向右移 2px,向下移 3px。
(3) 使用高斯模糊算法(或类似算法)将它进行 4px 的模糊处理。这在本质上表示在阴影边缘发生阴影色和纯透明色之间的颜色过渡长度近似于模糊半径的两倍(比如在这里是 8px)。
(4) 接下来,模糊后的矩形与原始元素的交集部分会被切除掉,因此它看起来像是在该元素的“后面”。这跟大多数开发者所理解的情况(元素叠在模糊后矩形的上层)可能稍有不同。不过,在某些场景下,意识到没有任何投影绘制在元素的下层十分重要。举例来说,如果给元素设置一层半透明的背景,我们就看不到它下层有任何投影。这一点跟 text-shadow 不同,因为文字下层的投影不会被裁切。
使用 4px 的模糊半径意味着投影的尺寸会比元素本身的尺寸大约 8px,因此投影的最外圈会从元素的四面向外显露出来1①。我们只需改变偏移量,就可以把投影的顶部和左侧隐藏起来,只要这两个方向上的偏移量不小于4px 就可以了。但是,这在某种程度上会导致外露的投影太过浓重,看起来不是很美观(参见下图)。另外,就算这个问题勉强可以接受,我们想要的投影也只能出现在单侧(而不是相邻的两侧),还记得吗?
最终的解决方案来自 box-shadow 鲜为人知的第四个长度参数。它排在模糊半径参数之后,称作扩张半径。这个参数会根据你指定的值去扩大或(当指定负值时)缩小投影的尺寸。举例来说,一个 -5px 的扩张半径会把投影的宽度和高度各减少 10px(即每边各 5px)。
从逻辑上来说,如果我们应用一个负的扩张半径,而它的值刚好等于模糊半径,那么投影的尺寸就会与投影所属元素的尺寸完全一致。除非用偏移量(前两个长度参数)来移动它,我们将完全看不见任何投影。因此,如果给投影应用一个正的垂直偏移量,我们就会在元素的底部看到一道投影,而元素的另外三侧是没有投影的,这正是我们一直苦苦追寻的效果:
box-shadow: 0 5px 4px -4px black;
邻边投影
另一个经常被问到的问题是,如何在元素的两条边上设置投影。如果这两条边是相邻的(比如右侧和底部),就比较容易一些:要么满足于第二个图这样的效果,要么运用上一段所述技巧,并做出如下调整。
☑ 我们不应该把投影缩得太小,而是只需把阴影藏进一侧,另一侧自然露出就好。因此,扩张半径不应设为模糊半径的相反值,而应该是这个相反值的一半。
☑ 需要指定两个偏移量,因为我们希望投影在水平和垂直方向上同时移动。它们的值需要大于或等于模糊半径的一半,因为我们希望把投影藏进另外两条边之内。举例来说,把一个 black、6px
的投影设置到右侧和底部可以这样做:
box-shadow: 3px 3px 6px -3px black;
双侧投影
当我们想把投影设置在元素的两条对边(比如左侧和右侧)时,事情就变得棘手了。因为扩张半径在四个方向上的作用是均等的(也就是说,我们无法指定投影在水平方向上放大,而在垂直方向上缩小),唯一的办法是用两块投影(每边各一块)来达到目的。然后基本上就是把“单侧投影”中的技巧运用两次:
box-shadow: 5px 0 5px -5px black,
-5px 0 5px -5px black;
不规则投影
难题
当我们想给一个矩形或其他能用 border-radius 生成的形状(在“自
适应的椭圆”一节中可以看到一些示例)加投影时,box-shadow 的表现都堪称完美。但是,当元素添加了一些伪元素或半透明的装饰之后,它就有些力不从心了,因为 border-radius 会无耻地忽视透明部分。这类情况包括:
☑ 半透明图像、背景图像、或者 border-image(比如老式的金质像框);
☑ 元素设置了点状、虚线或半透明的边框,但没有背景(或者当background-clip 不是 border-box 时);
☑ 对话气泡,它的小尾巴通常是用伪元素生成的;
☑ 我们在“切角效果”一节中见过的切角形状;
☑ 几乎所有的折角效果,包括“折角效果”一节将提到的例子;
☑ 通过 clip-path 生成的形状,比如“菱形图片”一节中提到的菱形
图像。
解决方案
滤镜效果规范(http://w3.org/TR/filter-effects)为这个问题提供了一个解决方案。它引入了一个叫作 filter 的新属性,这个属性也是从 SVG 那里借鉴过来的。尽管 CSS 滤镜基本上就是 SVG 滤镜,但我们并不需要掌握任何SVG 知识。相反,只需要一些函数就可以很方便地指定滤镜效果了,比如blur()、grayscale() 以及我们需要的 drop-shadow() !如果你喜欢,甚至可以把多个滤镜串连起来,只要用空格把它们分隔开就可以了,比如:
filter: blur() grayscale() drop-shadow()
drop-shadow() 滤镜可接受的参数基本上跟 box-shadow 属性是一样的,但不包括扩张半径,不包括 inset 关键字,也不支持逗号分割的多层投影语法。举个例子,上面的投影:
box-shadow: 2px 2px 10px rgba(0,0,0,.5);
可以这样来写:
filter: drop-shadow(2px 2px 10px rgba(0,0,0,.5));
CSS 滤镜最大的好处在于,它们可以平稳退化:当浏览器不支持时,不会出现问题,只不过没有任何效果而已。如果你确实需要这个效果在尽可能多的浏览器中显示出来,可以同时附上一个 SVG 滤镜,这样可以得到稍微好一些的浏览器支持度。你可以在滤镜效果规范(http://www.w3.org/TR/filter-effects/)中为每个滤镜函数找到对应的 SVG 滤镜版本。你可以把 SVG滤镜和简化的 CSS 滤镜放在一起使用,让层叠机制来决定哪一行最终生效:
filter: url(drop-shadow.svg#drop-shadow);
filter: drop-shadow(2px 2px 10px rgba(0,0,0,.5));
不幸的是,如果 SVG 滤镜是存放在一个独立文件里的,那它就无法像一个简洁易用的函数那样在 CSS 代码中进行随意配置;如果它是内联的,则又会搅乱你的代码。参数需要写死在文件内部,因此每当我们新加一种哪怕是大同小异的投影效果时,都需要多准备一个文件,这显然是难以接受的。当然,我们还可以使用 data URI(它也会省掉额外的 HTTP 请求),但这个方法仍然会带来文件体积的增长。总的来说,这个方法只是一种回退方案,因此只要我们把SVG 滤镜控制在一定数量以内,哪怕它们的效果大同小异,也是说得过去的。
另外一件需要牢记的事情就是,任何非透明的部分都会被一视同仁地打上投影,包括文本(如果背景是透明的),正如我们刚刚在图 4-7 中看到的那样。你可能会想,是不是可以通过 text-shadow: none; 来取消掉文本上的投影呢?其实 text-shadow 跟它是完全不相干的两码事,因此这样做并不能取消文本上的 drop-shadow() 效果。此外,如果你已经用 text-shadow 在文本上加了投影效果,文本投影还会被 drop-shadow() 滤镜再加上投影,这本质上是给投影打了投影!看看下面这段示例代码(请原谅它惨不忍睹的效果,这样只是为了凸显这个怪异的问题):
color: deeppink;
border: 2px solid;
text-shadow: .1em .2em yellow;
filter: drop-shadow(.05em .05em .1em gray);
你可以在下图中看到它的渲染效果,图中的文字被同时打上text-shadow 和 drop-shadow()。
染色效果
基于滤镜的方案
由于没有一种现成的滤镜是专门为这个效果而设计的,我们需要花一些心思,把多个滤镜组合起来。
我们要使用的第一个滤镜是 sepia(),它会给图片增加一种降饱和度的橙黄色染色效果,几乎所有像素的色相值会被收敛到 35~40(参见图 4-10)。如果这种色调正是我们想要的,那就可以收工了。不过我们的需求通常并非如此。如果我们想要的主色调的饱和度比这更高,可以用 saturate() 滤镜来给每个像素提升饱和度。假设我们想要的主色调是 hsl(335, 100%, 50%)
,那就需要把饱和度提升一些,于是我们将饱和度参数设置为 4。具体取值取决于实际情况,我们通常需要用肉眼来观察和判断。如图 4-11 所示,这两个滤镜的组合会让我们的图片具有一种暖金色的染色效果。图片现在看起来很不错,但我们并不希望把图片调为这种橙黄色调,而是稍深的亮粉色。因此,我们还需要再添加一个 hue-rotate() 滤镜,把每个像素的色相以指定的度数进行偏移。为了把原有的色相值 40 改变为 335,我们需要增加大约 295 度(335 – 40):
filter: sepia(1) saturate(4) hue-rotate(295deg);
此时,我们就把这张图片的色调改变了,效果如图 4-12 所示。如果这个效果需要由 :hover 或其他状态来触发切换,我们甚至还可以为这个变化增加过渡动画:
img {
transition: .5s filter;
filter: sepia(1) saturate(4) hue-rotate(295deg);
}
img:hover,
img:focus {
filter: none;
}
基于混合模式的方案
滤镜方案是行之有效的,但你可能会注意到它产生的结果与我们在图像处理软件中得到的效果不完全一致。即使我们想把图像调为一种很亮的颜色,结果仍然会显得像褪了色一般。如果尝试在 saturate() 滤镜中增加饱和度,又会得到一种不自然的、过度风格化的效果。不过,幸好我们还有另一种更好的实现方法——混合模式!
如果用过 Adobe Photoshop 这样的图像处理软件,那你可能已经对混合模式相当熟悉了。当两个元素叠加时,“混合模式”控制了上层元素的颜色与下层颜色进行混合的方式。用它来实现染色效果时,需要用到的混合模式是 luminosity。这种 luminosity 混合模式会保留上层元素的 HSL 亮度信息,并从它的下层吸取色相和饱和度信息。如果在下层准备好我们想要的主色调,并把待处理的图片放在上层并设置为这种混合模式,那本质上不就是在做染色处理吗?
要对一个元素设置混合模式,有两个属性可以派上用场:mix-blendmode可以为整个元素设置混合模式,background-blend-mode 可以为每层背景单独指定混合模式。这意味着,如果用这个方案来处理图片,我们实际上有两种选择。不过这两者各有所短。
☑第一种选择:需要把图片包裹在一个容器中,并把容器的背景色设
置为我们想要的主色调。
☑ 第二种选择:不用图片元素,而是用 <div>
元素——把这个元素的第一层背景设置为要染色的图片,并把第二层的背景设置为我们想要的主色调。
针对不同的场景,可以选择这两者的其中之一。举个例子,如果我们希望对一个 <img>
元素应用这个效果,就需要把它包含在另一个元素内部。不过如果我们已经有了这一层容器,比如 <a>
,那就水到渠成了:
<a href="#something">
<img src="tiger.jpg" alt="Rawrrr!" />
</a>
然后,只需要两行声明就可以实现这个效果:
a {
background: hsl(335, 100%, 50%);
}
img {
mix-blend-mode: luminosity;
}
和 CSS 滤镜类似,混合模式可以平稳退化:如果不被支持,效果只是不出现而已,图片本身还是完好可见的。
有一件事情需要注意,滤镜是可动画的,而混合模式则不是。我们在上面已经见识过了,一张图片只需要在 filter 属性上设置好 CSS 过渡之后就可以从全彩样式慢慢淡化为单色样式,但你无法对混合模式做同样的事情。不过也别着急,这并不表示过渡动画是完全不可能的,只是意味着我们需要跳出框框来重新思考。
如上面所解释的那样,mix-blend-mode 是把整个元素向下进行混合,而不管它的下层是什么。因此,如果我们把这个属性设置为 luminosity 混合模式,那图片就总是会跟某些东西进行混合。此外,使用 background-blend-mode 属性则可以让每层背景跟它的下层背景进行混合,但并不关心元素之外是什么情况。另外,当我们只有一个背景图像以及一个透明背景色时,会发生什么?你猜对了:不会出现任何混合效果!
好的,接下来我们将利用上述分析结果,采用 background-blend-mode 属性来达成我们想要的效果。在此之前,HTML 代码需要稍作整:
<div class="tinted-image"
style="background-image:url(tiger.jpg)">
</div>
这样一来,我们就只需要对一个 <div>
元素设置 CSS 了,因为这个技巧并不需要其他额外的元素:
.tinted-image {
width: 640px; height: 440px;
background-size: cover;
background-color: hsl(335, 100%, 50%);
background-blend-mode: luminosity;
transition: .5s background-color;
}
.tinted-image:hover {
background-color: transparent;
}
不过,就像前面提到的那样,这两种方法都不够理想。它们的主要问题在于:
☑ 图片的尺寸需要在 CSS 代码中写死;
☑ 在语义上,这个元素并不是一张图片,因此并不会被读屏器之类的设备读出来。
生活就是这样,没有十全十美。在这一节中,我们收获了三种实现染色效果的方法,每种方法都各有优缺点。到底选择哪种方法,还是要看项目的具体需求。