2018 年初,当我开始更深入地研究 CSS 渐变蒙版以创建有趣的视觉效果时(人们会认为如果不使用单个元素和少量 CSS 则无法实现),我了解到一个以前完全不知道的属性:mask-composite
。
由于这是一个 使用不广泛的属性,因此我找不到任何关于此主题的全面资源。 因此,当我开始更多地使用它并了解更多信息时(有些人可能还记得我在几篇 其他 文章中提到过它),我决定自己创建这样一个资源,于是就有了这篇文章! 在这里,我将介绍 mask-composite
的工作原理、它为何有用、它可以取哪些值、每个值的作用、支持情况以及在不支持的浏览器中的替代方案。
蒙版合成做什么
蒙版合成允许我们使用各种操作将不同的 mask
层组合成一个。 如何组合它们? 逐像素! 让我们考虑两个 mask
层。 我们获取每一对对应的像素,对它们的通道应用特定的合成操作(稍后我们将详细讨论每个可能的运算),并为结果层获取第三个像素。

在合成两层时,顶部的层称为源,底部的层称为目标,对我来说这实际上没什么意义,因为源听起来像输入,目标听起来像输出,但在这种情况下,它们都是输入,输出是我们作为合成操作结果获得的层。

当我们有多个层时,合成分阶段进行,从底部开始。
在第一阶段,从底部数起的第二层是我们的源,从底部数起的第一个层是我们的目标。 这两层被合成,结果成为第二阶段的目标,其中从底部数起的第三层是源。 将第三层与前两层的合成结果进行合成,得到第三阶段的目标,其中从底部数起的第四层是源。

依此类推,直到我们进入最终阶段,其中最顶层的层与下面所有层的合成结果进行合成。
为什么蒙版合成有用
CSS 和 SVG 蒙版都有其局限性、优点和缺点。 我们可以通过使用 CSS 蒙版来规避 SVG 蒙版的局限性,但是,由于 CSS 蒙版的工作方式与 SVG 蒙版不同,因此采用 CSS 路线会让我们无法在没有合成的情况下获得某些结果。
为了更好地理解所有这些,让我们考虑以下西伯利亚虎幼崽的图片

假设我们希望对其获得以下蒙版效果

此特定 mask
保持菱形可见,而分隔它们的线条被蒙版,我们可以透过图像看到后面的元素。
我们还希望此蒙版效果具有灵活性。 我们不希望受图像尺寸或纵横比的限制,并且希望能够轻松地在随图像缩放的 mask
和不随图像缩放的 mask
之间切换(只需将 %
值更改为 px
值即可)。
为此,我们首先需要了解 SVG 和 CSS 蒙版各自的工作原理以及我们可以用它们做什么以及不能做什么。
SVG 蒙版
SVG 蒙版默认情况下是luminance
蒙版。 这意味着与 white
mask
像素对应的被蒙版元素的像素完全不透明,与 black
mask
像素对应的被蒙版元素的像素完全transparent
,而与 mask
像素对应的被蒙版元素的像素在亮度方面介于 black
和 white
之间(grey
、pink
、lime
)是半透明的。
用于从给定 RGB 值中获取亮度的公式为.2126·R + .7152·G + .0722·B
对于我们的特定示例,这意味着我们需要使菱形区域为 white
,将分隔它们的线条设为 black
,从而创建如下所示的图案

为了获得上面的图案,我们从一个 white
SVG 矩形元素 rect
开始。 然后,人们可能会认为我们需要绘制很多黑线……但我们不需要! 相反,我们只需添加一个由该矩形的两条对角线组成的 path
,并确保其 stroke
为 black
。
要创建第一条对角线(左上到右下),我们使用“移动到”(M
)命令到左上角,然后使用“连接到”(L
)命令到右下角。
要创建第二条对角线(右上到左下),我们使用“移动到”(M
)命令到右上角,然后使用“连接到”(L
)命令到左下角。
我们到目前为止的代码是
svg(viewBox=[0, 0, w, h].join(' '))
rect(width=w height=h fill='#fff')
path(d=`M0 0
L${w} ${h}
M${w} 0
L0 ${h}` stroke='#000')
到目前为止的结果似乎与我们想要获得的菱形图案没有任何相似之处……
查看 thebabydino (@thebabydino) 在 CodePen 上的 Pen。
……但这一点即将改变! 我们增加 black
对角线的粗细(stroke-width
),并使它们成为虚线,虚线之间的间隙(7%
)大于虚线本身(1%
)。
svg(viewBox=[0, 0, w, h].join(' '))
rect(width=w height=h fill='#fff')
path(d=`M0 0
L${w} ${h}
M${w} 0
L0 ${h}` stroke='#000'
stroke-width='15%'
stroke-dasharray='1% 7%')
你现在能看出这是怎么回事了吗?
查看 thebabydino (@thebabydino) 在 CodePen 上的 Pen。
如果我们继续增加 black
对角线的粗细(stroke-width
)到 150%
这样的值,那么它们最终会覆盖整个矩形并给我们带来我们一直想要的图案!
查看 thebabydino (@thebabydino) 在 CodePen 上的 Pen。
现在我们可以将 rect
和 path
元素包装在 mask
元素中,并将此 mask
应用于我们希望的任何元素——在我们的例子中,是老虎图像。
svg(viewBox=[0, 0, w, h].join(' '))
mask#m
rect(width=w height=h fill='#fff')
path(d=`M0 0
L${w} ${h}
M${w} 0
L0 ${h}` stroke='#000'
stroke-width='15%'
stroke-dasharray='1% 7%')
img(src='image.jpg' width=w)
img { mask: url(#m) }
以上应该可以工作。 但遗憾的是,在实践中情况并非如此完美。 在这一点上,我们只在 Firefox 中获得了预期的结果(现场演示)。 更糟糕的是,在 Chrome 中没有获得所需的蒙版图案并不意味着我们的元素保持原样未被蒙版——应用此 mask
会使其完全消失! 当然,由于 Chrome 需要 mask
属性(在 HTML 元素上使用时)的 -webkit-
前缀,因此不添加前缀意味着它甚至不会尝试将 mask
应用于我们的元素。
对于 img
元素最直接的解决方法是将它们转换为 SVG image
元素。
svg(viewBox=[0, 0, w, h].join(' ') width=w)
mask#m
rect(width=w height=h fill='#fff')
path(d=`M0 0
L${w} ${h}
M${w} 0
L0 ${h}` stroke='#000'
stroke-width='15%'
stroke-dasharray='1% 7%')
image(xlink:href=url width=w mask='url(#m)')
查看 thebabydino (@thebabydino) 在 CodePen 上的 Pen。
这给了我们我们一直想要的结果,但如果我们想要 mask
另一个 HTML 元素而不是 img
元素,事情就会变得稍微复杂一些,因为我们需要使用 foreignObject
将其包含在 SVG 内。
更糟糕的是,使用此解决方案,我们正在硬编码尺寸,这总是让人感觉不舒服。
当然,我们可以使 mask
大得离谱,这样它不太可能覆盖任何图像。 但这感觉就像硬编码尺寸一样糟糕。
我们还可以尝试通过将 maskContentUnits
切换到 objectBoundingBox
来解决硬编码问题
svg(viewBox=[0, 0, w, h].join(' '))
mask#m(maskContentUnits='objectBoundingBox')
rect(width=1 height=1 fill='#fff')
path(d=`M0 0
L1 1
M1 0
L0 1` stroke='#000'
stroke-width=1.5
stroke-dasharray='.01 .07')
image(xlink:href=url width='100%' mask='url(#m)')
但我们仍然在 viewBox
中硬编码尺寸,虽然它们的实际值并不重要,但它们的纵横比很重要。 此外,我们的蒙版图案现在是在一个 1x1
正方形内创建的,然后拉伸以覆盖被蒙版元素。
形状拉伸意味着形状失真,这就是为什么我们的菱形不再像以前那样看起来的原因。
查看 thebabydino (@thebabydino) 在 CodePen 上的 Pen。
呃。
我们可以调整构成我们路径的两条线的起点和终点
svg(viewBox=[0, 0, w, h].join(' '))
mask#m
rect(width=1 height=1 fill='#fff')
path(d=`M-.75 0
L1.75 1
M1.75 0
L-.75 1` stroke='#000'
stroke-width=1.5
stroke-dasharray='.01 .07')
image(xlink:href=url width='100%' mask='url(#m)')
查看 thebabydino (@thebabydino) 在 CodePen 上的 Pen。
但是,为了获得特定的菱形图案,以及菱形的特定角度,我们需要知道图像的纵横比。
唉。 让我们放弃它,看看我们能用 CSS 做什么。
CSS 遮罩
CSS 遮罩默认情况下是alpha
遮罩。这意味着被遮罩元素的像素对应于完全不透明的mask
像素将完全不透明,被遮罩元素的像素对应于完全透明
的mask
像素将完全透明
,而被遮罩元素的像素对应于半透明的mask
像素将是半透明的。基本上,被遮罩元素的每个像素都会获取对应mask
像素的 alpha 通道。
对于我们的特定情况,这意味着使菱形区域不透明,并使分隔它们的线条透明
,所以让我们看看如何使用 CSS 渐变来实现这一点!
为了获得带有白色
菱形区域和黑色
分隔线的图案,我们可以分层使用两个重复线性渐变
查看 thebabydino 在 CodePen 上创建的 Pen(@thebabydino)。
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
mask
,那么这就是可以完成工作的图案。
但在alpha
mask
的情况下,不是黑色
像素给我们完全透明,而是透明
的像素。并且不是白色
像素给我们完全不透明,而是完全不透明的像素 - 红色
、黑色
、白色
……它们都可以完成工作!我个人倾向于使用红色
或棕褐色
,因为这意味着只需输入三个字母,输入的字母越少,发生糟糕的拼写错误(可能需要半小时才能调试)的机会就越少。
所以第一个想法是应用相同的技术来获得不透明的菱形区域和透明
的分隔线。但这样做,我们遇到了一个问题:第二层渐变的不透明部分覆盖了我们希望保持透明
的第一层的部分,反之亦然。
查看 thebabydino 在 CodePen 上创建的 Pen(@thebabydino)。
所以我们得到的结果与不透明的菱形区域和透明的分隔线相差甚远。
我最初的想法是使用带有白色
菱形区域和黑色
分隔线的图案,并结合将mask-mode
设置为luminance
来解决问题,使 CSS 遮罩像 SVG 遮罩一样工作。
但是,此属性仅受 Firefox 支持,尽管 WebKit 浏览器有非标准的mask-source-type
。遗憾的是,支持甚至不是最大的问题,因为标准的 Firefox 方法和非标准的 WebKit 方法都不能给我们想要的结果(实时演示)。
幸运的是,mask-composite
可以帮上忙!所以让我们看看此属性可以取哪些值以及每个值的效果。
mask-composite
的值及其作用
首先,我们为我们的mask
和我们要遮罩的图像确定两个渐变层。
我们用来说明此属性每个值如何工作的两个渐变mask
层如下所示
--l0: repeating-linear-gradient(90deg,
red, red 1em,
transparent 0, transparent 4em);
--l1: linear-gradient(red, transparent);
mask: var(--l1) /* top (source) layer */,
var(--l0) /* bottom (destination) layer */
这两个层可以在下面的 Pen 中看作是background
渐变(请注意,body
有一个散列的background
,以便透明
和半透明的渐变区域更明显)
查看 thebabydino 在 CodePen 上创建的 Pen(@thebabydino)。
顶部的层(--l1
)是源,底部的层(--l0
)是目标。
我们将mask
应用于这张美丽东北豹的图像。

mask
的图像。好了,现在我们已经解决了这个问题,让我们看看每个mask-composite
值的效果!
add
这是初始
值,它给我们与根本不指定mask-composite
相同的效果。在这种情况下发生的事情是渐变一个叠加在另一个之上,然后应用结果mask
。
请注意,在半透明mask
层的情况下,尽管值名称为“add”,但 alpha 值并不是简单地相加。而是使用以下公式,其中α₁
是源(顶部)层中像素的 alpha,α₀
是目标(底部)层中对应像素的 alpha
α₁ + α₀ – α₁·α₀
在至少一个 mask
层完全不透明(其 alpha 为1
)的地方,结果mask
将完全不透明,并且被遮罩元素的对应像素将完全不透明地显示(alpha 为1
)。
如果源(顶部)层完全不透明,则α₁
为1
,在上面的公式中替换后,我们得到
1 + α₀ - 1·α₀ = 1 + α₀ - α₀ = 1
如果目标(底部)层完全不透明,则α₀
为1
,我们得到
α₁ + 1 – α₁·1 = α₁ + 1 – α₁ = 1
在两个 mask
层都完全透明
(其 alpha 为0
)的地方,结果mask
将完全透明
,因此被遮罩元素的对应像素也将完全透明
(alpha 为0
)。
0 + 0 – 0·0 = 0 + 0 + 0 = 0
下面,我们可以看到这对我们正在使用的mask
层意味着什么 - 合成结果得到的层是什么样子,以及将其应用于我们的东北豹图像产生的最终结果。

mask-composite: add
的效果。subtract
名称指的是从源(上层)中“减去”目标(下层)。同样,这并不意味着简单地截断减法,而是使用以下公式
α₁·(1 – α₀)
上述公式意味着,由于任何与0
相乘的结果都是0
,因此在源(顶部)层完全透明
或目标(底部)层完全不透明的地方,结果mask
也将完全透明
,并且被遮罩元素的对应像素也将完全透明
。
如果源(顶部)层完全透明
,则将其 alpha 在我们的公式中替换为0
,我们得到
0·(1 – α₀) = 0
如果目标(底部)层完全不透明,则将其 alpha 在我们的公式中替换为1
,我们得到
α₁·(1 – 1) = α₁·0 = 0
这意味着使用先前定义的mask
并将mask-composite
设置为subtract
,我们将得到以下结果

mask-composite: subtract
的效果。请注意,在这种情况下,公式不是对称的,因此,除非α₁
和α₀
相等,否则如果我们交换两个 mask 层,我们将不会得到相同的结果(α₁·(1 – α₀)
与α₀·(1 – α₁)
不同)。这意味着如果我们交换两个层的顺序,我们将得到不同的视觉效果!

mask-composite: subtract
。intersect
在这种情况下,我们只看到两个mask
层相交处被遮罩元素的像素。使用的公式是两个层的 alpha 之间的乘积
α₁·α₀
上述公式的结果是,在任何一个 mask
层完全透明
(其 alpha 为0
)的地方,结果mask
也将完全透明
,被遮罩元素的对应像素也是如此。
如果源(顶部)层完全透明
,则将其 alpha 在我们的公式中替换为0
,我们得到
0·α₀ = 0
如果目标(底部)层完全透明
,则将其 alpha 在我们的公式中替换为0
,我们得到
α₁·0 = 0
此外,在两个 mask
层都完全不透明(其 alpha 为1
)的地方,结果mask
将完全不透明,被遮罩元素的对应像素也是如此。这是因为,如果两个层的 alpha 都为1
,我们得到
1·1 = 1
在我们的mask
的特定情况下,设置mask-composite: intersect
意味着我们有

mask-composite: intersect
的效果。exclude
在这种情况下,每个层基本上都从另一个层中排除,公式为
α₁·(1 – α₀) + α₀·(1 – α₁)
在实践中,此公式意味着,在两个 mask
层都完全透明
(其 alpha 为0
)或完全不透明(其 alpha 为1
)的地方,结果mask
将完全透明
,被遮罩元素的对应像素也将完全透明
。
如果两个mask
层都完全透明
,则将它们的 alpha 在我们的公式中替换为0
,结果为
0·(1 – 0) + 0·(1 – 0) = 0·1 + 0·1 = 0 + 0 = 0
如果两个mask
层都完全不透明,则将它们的 alpha 在我们的公式中替换为1
,结果为
1·(1 – 1) + 1·(1 – 1) = 1·0 + 1·0 = 0 + 0 = 0
这也意味着,在其中一层完全透明
(其 alpha 为0
),而另一层完全不透明(其 alpha 为1
)的地方,则结果mask
将完全不透明,被遮罩元素的对应像素也是如此。
如果源(顶部)图层完全透明
,而目标(底部)图层完全不透明,将α₁
替换为0
,并将α₀
替换为1
,则得到
0·(1 – 1) + 1·(1 – 0) = 0·0 + 1·1 = 0 + 1 = 1
如果源(顶部)图层完全不透明,而目标(底部)图层完全透明
,将α₁
替换为1
,并将α₀
替换为0
,则得到
1·(1 – 0) + 0·(1 – 1) = 1·1 + 0·0 = 1 + 0 = 1
使用我们的mask
,设置mask-composite: exclude
意味着我们有

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})
查看thebabydino在CodePen上创建的Pen(@thebabydino)。
我们想要的菱形区域形成于半透明条带的交点处。这意味着使用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)
请注意,我们甚至可以将合成操作包含在简写中!我真的很喜欢这一点,因为越少浪费至少十分钟的时间去理解为什么masj-composite
、msdk-composite
、nask-composite
、mask-comoisite
等等不起作用,就越好!
这不仅给我们带来了期望的结果,而且,如果现在我们将透明
条带宽度存储到一个变量中,将此值更改为%
值(例如$sw: .05%
),则蒙版会随着图像缩放!
如果透明
条带宽度为px
值,则菱形形状和分隔线的大小在图像随视口缩放时保持不变。

透明
分隔线具有px
值的宽度时,在两个不同的视口宽度下的蒙版图像。如果透明
条带宽度为%
值,则菱形形状和分隔线的大小相对于图像,因此会随图像一起缩放。

透明
分隔线具有%
值的宽度时,在两个不同的视口宽度下的蒙版图像。太好了,是真的吗?对此的支持情况如何?
坏消息是,目前只有Firefox支持mask-composite
。好消息是我们有适用于WebKit浏览器的替代方案,因此我们可以扩展支持。
扩展支持
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)
如果我们将合成操作从简写中取出,并在其后编写完整形式,如下所示
-webkit-mask: var(--l1), var(--l0);
-webkit-mask-composite: source-in;
mask: var(--l1) intersect, var(--l0)
……整个图像完全消失了!
如果你认为这很奇怪,请查看:使用其他三个操作中的任何一个add
/ source-over
、subtract
/ source-out
、exclude
/ xor
,我们都可以在WebKit浏览器和Firefox中获得预期的结果。只有source-in
值在WebKit浏览器中导致问题!
查看thebabydino在CodePen上创建的Pen(@thebabydino)。
怎么回事?!
为什么这个特定值会导致WebKit出现问题?
当我第一次遇到这个问题时,我花了几分钟时间试图在source-in
中找到错别字,然后从参考中复制粘贴它,然后从第二个参考中复制粘贴,以防第一个参考出错,然后从第三个参考中复制粘贴……最后我有了另一个想法!
似乎在非标准WebKit替代方案的情况下,我们还在底部图层与其下方的一层空白(被认为完全透明
)之间应用了合成。
对于其他三个操作,这绝对没有任何区别。实际上,添加、减去或排除空白不会改变任何东西。如果我们采用这三个操作的公式,并将α₀
替换为0
,我们总是得到α₁
add
/source-over
:α₁ + 0 – α₁·0 = α₁ + 0 - 0 = α₁
subtract
/source-out
:α₁·(1 – 0) = α₁·1 = α₁
exclude
/xor
:α₁·(1 – 0) + 0·(1 – α₁) = α₁·1 + 0 = α₁
但是,与空白的交集是另一回事。与空白的交集是空白!这也通过在intersect
/ source-in
操作的公式中用0
替换α₀
来证明
α₁·0 = 0
在这种情况下,结果图层的alpha为0
,因此难怪我们的图像会被完全遮罩!
因此,想到的第一个解决方法是使用另一个操作(其他三个中的任何一个都可以,我选择xor
因为它字母较少,并且可以通过双击完全选中)将底部图层与其下方的空白层进行合成
-webkit-mask: var(--l1), var(--l0);
-webkit-mask-composite: source-in, xor;
mask: var(--l1) intersect, var(--l0)
是的,这确实有效!
您可以调整下面嵌入的大小,以查看mask
在随图像缩放和不缩放时如何表现。
查看thebabydino在CodePen上创建的Pen(@thebabydino)。
请注意,我们需要在标准版本之前添加非标准的WebKit版本,以便当WebKit浏览器最终也实现标准版本时,这会覆盖非标准版本。
好吧,就是这样!希望您喜欢这篇文章,并从中学习到新东西。
更多演示
在结束之前,这里还有两个演示展示了为什么mask-composite
很酷。
第一个演示展示了一堆1个元素的雨伞。每个“缺口”都是用一个radial-gradient()
创建的,我们将其从完整的圆形形状中exclude
。Chrome有一点渲染问题,但在Firefox中结果看起来完美无缺。

mask-composite
的1个元素雨伞(实时演示)。第二个演示展示了三个1个元素的加载器(尽管只有后两个使用mask-composite
)。请注意,animation
仅在Chrome中有效,因为它需要Houdini。

mask-composite
的1个元素加载器(实时演示)。您呢——您能想到哪些其他用例?
这太棒了。这让我想知道,与可以做同样事情的图像相比,这是否需要更多的处理能力?
我不知道我不是唯一一个不知道所有CSS属性的人,这让我在几年后重新学习CSS时感觉更舒服。很多东西都变了。我对Flex和Grid非常好奇,听起来很有趣。
感谢您的教程!你们这里有很棒的新设计!
我会用以下方式解释源和目标的命名
您只有一个正在使用的“主要”蒙版。该蒙版可以使用其他蒙版进行修改,但是,在每个步骤中,您无需获取蒙版的新实例,而是就地修改现有的蒙版。
因此,(用您的话说)“顶部”蒙版是“底部”蒙版(即主要蒙版)修改的来源,最终将应用该修改。