CSS颜色混合模式

前言

混合模式(blend mode)是计算机图形学中的一个概念,用于处理帧绘制中不同(相邻)层级图像之间的颜色混合方式;CSS3开始引入了混合模式的功能,可以先从原理开始了解。

注:以下RGB颜色分量范围为[0, 1]。

混合模式

基本原理

关于混合模式的原理,其实W3C文档上面写的很详细,所有的混合模式可以精简成一个公式:

Cmix=f(Cbackdrop, Csource)C_{mix} = f(C_{backdrop},\ C_{source})

其中:

  • CmixC_{mix}:代表混合之后得到的颜色;
  • CbackdropC_{backdrop}:代表处于相对底层的颜色;
  • CsourceC_{source}:代表处于相对上层的颜色;
  • ff:代表用于混合两种颜色的函数;

所以,可以明显的看出来各种混合模式之间的区别就在于混合函数;

相关概念

  • 颜色通道:由于数字图像存储像素颜色信息都是按不同颜色成分分别进行存储的,因此将图像中的某一成分单独拿出来组成的灰度图像就是其中的一个颜色通道;例如常见的RGBA图像就有R,G,B,A四个颜色通道;

    A channel in this context is the grayscale image of the same size as a color image, made of just one of these primary colors.[1]

  • 互补色:在RGB颜色模型中,一种颜色的互补色就是用白色减去该颜色所得到的颜色;可以这样描述:

C=1CC_{互补} = 1 - C

  • 可分离混合模式Separable blend modes):这里的可分离指的是对不同的颜色通道进行混合然后再合并得到的混合效果是一样的,即不同的颜色通道只依赖于对应的颜色通道信息

混合函数

可分离

  • 正常(normal):默认情况,即上层像素直接覆盖下层像素;

f(Cb, Cs)=Csf(C_b,\ C_s) = C_s

  • 正片叠底(multiply):上层像素与下层像素各通道值进行相乘,得到的图像较原图像会偏暗;当一方像素为黑色时得到的像素自然也是黑色,反之若一方像素为白色得到的像素则为另一方像素原值;

f(Cb, Cs)=CbCsf(C_b,\ C_s) = C_b * C_s

  • 滤色(screen):先将两者的互补进行正片叠底,然后再取其互补;这种情况通常会使图像变得更亮,当一方像素为黑色时得到的像素值就是另一方像素值,而当一方像素为白色时得到的像素也是白色;因此最后混合得到图像会有一个特点:颜色值再低也不会低于两者中的任何一个

f(Cb, Cs)=1[(1Cb)(1Cs)]=Cb+CsCbCs\begin{aligned} f(C_b,\ C_s) &= 1 - [(1 - C_b) * (1 - C_s)]\\[1em] &= C_b + C_s - C_b * C_s \end{aligned}

  • 变暗(darken):取两者中各自通道中最小的值

f(Cb, Cs)=min(Cb,Cs)f(C_b,\ C_s) = min(C_b, C_s)

  • 变亮(lighten):取两者中各自通道中最大的值

f(Cb, Cs)=max(Cb,Cs)f(C_b,\ C_s) = max(C_b, C_s)

  • 颜色减淡(color-dodge):作用是提高底层颜色亮度,然后反射上层颜色,最后达到提高图像亮度的目的;

f(Cb, Cs)={0,Cb=01,Cs=1min(1, Cb1Cs),elsef(C_b,\ C_s) = \begin{cases} 0, & C_b = 0 \\[1em] 1, & C_s = 1 \\[1em] min(1,\ \large\frac{C_b}{1 - C_s}), & else \end{cases}

  • 颜色加深(color-burn):跟color-dodge作用相反,会降低底层颜色亮度,然后反射上层颜色,降低图像的亮度

f(Cb, Cs)={1,Cb=10,Cs=01min(1, 1CbCs),elsef(C_b,\ C_s) = \begin{cases} 1, & C_b = 1 \\[1em] 0, & C_s = 0 \\[1em] 1 - min(1,\ \large\frac{1 - C_b}{C_s}), & else \end{cases}

  • 强光(hard-light):颜色变亮或变暗取决于上层的像素颜色,暗则更暗,亮则更亮;效果类似于在底层图像上加上高(镜面)反射光

f(Cb, Cs)={fmultiply(Cb, 2Cs),Cs0.5fscreen(Cb, 2Cs1),elsef(C_b,\ C_s) = \begin{cases} f_{multiply}(C_b,\ 2 * C_s), & C_s \le 0.5 \\[1em] f_{screen}(C_b,\ 2 * Cs - 1), & else \end{cases}

  • 弱光(soft-light):颜色变亮或变暗取决于上层的像素颜色,效果类似于在底层图像上加上漫反射光

D(C)={((16C12)C+4)C,C0.25C,elsef(Cb, Cs)={Cb(12Cs)Cb(1Cb),Cs0.5Cb+(2Cs1)(D(Cb)Cb),elseD(C) = \begin{cases} ((16 * C - 12) * C + 4) * C, & C \le 0.25 \\[1em] \sqrt{C}, & else \end{cases}\\[2em] f(C_b,\ C_s) = \begin{cases} C_b - (1 - 2 * C_s) * C_b * (1 - C_b), & C_s \le 0.5 \\[1em] C_b + (2 * Cs - 1) * (D(C_b) - C_b), & else \end{cases}

  • 叠加(overlay):将两者互换位置,然后再进行强光处理;因此类似于给上层图像加了镜面反射光;

f(Cb, Cs)=fhardlight(Cs, Cb)f(C_b,\ C_s) = f_{hard-light}(C_s,\ C_b)

  • 差值(difference):获取两者各自通道之间的差值;

f(Cb, Cs)=CbCsf(C_b,\ C_s) = | C_b - C_s |

  • 排除(exclusion):从结果来看,exclusion等于滤色效果减去正片叠底;从效果上看则与差值类似,但是具有更低的对比度;

f(Cb, Cs)=Cb+Cs2CbCsf(C_b,\ C_s) = C_b + C_s - 2 * C_b * C_s

不可分离

顾名思义,不可分离指的就是一个颜色通道的颜色混合会依赖其他颜色通道的数据;CSS中不可分离混合模式通常采用以下步骤:

  1. RGB转为HSL颜色模型;
  2. 然后对H,S,L分量进行处理;
  3. 最后再最转回RGB颜色模型;

根据W3C文档可知,不可分离混合模式会使用以下几个函数:

  • Lum(C)Lum(C):获取颜色灰度值;
  • ClipColor(C)ClipColor(C):将颜色值范围进行校正,保证颜色分量在[0, 1]之间;
  • SetLum(C, l)SetLum(C,\ l):将颜色值转为灰度为l的颜色;
  • Sat(C)Sat(C):获取RGB颜色的饱和度(即HSL中的S分量);
  • SetSat(C, s)SetSat(C,\ s):将RGB颜色中的饱和度转为s

可以用GLSL进行相应函数的描述:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
void main {
// RGB转灰度
float lum (vec3 color) {
return 0.3 * color.r + 0.59 * color.g + 0.11 * color.b;
}

// 获取rgb颜色分量中的最大值
float minChannel (vec3 c) {
return min(min(c.r, c.g), min(c.g, c.b));
}

// 获取rgb颜色分量中的最小值
float maxChannel (vec3 c) {
return max(max(c.r, c.g), max(c.g, c.b));
}

// 校正rgb颜色值
vec3 clipColor (vec3 color) {
vec3 c = color;
float l = lum(color);
float n = maxChannel(color);
float x = minChannel(color);
if (n < 0.0) {
c = l + (((c - l) * l) / (l - n));
}
if (x > 1.0) {
c = l + (((c - l) * (1.0 - l)) / (x - l));
}
return c;
}

// 设置rgb颜色灰度
vec3 setLum (vec3 color, float l) {
float d = l - lum(color);
return clipColor(color + d);
}

// 获取rgb颜色的饱和度s分量
float sat (vec3 c) {
return maxChannel(c) - minChannel(c);
}
}
  • 色相(hue):使用底层颜色的SL分量,加上上层颜色的H分量,得到一个新的颜色;

f(Cb, Cs)=SetLum(SetSat(Cs, Sat(Cb)), Lum(Cb))f(C_b,\ C_s) = SetLum(SetSat(C_s,\ Sat(C_b)),\ Lum(C_b))

  • 饱和度(saturation):使用底层颜色的HL分量,加上上层颜色的S分量,得到一个新的颜色;

f(Cb, Cs)=SetLum(SetSat(Cb, Sat(Cs)), Lum(Cb))f(C_b,\ C_s) = SetLum(SetSat(C_b,\ Sat(C_s)),\ Lum(C_b))

  • 颜色(color):使用上层颜色的HS分量,加上底层的颜色的L分量,得到一个新的颜色;

f(Cb, Cs)=SetLum(Cs, Lum(Cb))f(C_b,\ C_s) = SetLum(C_s,\ Lum(C_b))

  • 明度(luminosity):使用底层颜色的HS分量,加上上层的颜色的L分量,得到一个新的颜色;

f(Cb, Cs)=SetLum(Cb, Lum(Cs))f(C_b,\ C_s) = SetLum(C_b,\ Lum(C_s))

CSS中混合模式相关的属性

css3中引入了两个与混合模式有关的属性:

  • background-blend-mode:用于指定元素背景(包括背景图和背景色)之间的混合模式;
  • mix-blend-mode:用于指定元素与其叠层之下的元素/层级之间的混合模式;

混合模式的应用

滤色模式叠加特效

可以使用mix-blend-mode: screen的方式给图片/视频或某一区域叠加特效图片/动图,从而达到叠加特效的效果;比如:

img

上面这种效果就是利用上述混合模式,在图片上叠加了一层火焰燃烧的动图,动图如下:

img

由滤色混合模式特点可知,上层像素为黑色时混合的结果就是底层像素,因此这种模式下特效图片都是黑底,就是为了让非特效部分像素保持原样,而特效部分像素则与
底层像素进行融合;

有人可能会有些疑问,为何不直接把特效图片做成带透明通道的,这样非特效部分的像素就是完全透明的,然后直接叠加在底层区域上不就好了;显然,直接叠加的效果不太真实,而滤色混合模式则会对底层像素和上层像素进行某种比例的融合,这样看起来有点接近光反射的模型,看起来更真实;

滤色模式制作图片背景的文字

img

看到上面这个效果,可能很多人马上能够想到利用backgound-image + -webkit-background-clip: text + color: transparent组合实现;然而这个目前仅能在webkit内核浏览器才能有效果,但是知道了滤色混合模式的原理后,可以利用其“顶层为黑色像素时保留底层像素,为白色像素时则为白色”的特点来实现一样的效果:

  • 文字单独设置成一层,然后设置mix-blend-mode: screen
  • 文字颜色为黑色:因此可以对文字点阵区域保留底层的像素;
  • 文字区域背景色为白色:因此文字点阵以外区域皆为白色;
1
2
3
4
<div class="demo">
<img src="https://placekitten.com/200/100">
<div class="text">text</div>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.demo2 {
position: relative;
width: 200px;
height: 100px;
}
.text {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
font-size: 80px;
line-height: 100px;
text-align: center;
background-color: white;
mix-blend-mode: screen;
}

而且mix-blend-mode目前兼容性比较好,PC端完全没问题,移动端可能安卓稍低的版本不太支持;

渐变色文字

img

渐变色文字原理跟上面的图片背景文字一样,只不过是把底层的图片换成渐变背景色而已;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
.demo {
position: relative;
width: 600px;
height: 100px;
margin-top: 20px;
background-image: linear-gradient(45deg, blue, green, orange);
}
.grad {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
font-size: 60px;
font-weight: bold;
line-height: 100px;
text-align: center;
background-color: white;
mix-blend-mode: screen;
}
1
2
3
<div class="demo">
<span class="grad">渐变文字 Color</span>
</div>

自制滤镜

虽然css3新增的filter属性提供了很多滤镜,但是丰富的混合模式加上各自的特点,再配合各种各样的图片就能够形成丰富多彩的自定义滤镜;

正片叠底过滤颜色通道

由于正片叠底的特点,当某颜色通道为0时,可以使混合后相应的颜色通道也为0,也就达到了过滤特定颜色通道的目的;比如上层图像为纯红色(相当于(1, 0 , 0)),此时正片叠底的效果就是只保留底层图像的R通道;

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
.demo {
margin-top: 20px;
}
.single {
display: inline-block;
width: 200px;
height: 160px;
background: url("https://placekitten.com/200/160"), red;
background-blend-mode: multiply;
}
.single:nth-of-type(2) {
background: url("https://placekitten.com/200/160"), green;
}
.single:nth-of-type(3) {
background: url("https://placekitten.com/200/160"), blue;
}
.single:nth-of-type(4) {
background: url("https://placekitten.com/200/160");
}
1
2
3
4
5
6
<div class="demo">
<div class="single"></div>
<div class="single"></div>
<div class="single"></div>
<div class="single"></div>
</div>

也可以保留任意两个颜色通道:

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.single {
display: inline-block;
width: 200px;
height: 160px;
background: url("https://placekitten.com/200/160"), rgb(255, 255, 0);
background-blend-mode: multiply;
}
.single:nth-of-type(2) {
background: url("https://placekitten.com/200/160"), rgb(255, 0, 255);
}
.single:nth-of-type(3) {
background: url("https://placekitten.com/200/160"), rgb(0, 255, 255);
}
.single:nth-of-type(4) {
background: url("https://placekitten.com/200/160");
}
1
2
3
4
5
6
<div class="demo">
<div class="single"></div>
<div class="single"></div>
<div class="single"></div>
<div class="single"></div>
</div>

甚至可以控制单一颜色通道的比例然后再混合起来,相当于逐像素进行计算来实现某种滤镜算法;当然,这种利用混合模式去实现滤镜效果和直接使用GPU计算在性能上的差距还是有待验证的,不过简单的组合还是可行的;

晕影效果

利用正片叠底 + 径向渐变可以实现晕影的滤镜效果,且通过调整径向渐变的半径大小可以控制晕影大小;

img

1
2
3
4
5
6
7
.demo {
width: 300px;
height: 300px;
margin-top: 20px;
background: url("https://placekitten.com/300/300"), radial-gradient(white 50%, black 100%);
background-blend-mode: multiply;
}

其他

了解了混合模式的原理后,可以发现混合模式可以用来做出非常丰富的图像处理,只要结合各个混合模式的算法特点就可以实现很棒的效果,还不用写底层代码;

相关文档


  1. Channel (digital image) - Wikipedia ↩︎