今年早些时候,我偶然发现了 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}
- });
由于将文本拆分为多个元素可能无法与屏幕阅读器很好地配合使用,因此我们为整个内容提供了 role
为 image
和 aria-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;
}

width
,而是放置在中间。我们也可以添加一些美化效果,例如容器上的漂亮 font
或 background
。
接下来,我们使用一个绝对定位的 ::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
布局,其中标题在水平方向上居中对齐,这意味着视窗的垂直中线与标题的垂直中线重合,将两者都分成两个宽度相等的半部分。
因此,视窗左边缘和标题右边缘之间的距离是视窗宽度的一半(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 轴的负方向,所以符号将是减号)移动其完整的 width
(100%
),然后才允许它回到其正常位置。
@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
。

<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 效果! 但是请注意,这里使用的字体是经过选择的,以便我们的结果看起来不错,而其他字体可能效果不佳。
还有其他人十分钟前认为 3D 穿线是不可能的嗎? 旋转字母的取消压缩太天才了。
Ana 的作品总是让我惊叹不已。
嗨
我只是想通知你,我的一位前学生(他的名字叫 Côme Gaillard)重写了这个,没有使用任何库,只是纯 HTML-CSS-JS,我认为你会感兴趣的。
在 Firefox 上运行 3D 版本时,红色的线会滑过文本,只有在动画结束时才会切换到穿过字母。 我想知道这个错误是在浏览器中还是在演示中。
哦,哇,我认为这可能比仅仅是浏览器或演示错误更复杂…
你在哪个版本的 Firefox 和哪个操作系统上看到这个错误? 我刚刚在 Windows 10 上的 Nightly 和稳定版中再次测试,它们都按预期工作,线在滑动到右侧时穿过字母,而不是在动画结束时。
我在 Windows 10 上使用 73.0.1,配备 GeForce GTX 750 Ti。 查看我的“支持信息”,可能存在驱动程序问题,但我不知道为什么这会导致这个特定问题。
嗯,重新启动浏览器使其正常工作。由于它最初是从远程桌面连接启动的,我认为这是远程桌面使用的视频驱动程序中或 Firefox 在无法使用硬件加速时设置其合成器的方式中存在的问题。
虽然我喜欢这个演示,但我发现上面那句话读起来有点讽刺,或者是不小心自相矛盾,因为这种方法做的事情本质上与它声称不会做的事情完全一样:使用库将文本拆分为多个单独的 span。它只是将执行成本从客户端设备/浏览器转移到了服务器端预渲染进程。
这是在客户端运行的小片段 JavaScript 代码的执行成本与客户端从将所有非常小的 HTML 片段扩展成更长的 span 集群(在它们被发送到网络之前)中下载更多数据位的开销之间永恒的斗争。在这种情况下,大多数情况下,权衡可能有利于预渲染,但 HTML 能量的节省意味着某人在某个地方必须运行代码才能进行这种渲染......
库在哪里?我根本没看到任何提及,也不会将一小段手写脚本归类为库。
好工作!3D 版本高度依赖于所使用的字母。您可以在感叹号上看到问题,其中线穿过它而不是绕过它。
如果我们使用等宽字体或所谓的固定宽度字体,可以用两条虚线创建类似的效果。
线条是用相位偏移和
repeating-linear-gradient
绘制的,颜色停留在1ch
(对应于等宽字体中字母/字符的宽度)。将一条虚线放在文本下方,另一条放在文本上方,会产生连续线的错觉。理论上,任何等宽字体都应该在这个演示中工作。我已经测试了一些字体:PT Mono、Roboto Mono、Nova Mono 和 Courier Prime。
https://fonts.google.com/?category=Monospace