对比文本与背景的方法

Avatar of Ana Tudor
Ana Tudor

DigitalOcean 为您的每个旅程阶段提供云产品。立即开始使用 200 美元的免费积分!

这始于看到 Mandy Michael 最近的一篇 Pen 关于 文本效果演示。我是一个视觉动物,所以我首先注意到的是效果,而不是标题(标题清楚地说明了效果是如何实现的)。瞬间,我的脑海里就浮现出“混合模式!”,结果证明这是错误的。

演示实际上使用的是 clip-path。首先,文本被复制。我们有一个黑色文本在下层作为元素的实际文本内容,有一个白色文本在上层作为 content 属性的值(取自一个通过 JS 更新的数据属性)。这两个文本彼此叠加(完全重叠)。然后,带有白色文本的伪元素被剪裁成黑色连衣裙的形状。

但是,这意味着如果我们更改图像,我们需要更改剪裁路径,而且在这一点上,使用开发者工具找出具有许多点的多边形剪裁路径并非易事(这就是为什么像 Benett Feely 的 Clippy 这样的工具在开发者工具中直接进行双向编辑会非常有用)。因此我决定尝试一下我最初的想法——混合模式。

假设我们有一个 contentEditable1 容器中的标题,带有黑白图像(即灰度)background。HTML 结构如下

<header>
  <h2 contentEditable role='textbox' aria-multiline='true'>And stay alive</h2>
</header>

我们在 header 容器上设置 background-image,将 h2 文本设置为白色并设置其 mix-blend-modedifferenceexclusion。相关的 CSS 如下

header { background: url(black-and-white-image.jpg) }

h2 {
  color: white;
  mix-blend-mode: difference;
}

我不能真正理解混合模式,也通常看不出 differenceexclusion 之间的区别,但是 根据 MDN 的说法,它们基本上是一样的,exclusion 的对比度更低。我能理解这种情况是,在图像上使用白色文本会得到一个结果,即图像在文本重叠的地方被反转。对于简单的黑白图像,原始图像中的黑色在白色文本覆盖的地方变为白色,原始图像中的白色在白色文本覆盖的地方变为黑色。

结果可以在下面的 Pen 中看到

查看 thebabydino 的 Pen (@thebabydino) 在 CodePen 上。

任务完成!文本和图像都可以更改,效果保持不变,无需任何 JavaScript 或任何 CSS 更改。

但我立即发现另一个需要解决的问题:如果图像不仅仅是黑白怎么办?好吧,让我们试试!结果证明,结果实际上看起来还不错

查看 thebabydino 的 Pen (@thebabydino) 在 CodePen 上。

但是,文本不再是灰度的,设置 filter: grayscale(1) 不会改变任何东西。是否有任何 filter 值有效?好吧,接下来尝试了 drop-shadow() 来找到这个问题的答案。答案是,是的,drop-shadow() 有效,但它的工作方式——阴影与 header 背景2 混合——提供了一个线索,说明为什么 grayscale() 不会改变任何东西:过滤器是在混合之前应用的,我们的文本是白色的,grayscale() 在这种情况下输出与其输入相同(白色输入,白色输出)。而且,由于在混合之前没有任何变化,因此其结果也是一样的。

查看 thebabydino 的 Pen (@thebabydino) 在 CodePen 上。

这可能不应该让人感到意外。我之前遇到过因属性应用顺序导致的问题。

例如,filter 也是在 clip-path 之前应用的,所以如果我们把一个元素剪裁成非矩形形状,并且我们想在这个元素上添加一个投影,那么很不幸,在同一个元素上设置 filter 不会得到预期的结果。这是因为矩形元素被应用了 filter,因此我们会在其矩形框周围有一个投影,并且 仅此之后 才会剪裁成通过 clip-path 指定的形状。无论你在 CSS 中设置它们的顺序如何,这两个属性的应用顺序始终如此。

Diagram. A box representing an element (left) 
has a CSS filter applied to become a box with a box-shadow (center) then has a clip-path applied to 
become a star with no shadow (right).
浏览器在同一个元素上设置 filterclip-path 时应用方式的示意图。

解决 filter + clip-path 问题的办法是,在父元素上设置 filter,并且父元素除了被剪裁的子元素之外没有任何可见内容。这里的“没有任何可见内容”很重要,因为如果父元素有一些可见的文本或边框,它们也会得到投影,而如果父元素有背景,那么整个背景区域都会得到投影。

查看 thebabydino 的 Pen (@thebabydino) 在 CodePen 上。

但是,这个解决方案不适用于 filter + mix-blend-mode 问题。将我们的 h2 包裹在一个 div 中,并在该 div 上设置 filter 会破坏混合效果。

查看 thebabydino 的 Pen (@thebabydino) 在 CodePen 上。

更糟糕的是,我似乎无法想到任何可以解决这个问题的方法。可能可以通过复制文本并使用 luminosity 混合模式来实现,但是,正如前面提到的,我不太理解混合模式,而且我还没有能做到这一点。

但也许我们可以尝试完全不同的方法,一种不使用混合的方法。所有关于过滤器的尝试让我想到,我们可以将图像应用于文本,然后应用 invert() 过滤器,然后可以将 grayscale()contrast() 等与之串联。

使用 background-clip: text 的这种方法,我们还有一个跨浏览器解决方案的优势,因为 Firefox 和 Edge 现在也实现了它。

我们的方法如下:确保 headerh2 有相同的背景,并且这些背景完全重叠。然后,在 h2 上设置 color: transparent,并将它的背景剪裁为 text。最后一步是在 h2 上设置 filter: invert(1)。相关的 CSS3 如下

h2 {
  background: inherit;
  background-clip: text;
  color: transparent;
  filter: invert(1);
}

结果可以在下面的 Pen 中看到

查看 thebabydino 的 Pen (@thebabydino) 在 CodePen 上。

它看起来和以前一样,只是使用了不同的方法,现在我们可以将更多函数与 filter 属性串联。例如,我们可以将文本设置为灰度并提高其对比度

filter: invert(1) grayscale(1) contrast(9)

查看 thebabydino 的 Pen (@thebabydino) 在 CodePen 上。

调整 filter 值也是我们添加文本阴影的方式。虽然在第一种情况下(使用 mix-blend-mode)我们可以简单地设置 text-shadow 属性(并且阴影也会与背景混合),但在第二种情况下这样做会导致问题

查看 thebabydino 的 Pen (@thebabydino) 在 CodePen 上。

幸运的是,我们可以将 drop-shadow() 与我们的过滤器串联。

查看 thebabydino 的 Pen (@thebabydino) 在 CodePen 上。

下面的 Pen 展示了本文中描述的两种方法。左边是 mix-blend-mode 方法,右边是 background-clipfilter 方法(包含额外的灰度和对比度组件)。文本是可编辑的,缩略图允许更改 background-image

查看 thebabydino 的 Pen (@thebabydino) 在 CodePen 上。

所以,如果我们比较这两种方法,哪种更好呢?

我们不会谈论美观问题,因为这是一个主观问题,而且它可能因情况而异,甚至因心情而异。

就基本支持而言,最后一种方法更占优势,因为它在 Edge 中得到支持,而 mix-blend-mode 则没有(但如果你希望它在 Edge 中可用,请 为它投票,因为 您的反馈很重要)。Mandy 最初的示例使用的是 clip-path,这是另一个非常有用的功能,但不幸的是,它在 Edge 中也不起作用(你可以 在这里为它投票)。

当涉及到选择文本时,mix-blend-mode 方法可以完美地工作,并且在它支持的浏览器中看起来最好。

The editable text, made to contrast with the background image using the mix-blend-mode method, shown selected in its entirety. The selection background is blended with the image underneath.
使用 mix-blend-mode 方法选择文本

使用 clip-path 方法,选择看起来很干净,文本仍然可读,但在我使用伪元素文本时,它似乎在 Firefox 中出现问题。幸运的是,这个问题有一个简单的解决方法:在伪元素上设置 pointer-events: none

Animated gif. The editable text, made to contrast with the background image using the clip-path method, gets selected in Firefox. The selection stumbles where the copy of this text, generated via a pseudo-element, overlaps the original. Setting pointer-events: none on the pseudo-element fixes this problem.
使用 clip-path 方法在 Firefox 中选择文本

至于最后一种方法,选择看起来可能很丑,并且可能使文本更难阅读。更不用说,在这种情况下,投影最终被应用于整个选择矩形,而不仅仅是实际文本。

The editable text, made to contrast with the background image using the background-clip: text + filter method, shown selected in its entirety. The whole selection box has the filter effect applied, not just the text, so the selection background is grayscale and the drop-shadow is now on the selection box, not just on the text itself.
使用 background-clip: text + filter 方法选择文本

使用最后一种方法在 Firefox 中更改文本也会变得很麻烦,因为当我键入新文本时,屏幕会变得“脏”。

Animated gif. The editable text, made to contrast with the background image using the background-clip: text + filter method, gets edited in Firefox. As you write, the text becomes clipped and garbled, overlapping itself.
使用 background-clip: text + filter 方法在 Firefox 中更改文本

clip-path 方法最初在 Firefox 中更改文本时也存在问题:尝试从伪元素文本的中间开始编辑不起作用。幸运的是,在伪元素上设置 pointer-events: none 也可以解决这个问题。

Animated gif. The editable text, made to contrast with the background image using the clip-path method, gets edited in Firefox. The cursor cannot be placed to start editing from the part where the copy of this text, generated via a pseudo-element, overlaps the original. Setting pointer-events: none on the pseudo-element fixes this problem.
使用 clip-path 方法在 Firefox 中更改文本

就文本的改变灵活性而言,最后一种方法表现最好,因为将过滤器函数串联起来可以让我们走很远。

最终,哪种方法更好取决于具体的用例。应该支持哪些浏览器?图像是否会更改?文本是否会更改?文本应该如何进行视觉调整?


1 contentEditable 默认情况下并非所有浏览器都支持,因此我们需要提供语义来解决这个问题。

2 在尝试混合模式时,请注意它们可能产生的可读性问题,特别是关于对比度的。 WCAG 2.0 规定,除非文本构成纯粹的装饰性图像的一部分,否则它应该通过最低对比度阈值。 像 Color ContrastAnalyzer 这样的工具可以帮助您满足该要求。

3 为了简洁起见,省略了前缀,但 background-clip 仍然需要 -webkit- 前缀才能在 WebKit 浏览器(Firefox 和 Edge 已实现它没有前缀,尽管 它们都支持 -webkit- 前缀,可能是因为 -webkit- 前缀已经被用烂了,并且 使用了该前缀),并且需要手动添加此前缀或通过预处理器混合,这是我通常不鼓励的,但在这种特殊情况下,通过 AutoprefixerPrefixfree 的自动前缀添加不会发生(Prefixfree 通过功能检测工作,它不会捕获 background-clipfilterclip-path 等属性,这是 Autoprefixer 问题)。至于 filter,它现在 支持 在所有当前的桌面浏览器中没有前缀,Autoprefixer 可以为它添加前缀。