更丰富的网页多图层效果:css混合模式

图层

在Photoshop等图像编辑软件里,图层是最基础的概念之一。我们平时看一张照片,就可能想到“远处的背景”、“近处的人物”这样的描述,这其实就是在划分图层。多个图层从下到上(从远到近)依次拼合,就得到完整的图像。

分图层很有用。一方面,图层是独立的,修改时不会相互影响,另一方面,图层可以保留原始图像,便于还原或做其他调整。

网页里的图层

在网页里,并没有明确的图层的概念,但却很适合当做图层去理解。网页内的每个元素,都可以看做有一个自己的图层。css里的z-index,也很像是调整图层顺序的工具。

准确地说,决定网页内元素覆盖关系的是绘制顺序,绘制顺序靠后的元素,将覆盖绘制顺序靠前的元素。

与绘制顺序密切相关的概念是层叠上下文stacking context)。在一个层叠上下文内,浏览器总是遵循特定的顺序去绘制该上下文内的所有元素。

重叠与合成

我们有时候会很仔细地去调整z-index,这是因为网页内的元素会有重叠:

像这样重叠时,一个元素就挡住另一个元素的一部分。我们可能想改变一下覆盖关系:

在这里,它们的差异只有元素的重叠部分。如果重叠部分和其中一个元素一致,那么看起来就是这个元素更靠上,覆盖了另一个元素。

实际上,网页里的重叠远不只有这样一小部分。上面的示例里的两个元素除了相互重叠外,各自也与本文的背景元素存在重叠。如果在<body>元素上设置一个背景,那么可以说页面内的所有可见元素的最终显示效果,都和这个背景关联了起来。

网页是如何决定在这样多的重叠关系下,最终呈现的效果的?这也是一个有一定章法的过程,叫做合成compositing)。

简单透明度合成

简单透明度合成simple alpha compositing)是我们很容易理解的一种合成方式。它是在本文接下来要介绍的混合模式出现之前,唯一在网页里应用的合成方式。它在当前各类浏览器内都可用。

之所以说容易理解,是因为它和我们平时的视觉感受很一致。比如前文的例子里改变一下其中一个元素的透明度:

当一个元素是半透明的,那么即便它遮挡了在它后面的元素的一部分,那一部分也是可见的。透明度越高(opacity越接近0),则重叠区域越偏向于被遮挡的元素。完全透明(opacity0)时,则看起来没有元素被遮挡。

新的合成:混合模式

混合模式一直是Photoshop等图像编辑软件内的图层面板的重要功能,现在它也存在于w3c的推荐规范

网页中可用的混合模式如下:

web混合模式

上图是对照Photoshop里的混合模式来标注的,其中紫红色部分是网页里可用的混合模式,最右边则是混合模式的群组分类。默认的“正常”,实际就是前面说的简单透明度合成,因此可以认为是新增了15种混合模式。把“正常”也算上的话,现在网页里可用的混合模式一共16种。

虽然混合模式种类不少,但最为常用的并不多,它们分别是正片叠底(Multiply)、滤色(Screen)、叠加(Overlay)和柔光(Soft Light)。

混合模式的简要原理

混合模式本质是分别取前景和背景(参与混合的两个图层)的像素点,然后用它们的颜色值进行数学运算,从而得到一个新的颜色值。每一个重叠区域的像素点都会经过这个计算过程。

下面以正片叠底为例,说明一下这个过程。

颜色值归一化

混合模式既然拿颜色值来做数学计算,那么颜色值一定是数字的形式。不过,颜色值会对应怎样的数字呢?在混合模式计算里,所有的颜色值都是从0到1的小数(区间[0, 1])。因此,颜色值在参与计算前,都会转换为这样的小数。

颜色都可以用RGB来表示,如纯白色在css里可以表示为rgb(255, 255, 255)。注意RGB三通道的最大值为255,最小值为0,因此可以用通道色值÷255的计算式转换为01的数字。比如纯白色将是rgb(1, 1, 1)

分通道进行计算

正片叠底(Multiply,日语版写作“乗算”)的计算式是:

x = a × b

可以看到,这就是一个简单的乘法。由于都是0到1之间的小数,因此乘法运算会使结果值更接近0(比如0.8 x 0.5 = 0.4)。因此,正片叠底是一个变暗的混合模式。

每一种混合模式,都会对应这样一个计算式,其中a表示前景图层(Active Layer),b表示背景图层(Background Layer)。RGB三通道分别进行一次运算后,就可以得到混合后的颜色值。整个过程如下:

正片叠底的计算过程

在图层设置了透明度的情况下,混合的计算式中的数字仍然取图层原本的像素颜色(也就是opacity: 1;时看到的颜色)。

css用法说明

与混合模式有关的一共3个属性,background-blend-modemix-blend-modeisolation

background-blend-mode

这个属性一般和多重背景的background-image搭配使用,它和background-image一样可以指定多个值。这将在某一个元素的多个背景之间形成混合,比如:

.blending-element-0 {
    background-image: url(1.jpg), url(2.jpg), url(3.jpg);
    background-blend-mode: multiply, screen;
}

可以看到,这个元素指定了多个背景,从上到下依次是1.jpg2.jpg3.jpg。下面的background-blend-mode则是与它对应的,它表示1.jpg的混合模式是multiply2.jpg的混合模式是screen3.jpg的混合模式是multiply(当background-blend-mode的数目比background-image少时,会按照值列表进行重复)。

需要注意的是,其中3.jpg这个位于最下层的背景(该元素无背景色),它的混合模式multiply其实是没有作用的,可以认为就是默认值normal。你可以试试在Photoshop里只留一个图层,然后切换各种混合模式,会发现图像不会有任何变化。

如果元素指定了背景色(background-color),那么背景色将成为最下层的背景。

如果有使用background这个简写属性,那么background-blend-mode的值将会被重置为默认。

mix-blend-mode与isolation

这2个属性是搭配使用的。相比前面的background-blend-mode是应用在单个元素的多背景之间,mix-blend-mode则是应用于多个元素,而且除背景外,元素内的文字等其他内容也会被混合。

mix-blend-mode比较类似opacity,作用于一个元素的同时也会作用于这个元素的全部子元素。因此,如果不想要子元素内容也受到影响(就像设置半透明时可能希望里面的文字仍是不透明的),改用background-blend-mode会更合适。

使用mix-blend-mode的代码大致这样:

<div class="container">
    <div class="blending-element-1"></div>
    <div class="blending-element-2"></div>
</div>
.blending-element-1,
.blending-element-2{
    mix-blend-mode: soft-light;
}

在这个例子中,只要div.blending-element-1div.blending-element-2存在重叠,就可以有混合效果。那么,只是这2个元素之间混合吗?父元素div.container有背景,甚至前面还有其他的位于下方的元素的话,它们也参与混合吗?

这就是如何界定哪些元素参与混合的问题。网页里是这样做的:以层叠上下文(stacking context)为依据,为元素进行分组,位于同一个层叠上下文内的元素算作同一组,同一组内才能发生混合

请再看这样一个例子(只列出了相关的css):

<div class="container">
    <div class="inner-wrapper">
        <img class="blending-image" src="1.jpg" alt="Rorona">
    </div>
</div>
.container {
    background: gray;
}
.blending-image {
    mix-blend-mode: multiply;
}

效果是:

mix-blend-mode

可以看到div.container的灰色背景和img.blending-image的图片内容已经有了混合效果。这时候,再给中间的元素div.inner-wrapper稍作改变:

.inner-wrapper{
    isolation: isolate;
}

会看到混合模式失效了:

mix-blend-mode + isolation

可以看到,在有了isolation: isolate这个属性后,好像就有了字面上“隔离”的效果。

事实上,并没有“隔离”这种特殊效果,它的本质是创建新的层叠上下文。在元素合成这一点上,你可以理解为是新开一个分组。分组有什么用呢?请看下图:

为图层分组

这种层级关系非常像html的DOM树,只不过,每一个节点(分组)的生成,都需要有对应元素创建新的层叠上下文。如果没有元素创建新层叠上下文,那么无论DOM树多么复杂,它们都属于同一个层叠上下文内(也就是上图没有任何group,layer1~6平铺)。

分组会改变元素参与混合的先后顺序。在复杂的DOM树中,可能有多个元素都设置了混合模式,这时候,总是组内的图层先相互混合,然后把整个组看作一个整体,再和组外的其他元素混合。由于DOM元素默认的混合方式都是normal,也就是上层遮挡下层的风格,因此看起来就好像组内和组外隔离了开来,这就是isolation的意思。

前面说isolation可以和mix-blend-mode搭配使用,就是因为它可以为元素之间的混合增加一层控制。对于background-blend-mode而言,isolation并没有用,因为background-blend-mode作用的多个背景都位于同一个元素内部,相当于一定在一个独立的分组内,和外部的其他元素无关。

关于更多的创建新的层叠上下文的条件,推荐查看MDN的文档。你可以看到isolation: isolate;只是其中的一种情况。

附带的一点补充

浏览器兼容性

就本文的时间点而言,浏览器的支持范围还是有些不足。不过,混合模式只是一个视觉效果,要做兼容的话,先看看那些不支持的浏览器里的效果吧。如果差得不多,你也许可以接受。如果情况并不能接受,那就做一些调整,比如图片素材等,直到它看起来不是太难看。

合成的另一个步骤

w3c文档里提到合成(compositing)实际上分为两步,第一步是混合(blending),紧接着还有第二步叫Porter-Duff compositing。如果你有兴趣,可以自行查看。

这个Porter-Duff compositing虽然包含了很多种类,但就css而言,其实只有source-over可用(也就是说,固定的)。这也正是和我们视觉感受最一致的前景覆盖后景的效果。

前文有提到混合模式的计算式所取的颜色值是不考虑透明度的,但实际我们知道在应用混合模式的同时设置透明度是可以改变效果的。这就是因为透明度会在这第二步合成里参与计算。

更多混合模式的计算原理

推荐阅读Photoshop Blend Modes Explained

结语

一个有趣的事情是,我们在用混合模式的时候,几乎都是会去一个一个试,直到找到看起来还不错的效果。而不是说,我能一眼知道应该用哪个混合模式。不过即便如此,了解一点混合模式的原理,也还是应该有助于我们更快地找到那个不错的混合模式的,大概。

在我看来,网页的混合模式可以减少某些图像素材的编辑工作。另外,因为原图被保留了下来,所以可以在任何需要的时候还原,或者重新参与合成。

Express结合Webpack的全栈自动刷新 3d transform的坐标空间及位置