CSS的mask-composite

发布于 大漠

CSS的mask(遮罩),有时也称CSS的蒙层,最早是苹果公司2008年提出的,并且添加到webkit引擎当中。遮罩提供一种基于像素级别的,可以控制元素透明度的能力,类型于png24位或png32位中的alpha透明通道的效果。2012年被纳入到W3C的草案中,但这个版本与苹果公司提出的版本是不同的。时至今日,该规范已经有多个版本,现在是CSS Masking Module Level1版本,属于TR阶段。据Caniuse.com统计来讲,该属性得到的支持度还是有一定的限制,仅部分属性被浏览器支持。虽然如此,但该属性还是非常的有意思,值得大家花点时间去探究,比如今天要聊的mask-composite属性就是非常有意思的一个属性。

mask原理和语法

文章开头也提到过,mask提供一种基于像素级别的,可以控制元素透明度的能力,类似于png24位或png32位中alpha透明通道的效果。

图像是由RGB三个通道以及在每个像素上定义的颜色组成的。但是在他们之上还有第四个通道,即**alpha通道**。通过高度定义每个像素上的透明度。白色意味着不透明,黑色意味着透明,介于黑白之间的灰色表示半透明。比如下图:

给一个HTML元素使用CSS的mask属性,就会这样处理。不用给图片应用一个alpha通道,只需要给一个图片运用一个mask-imagemask-border-source的样式:

mask-image: url(mouse.png)

他从图片遮罩里读出图片的透明信息,然后应用到HTML元素上,比如下图这个效果:

遮罩可以让头像按照特定形状显示。

CSS遮罩可以使得图片按照任意的形状显示。或者你可能有很长的文本需要滚动显示,那么可以使用遮罩让他从不透明到透明的渐变显示

在使用mask的时候,其中有一个不可或缺的部分就是遮罩(也被称为蒙板),该遮罩可以是半透明的PNG图片CSS的渐变SVG元素,遮罩元素的alpha值为0的时候会覆盖下面的元素,为1的时候会完全显示下面的内容。如下图所示:

而使用CSS的mask也并不太复杂,其语法规则和background非常的类似。在W3C官网上提供了两者相关属性的对照表:

mask属性 background属性
mask-clip background-clip
mask-image background-image
mask-origin background-origin
mask-position background-position
mask-repeat background-repeat
mask-size background-size
mask-mode  
mask-composite  
  background-attachment
  background-color

具体的语法规则:

mask: <mask-layer>

<mask-layer>对应的值:

<mask-layer> = <mask-reference> <masking-mode>? || <position> [ / <bg-size> ]? ||
<repeat-style> || <geometry-box> || [ <geometry-box> | no-clip ] || <compositing-operator>

即:

mask: [mask-image] [mask-repeat] [mask-position] / [ mask-size]

background属性类似,建议mask-size单独写出来:

mask: [mask-image] [mask-repeat] [mask-position]
mask-size: [mask-size]

上面我们列出的的仅是mask部分属性,在规范中还提供了其他的属性,这几个属性有点类似于CSS中的border属性,比如mask-border-sourcemask-border-modemask-border-slicemask-border-widthmask-border-outsetmask-border-repeatmask-border等。另外还可以用于SVG,比如mask-type

上面简单的介绍了CSS中mask的基本原理和使用规则,但我们今天的重点并不是来了解所有的属性,而是针对性的学习其属性之一,即**mask-composite**属性。如果你想了解更多有关于mask相关的知识,建议你花点时间阅读下列文章:

mask-composite

mask-composite仅是CSS的mask的子属性之一。该属性的工作原理是什么?它有什么用?使用它的价值是什么?

如果将上述问题一一答出来之后,可以彻底让我们掌握mask-composite的原理和使用方式。@Ana Tudor新出的博文《Mask Compositing: The Crash Course》就详细介绍了这些,也称得上是mask-composite速成记。

特别声明,接下来的内容和图片资源来自于@Ana Tudor的《Mask Compositing: The Crash Course》一文!在此特别感谢@Ana Tudor为我们提供这么优秀的教程。

什么是遮罩合成

遮罩合成指的是我们可以使用不同的操作将多个不同的遮罩层合并成一个独立的遮罩层。那么问题来了?多个遮罩层是如何合并成一个呢?比如我们有两个遮罩层,在这两个遮罩层中取每对对应的像素,在它们的通道上应用特定的合成操作(具体合成的操作细节,后面会介绍),并为最终层获得第三个像素。如下图所示:

上图中左上图和左下图合层起来成了右侧的层。而左上图被称为源(Source),左下图被称为目标层(Destination),这对地我们来说没有多大的意义,因为给我的感觉一个是输入源,一个是输出结果(事实上,这两个都是输入),但是,就上图的结果而言,这两个层(源和目标层)却做了一个合层的操作(也被称为合层计算),从而得到最终的结果(上图右侧的合并层)。

上面演示的是仅有两个层合并,而事实上呢?我们可能会有两个以上的层合并,当有这种情形时,合层是分阶段完成的,从底部开始。

在第一阶段,从底部开始的第二层是源,从底部开始的第一层是目标,这两层被合层,结果成为第二阶段的目标,接着和从底部开始的第三层(源)合并。通过合成前两层的结果合成第三层,我们就得到了第三阶的目标,接着再从底部的第四层源合并。如下图这样的一个合并过程:

以此类推,直到我们达到最后一个阶段,在这里,最顶层由下面所有层的合成结果组成

遮罩合成有什么用?

CSS和SVG的遮罩都有各自的局限性和优缺点。我们可以通过使用CSS遮罩来绕过SVG遮罩的限制,但是,由于CSS的遮罩和SVG遮罩的工作方式不同,采用CSS遮罩我们无法在不进行合成的情况下实现某些结果。

为了更好地理解这一切,让我们来看看下面这张令人敬畏的西伯利亚小老虎的图片:

假设我们使用遮罩期望得到的效果如下图所示:

这个特殊的mask保持了菱形的可见性,而分隔它们的线被遮罩,我们可以通过图像看到后面的元素。

我们希望这种遮罩效果是灵活的。我们不希望与图像的尺寸或长宽比绑定,我们希望能够轻松地随图像缩放和不缩放的mask之间进行切换(只需要将%值更改为px)。

为了做到这一点,我们首先需要了解SVG和CSS的遮罩是如何工作,以及我们可以和不可以使用它们做什么。

SVG遮罩

SVG遮罩默认情况下是亮度(luminance)遮罩。这意味着遮罩元素上白色像素部分是完全不透明,而黑色像素部分是完全透明的,而遮罩元素白色和黑色像素之间的亮度(greypinklime)是半透明的。

给定RGB值对应亮度的计算公式为:.2126·R + .7152·G + .0722·B

对于我们这个示例,这意味着我们需要将菱形区域变成白色,分隔线是黑色,如下图所示:

为了得到上图的效果,使用SVG的rect绘制一个纯白色的矩形,然后使用path在这个矩形中添加两条黑色的对角线(确保stroke的值为black)。

先创建第一条对角线(从左上角到右下角),在左上角使用M命令,然后在右下角使用L命令;接着创建第二条对角线(从右上角到左下角),在右上角使用M命令,然后在左下角使用L命令。对应的SVG代码如下:

<svg viewBox="0 0 837 551" width="837" height="551">
    <rect width="837" height="551" fill="#fff"></rect>
    <path d="M0 0 L837 551 M837 0 L0 551" stroke="#000"></path>
</svg>

得到的效果如下图所示,但离我们想要的菱形图案的效果还相差很远。

我们可以使用stroke-width来改变线条的粗细,然后使用stroke-dasharray属性来改变线条之间的间距:

<svg viewBox="0 0 837 551" width="837" height="551">
    <rect width="837" height="551" fill="#fff"></rect>
    <path d="M0 0 L837 551  M837 0 L0 551" stroke="#000" stroke-width="15%" stroke-dasharray="1% 4%"></path>
</svg>

我们可以继续增大stroke-width的值到150%,最终效果会覆盖整个矩形,而且离我们想要的效果越来越近了:

现在,我们可以将rectpath元素封装在一个mask元素中,并将这个遮罩应用于我们想要的任何元素上,比如我们这个示例中,就是小老虎的img

<svg viewBox="0 0 837 551">
    <mask id="m">
        <rect width="837" height="551" fill="#fff"></rect>
        <path d="M0 0 L837 551 M837 0 L0 551" stroke="#000" stroke-width="15%" stroke-dasharray="1% 7%"></path>
    </mask>
</svg>

<img src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/2017/amur_tiger_cub_buffalo_zoo.jpg" width="837"/>

img { 
    mask: url(#m) 
}

上述方法应该是有效的。但遗憾的是,效果并不是我们期许的一样。在Firefox中得么的效果是我们期望的,而在Chrome中并不是我们想要的效果,甚至应用该遮罩之后(mask添加-webkit前缀),元素(img)消失了。

如果我们希望在Chrome浏览器中也能得到我们期望的效果,最简单的做法就是把img元素放在SVG中,使用image元素来替代:

<svg viewBox="0 0 837 551" width="837">
    <mask id="m">
        <rect width="837" height="551" fill="#fff"></rect>
        <path d="M0 0 L837 551  M837 0 L0 551" stroke="#000" stroke-width="150%" stroke-dasharray="1% 7%"></path>
    </mask>
    <image xlink:href="https://s3-us-west-2.amazonaws.com/s.cdpn.io/2017/amur_tiger_cub_buffalo_zoo.jpg" width="837" mask="url(#m)"></image>
</svg>

得到的效果和我们期望的很相似了:

虽然得到我们想要的效果,但如果我们想屏蔽另一个HTML元素,而不是img元素,事情就会变得有点复杂,因为我们需要将它放在SVG的foreignObject中。

更糟糕的是,使用这个解决方案,我们要对维度进行硬编码,这令人非常恶心心。我们可以尝试通过maskContentUnits切换到objectBoundingBox

<svg viewBox="0 0 837 551" width="837">
    <mask id="m" maskContentUnits="objectBoundingBox">
        <rect width="1" height="1" fill="#fff"></rect>
        <path d="M0 0 L1 1 M1 0 L0 1" stroke="#000" stroke-width="1.5" stroke-dasharray=".01 .07"></path>
    </mask>
    <image xlink:href="https://s3-us-west-2.amazonaws.com/s.cdpn.io/2017/amur_tiger_cub_buffalo_zoo.jpg" width="100%" mask="url(#m)"></image>
</svg>

但我们仍然在viewBox中硬编码尺寸大小,虽然它们的实际值并不重要,但它们的长宽比很重要。此外,我们的遮罩模式现在是在1x1正方形内创建的,然后拉伸到覆盖mask元素。形状拉伸意味着形状的扭曲,这就是为什么我们的菱形不再像以前那样了。

我们可以尝试调整path的起点和终点:

<svg viewBox="0 0 837 551" width="837">
    <mask id="m" maskContentUnits="objectBoundingBox">
        <rect width="1" height="1" fill="#fff"></rect>
        <path d="M-.75 0 L1.75 1 M1.75 0 L-.75 1" stroke="#000" stroke-width="1.5" stroke-dasharray=".01 .07"></path>
    </mask>
    <image xlink:href="https://s3-us-west-2.amazonaws.com/s.cdpn.io/2017/amur_tiger_cub_buffalo_zoo.jpg" width="100%" mask="url(#m)"></image>
</svg>

也就是说,要得到一个特定的菱形图案,就需要知道菱形的角度,也就需要知道图像的长宽比。

上面我们看了SVG中遮罩是如何工作的,我们接着来看看CSS中的遮罩是怎么做的。

CSS遮罩

CSS遮罩默认情况下是alpha遮罩。这意味着,完全不透明遮罩像素对应的是遮罩元素像素完全不透明,完全透明遮罩像素对应的是遮罩元素像素完全透明,半透明遮罩像素对应的是遮罩元素的像素是半透明的。简单地说,遮罩元素的每个像素将获得对应遮罩元素像素的alpha通道。

就我们示例而言,菱形区域是不透明的,而分隔它们的线是透明的。在SVG中使用path绘制菱形,而在CSS中,我们可以使用CSS渐变来绘制菱形。为了得到白色菱形区域和黑色分隔线的图案,可以使用两个repating-linear-gradient来绘制:

关键代码如下:

repeating-linear-gradient(-60deg,  #000 0, #000 5px, transparent 0, transparent 35px), 
repeating-linear-gradient(60deg,  #000 0, #000 5px, #fff 0, #fff 35px)

如果我们也想要一个亮度(luminance)的遮罩,那么这个模式就会起作用。

但在alpha遮罩的例子中,并不是黑色的像素给了我们完全的透明度,而是透明的像素;并不是白色像素让我们完全不透明,而是完全不透明的像素。其中redblackwhite都做同样的事情,但我从此人更倾向于使用redtan,因为这意味着只需要输入三个字母,输入的字母更少,出错的机会也就更少。

首先想到的是使用同样的技术得到不透明的菱形区域和透明的分隔红玫瑰。但是这样做的时候,我们遇到了一个问题:第二层渐变层的不透明部分覆盖了第一层部分,但我们希望仍然要保持透明,反之亦然。

我最初的想法是使用带有白色菱形区域和黑色分隔线的模式,结合设置mask-mode的值为luminance,让CSS遮罩像SVG遮罩一样工作来解决。

div {
    background: repeating-linear-gradient(60deg, #000 0, #000 0.5em, transparent 0, transparent 1.5em), repeating-linear-gradient(-60deg, #000 0, #000 0.5em, #fff 0, #fff 1.5em);
}
div:nth-child(2) {
    background: repeating-linear-gradient(60deg, #fff 0, #fff 0.5em, transparent 0, transparent 1.5em), repeating-linear-gradient(-60deg, #fff 0, #fff 0.5em, #000 0, #000 1.5em);
}
div:nth-child(n + 3) {
    background: linear-gradient(white, dimgrey);
    --m: repeating-linear-gradient(60deg, #000 0, #000 0.5em, transparent 0, transparent 1.5em), repeating-linear-gradient(-60deg, #000 0, #000 0.5em, #fff 0, #fff 1.5em);
    -webkit-mask: var(--m);
    mask: var(--m);
    mask-mode: luminance;
    mask-source-type: luminance;
}
div:nth-child(4) {
    --m: repeating-linear-gradient(60deg, #fff 0, #fff 0.5em, transparent 0, transparent 1.5em), repeating-linear-gradient(-60deg, #fff 0, #fff 0.5em, #000 0, #000 1.5em);
}

此属性仅得到Firefox的支持,下图是Firefox和Chrome两个浏览器的效果:

幸运的是,mask-composite可以实现我们想要的效果。接下来我们来看看这个属性可以取什么值以及它们各自有什么效果?

mask-composite的值及其作用

首先为mask提供了两个渐变的遮罩层(好比文章最开始展示的左侧图)和一张图片(比如小老虎图片)。接下来使用这两个渐变遮罩层来说明mask-composite属性有哪些值,每个值是如何工作的,如下:

--l0: repeating-linear-gradient(90deg, red, red 1em,  transparent 0, transparent 4em);
--l1: linear-gradient(red, transparent);

mask: var(--l1) /* 顶部层(源) */, 
    var(--l0) /* 底部层(目标) */

这两个渐变层的效果如下所示:

--l1是源,--l0是目标。我们将这个遮罩运用在下面这张老虎图上。

接下来看看mask-composite值带来的效果。

add

addmask-composite的初始值(initial),和mask-composite不显式设置值起到的效果相同。在本例中所发生的是将渐变添加到另一个渐变之上,并生成新的遮罩层。

注意,在半透明遮罩层的情况下,不只是简单地添加alpha,不管值的名称是什么。相反,使用下列公式,α₁是顶部层(源),α₀是底部层(目标):

α₁ + α₀ – α₁·α₀

如果至少有一个遮罩层是完全不透明的(它的alpha值是1),那么得到的遮罩就是完全不透明的,并爱理不理遮罩元素的相应像素显示为完全不透明(alpha值为1)。

如果顶层(源)是完全不透明的,即α₁的值是1,如果把该值替换到上面的公式中:

1 + α₀ - 1·α₀ = 1 + α₀ - α₀ = 1

相应的,底层(目标)也是完全不透明的,即α₀的值是1,同样把该值替换到上面的公式中:

α₁ + 1 – α₁·1 = α₁ + 1 – α₁ = 1

当两个遮罩层都是完全透明的(它的alpha的值为0),得到的遮罩就是完全透明的,因此遮罩元素的相应像素也是完全透明的(alpha值为0)。

0 + 0 – 0·0 = 0 + 0 + 0 = 0

下图中,我们可以看到这对于我们正在使用的遮罩图层意味着什么 —— 通过合成得到的图层是什么样子的,以及将它应用到老虎图片上最图得到的效果:

subtract

该值的意思即是从源(顶层)中减去目标(底层)mask-composite取值为subtract时,两个遮罩层合成在一起的计算公式是:

α₁·(1 – α₀)

上面的公式可以得知,任何与0相乘的结果都是0。无论源(顶层)是完全透明的,还是目标层(底层)是完全不透明的,得到的遮罩层也是完全透明的,遮罩元素的相应像素也是完全透明的。

如果源(顶层)是完全透明的,则将公式中的α₁替换为0,得到的值为:

0·(1 – α₀) = 0

如果目标(底层)是完全不透明的,则将公式中的α₀替换为1,得到的值为:

α₁·(1 – 1) = α₁·0 = 0

得到的效果如下:

注意,在本例中,这个价值观式不是对称的,除非α₁α₀是相等的。另外α₁·(1 – α₀)α₀·(1 – α₁)不相同,如果我们交换两个遮罩层,得到的效果将会不一样。

intersect

mask-composite取值为intersect时,对应的公式是两个alpha值相乘:

α₁·α₀

上面公式得到的结果是,无论哪个遮罩层是完全透明的(alpha值为0),计算出来的遮罩层也是完全透明的,遮罩元素的相应像素也是完全透明的。

如果源(顶层)是完全透明的,那么α₁的值为0,将该值替换到上面的公式中,得到的结果是:

0·α₀ = 0

如果目标(底层)是完全透明的,那么α₀的值为0,将该值替换到上面的公式中,得到的结果同样也是0

α₁·0 = 0

另外,如果两个遮罩层都是完全不透明的(它们的alpha值为1),那么计算出来的遮罩层就是完全不透明的,遮罩元素的相应像素也是完全不透明的。这是因为α₁α₀的值都是1,将这两值运用到上面的公式中,得到的结果是1

1·1 = 1

mask-composite:intersect得到的效果如下图所示:

exclude

mask-composite取值为exclude时,两个遮罩层是互斥的,合并遮罩层对应的公式为:

α₁·(1 – α₀) + α₀·(1 – α₁)

实际上,这个公式意味着,无论两个遮罩层都是完全透明的(alpha的值是0)或完全不透明(alpha的值是1),计算出来的遮罩都是完全透明的。遮罩元素的相应像素也是完全透明的。

如果两个遮罩层都是完全透明的,那么α₁α₀的值都是0,将该值相应替换到上面的公式中得到的结果是0

0·(1 – 0) + 0·(1 – 0) = 0·1 + 0·1 = 0 + 0 = 0

如果两个遮罩层都是完全不透明的,那么α₁α₀的值都是1,将该值相应替换到上面的公式中得到的结果也是0

1·(1 – 1) + 1·(1 – 1) = 1·0 + 1·0 = 0 + 0 = 0

它也意味着,只要有一个层是完全透明(alpha值为0),而另一层是完全不透明(alpha值为1),那么计算出来的遮罩层就是完全不透明的,遮罩元素的相应像素也是完全不透明的。

如果源(顶层)完全透明,而目标(底层)完全不透明,那么α₁α₀对应的值分别是01

0·(1 – 1) + 1·(1 – 0) = 0·0 + 1·1 = 0 + 1 = 1

如果源(顶层)完全不透明,而目标(底层)完全透明,那么α₁α₀对应的值分别是10

1·(1 – 0) + 0·(1 – 1) = 1·1 + 0·0 = 1 + 0 = 1

mask-composite:exclude对应的效果如下:

实例

回到上面,采用两个渐变绘制出来想要的菱形图形:

--l1: repeating-linear-gradient(-60deg, transparent 0, transparent 5px, tan 0, tan 35px);
--l0: repeating-linear-gradient(60deg, transparent 0, transparent 5px, tan 0, tan 35px)

如果我们将完全不透明(示例中tan)的部分设置为半透明(rgba(tan, .5)),那么视觉效果就会告诉我们如何进行合成:

$c: rgba(tan, .5);
$sw: 5px;

--l1: repeating-linear-gradient(-60deg, transparent 0, transparent #{$sw}, #{$c} 0, #{$c} #{7*$sw});
--l0: repeating-linear-gradient(60deg, transparent 0, transparent #{$sw}, #{$c} 0, #{$c} #{7*$sw})

html {
    height: 100vh;
    background: var(--l1), var(--l0);
}

得到的效果如下:

我们要研究的菱形区域是在半透明条带的交点处形成的。这意味着使用mask-composite:intersect可以达到我们想要的效果:

$sw: 5px;

--l1: repeating-linear-gradient(-60deg,  transparent 0, transparent #{$sw},  tan 0, tan #{7*$sw});
--l0: repeating-linear-gradient(60deg,  transparent 0, transparent #{$sw},  tan 0, tan #{7*$sw});
mask: var(--l1) intersect, var(--l0)

这不仅给了我们想要的结果,而且,将透明宽度存储到一个变量中,那么将这个值更改为%值(假设$sw:.05%)将使遮罩和图像一起缩放。

如果透明条的宽度是px值,那么菱形和分隔线的大小都保持不变,因为图像会随着视窗进行缩放。

如果透明条宽度是%值,那么菱形和分隔红玫瑰的大小都与图像相关,因此可以随着图像大小进行缩放。

到目前为止,只有Firefox只支mask-composite。但值得庆幸的是,webkit内核提供了一个可替代方案。

扩展支持

webkit内核浏览器使用mask-composite需要添加相应的前缀,而且它需要不同的值才能正常工作:

  • source-over等价于add
  • source-out等价于subtract
  • source-in等价于intersect
  • xor等价于exclude

大家可能会想,只需要额外添加一个webkit版本即可,对吧?事实并没那么简单。首先,我们不能在-webkit-mask简写中使用这个值,比如说,下面的方法是不会起作用的:

-webkit-mask: var(--l1) source-in, var(--l0)

如果要它们起作用,需要把mask-composite单独拿出来,如下:

-webkit-mask: var(--l1), var(--l0);
-webkit-mask-composite: source-in;
mask: var(--l1) intersect, var(--l0)

整个图像完全消失了!

如果你觉得这很奇怪,请检查一下,使用其他三种操作的任何一种,在Webkit内核的浏览器和Firefox中都能得到预期的结果,只有source-in才会破坏Webkit内核浏览器中的东西:

  • add / source-over
  • subtract / source-out
  • exclude / xor

我们也可以把α₀取值为0,可以得到上面三个值得到的最终值是α₁

  • add / source-over: α₁ + 0 – α₁·0 = α₁ + 0 - 0 = α₁
  • subtract / source-out: α₁·(1 – 0) = α₁·1 = α₁
  • exclude / xor: α₁·(1 – 0) + 0·(1 – α₁) = α₁·1 + 0 = α₁

然而,与空无一物相交则是另一回事。与虚无相交就是虚无!这也说明 intersectsource-in操作中可以将α₀设置为0

α₁·0 = 0

在这种情况之下,结果层的alpha值是0,这也就是图像完全被遮上的原因所在。针对这个现象,想到的第一个修复是使用另一种操作来合成底部的层:

-webkit-mask: var(--l1), var(--l0);
-webkit-mask-composite: source-in, xor;
mask: var(--l1) intersect, var(--l0)

这种方法确实有效。最终我们示例的结果如下:

有关于mask-composite的Demo效果除了上面文章中提到的案例之外,在Codepen上还有很多类似的案例,如果感兴趣的话可以猛击这里

最后再次感谢@Ana Tudor为我们提代这么优秀的教程:《Mask Compositing: The Crash Course

小结

这篇文章简单的介绍了CSS中mask的基本原理和使用方法,但是大部分内容介绍了mask-composite属性的使用。通过该文的学习,可以深入的了解到该属性的每个值,以及每个值的计算公式。而且每个属性值所起的效果不同之处。感兴趣的同学,可以自己动手撸几个Demo,如果您在这方面有更好的建议或经验,欢迎在下面的评论中与我们一起分享。文章中有关于mask-composite的原理和公式来自于@Ana Tudor的《Mask Compositing: The Crash Course》一文。最后再次感谢@Ana Tudor为我们提供这么优秀的教程。jordans for sale discount