在 CSS 中用线穿文本

Avatar of Ana Tudor
Ana Tudor

DigitalOcean 提供适用于旅程各个阶段的云产品。 立即开始使用 价值 200 美元的免费信用额度!

今年早些时候,我偶然发现了 Florin Pop 的 这个演示,它可以让一条线在单行标题的字母上方或下方穿过。 我觉得这是一个很酷的想法,但在实现过程中,我发现一些小细节可以同时简化和改进。

首先,原始演示复制了标题文本,我知道这很容易避免。 然后是穿过文本的线的长度是一个神奇的数字,这并不是一个非常灵活的方法。 最后,我们不能摆脱 JavaScript 吗?

所以让我们看看我最终是如何实现这个功能的。

HTML 结构

Florin 将文本放入标题元素中,然后复制该标题,使用 Splitting.js 将复制标题的文本内容替换为 span,每个 span 包含原始文本中的一个字母。

已经决定不复制文本,使用库将文本拆分为字符,然后将每个字符放入 span 感觉有点过分,所以我们用 HTML 预处理器来完成所有操作。

- let text = 'We Love to Play';
- let arr = text.split('');

h1(role='image' aria-label=text)
  - arr.forEach(letter => {
    span.letter #{letter}
  - });

由于将文本拆分为多个元素可能无法与屏幕阅读器很好地配合使用,因此我们为整个内容提供了 roleimagearia-label

这将生成以下 HTML

<h1 role="image" aria-label="We Love to Play">
  <span class="letter">W</span>
  <span class="letter">e</span>
  <span class="letter"> </span>
  <span class="letter">L</span>
  <span class="letter">o</span>
  <span class="letter">v</span>
  <span class="letter">e</span>
  <span class="letter"> </span>
  <span class="letter">t</span>
  <span class="letter">o</span>
  <span class="letter"> </span>
  <span class="letter">P</span>
  <span class="letter">l</span>
  <span class="letter">a</span>
  <span class="letter">y</span>
</h1>

基本样式

我们使用 grid 布局将标题放置在其父元素(在本例中为 body)的中间。

body {
  display: grid;
  place-content: center;
}
Screenshot of grid layout lines around the centrally placed heading when inspecting it with Firefox DevTools.
标题不会横跨其父元素以覆盖其整个 width,而是放置在中间。

我们也可以添加一些美化效果,例如容器上的漂亮 fontbackground

接下来,我们使用一个绝对定位的 ::after 伪元素创建一条线,其厚度(height)为 $h

$h: .125em;
$r: .5*$h;

h1 {
  position: relative;
  
  &::after {
    position: absolute;
    top: calc(50% - #{$r}); right: 0;
    height: $h;
    border-radius: 0 $r $r 0;
    background: crimson;
  }
}

以上代码处理了伪元素的定位和 height,但 width 呢? 我们如何让它从视窗的左边缘延伸到标题文本的右边缘?

线长

好吧,由于我们有一个 grid 布局,其中标题在水平方向上居中对齐,这意味着视窗的垂直中线与标题的垂直中线重合,将两者都分成两个宽度相等的半部分。

SVG illustration. Shows how the vertical midline of the viewport coincides with that of the heading and splits both into equal width halves.
居中对齐的标题。

因此,视窗左边缘和标题右边缘之间的距离是视窗宽度的一半(50vw)加上标题宽度的一半,这可以用 % 值表示,当用于计算其伪元素的 width 时。

所以我们的 ::after 伪元素的 width

width: calc(50vw + 50%);

让线在上方和下方穿过

到目前为止,结果只是一条红色的线穿过一些黑色的文本。

我们想要的是让一些字母显示在线的上面。 为了获得这种效果,我们随机地为它们(或不为它们)添加一个 .over 类。 这意味着稍微修改 Pug 代码。

- let text = 'We Love to Play';
- let arr = text.split('');

h1(role='image' aria-label=text)
  - arr.forEach(letter => {
    span.letter(class=Math.random() > .5 ? 'over' : null) #{letter}
  - });

然后我们相对定位具有 .over 类的字母,并为它们提供一个正的 z-index

.over {
  position: relative;
  z-index: 1;
}

我最初的想法是使用 translatez(1px) 而不是 z-index: 1,但后来我意识到使用 z-index 具有更好的浏览器支持,而且工作量更少。

线穿过一些字母,但在其他字母下方。

动画效果!

现在我们已经克服了棘手的部分,我们也可以添加一个 animation 来让线进入。 这意味着让红色的线在开头向左(在 x 轴的负方向,所以符号将是减号)移动其完整的 width100%),然后才允许它回到其正常位置。

@keyframes slide { 0% { transform: translate(-100%); } }

我选择在 animation 开始之前留一些喘息时间。 这意味着添加 1s 延迟,进而意味着为 animation-fill-mode 添加 backwards 关键字,这样线在 animation 开始之前将保持 0% 关键帧指定的狀態。

animation: slide 2s ease-out 1s backwards;

3D 触控

这样做给了我另一个想法,那就是让线穿过每个字母,也就是说,从字母上方开始,穿过它,然后在下方结束(反之亦然)。

这需要真正的 3D 和一些小的调整。

首先,我们在标题上将 transform-style 设置为 preserve-3d,因为我们希望其所有子元素(和伪元素)成为同一个 3D 组件的一部分,这将使它们根据它们在 3D 中的定位进行排序和相交。

接下来,我们希望围绕每个字母的 y 轴旋转,旋转方向取决于随机分配的类(我们将其名称从“over”更改为“rev”,因为“over”并不能真正暗示我们在这里做什么)。

但是,在我们这样做之前,我们需要记住我们的 span 元素仍然是内联元素,在内联元素上设置 transform 没有任何效果。

为了解决这个问题,我们在标题上设置了 display: flex。 但是,这会创建一个新问题,那就是包含单个空格(" ")的 span 元素会被压缩为零 width

Screenshot showing how the span containing only a space gets squished to zero width when setting `display: flex` on its parent.
在 Firefox DevTools 中检查一个仅包含空格的 <span>

一个简单的解决方法是在我们的 .letter span 上设置 white-space: pre

完成此操作后,我们可以将 span 旋转 $a 角… 向一个方向或另一个方向旋转!

$a: 2deg;

.letter {
  white-space: pre;
  transform: rotatey($a);
}

.rev { transform: rotatey(-$a); }

由于围绕 y 轴的旋转会水平压缩我们的字母,我们可以沿 x 轴按 $a 的余弦倒数($f)进行缩放。

$a: 2deg;
$f: 1/cos($a)

.letter {
  white-space: pre;
  transform: rotatey($a) scalex($f)
}

.rev { transform: rotatey(-$a) scalex($f) }

如果你想了解使用这种特定缩放因子的原因,可以查看这篇旧的 文章,我在其中详细解释了所有内容。

就这样! 我们现在已经得到了我们想要的 3D 效果! 但是请注意,这里使用的字体是经过选择的,以便我们的结果看起来不错,而其他字体可能效果不佳。