CSS Cyberpunk 2077 按钮 - 带你的 CSS 去夜之城
如果你对电子游戏稍有兴趣,你肯定对《赛博朋克 2077》有所耳闻。它是 2020 年最受期待的游戏之一。它所描绘的世界独具风格。游戏官网出色地展现了这种美感。其设计在传达游戏观感方面也做得非常出色。你可以想象,这意味着一些界面元素看起来相当精致。
有人先联系我,问我如何创建网站上使用的图像效果。如果你把鼠标悬停在图片库里的图片上,它们就会呈现出这种漂亮的“噪点”效果。

我接受了挑战。我深入研究了网站的源代码。一番挖掘之后,我发现它是用着色器和 WebGL 实现的。我对编写着色器和 WebGL 完全陌生。这确实激励我尝试一下。但是,目前,我暂时把学习 WebGL 和着色器代码的工作放在了一边。
当我继续浏览我的直播网站时,真正吸引我们眼球的是那些漂亮的故障效果按钮。我对用 CSS 创建故障效果并不陌生。我们决定尝试重现它们。

以下是具体操作方法!
让我们从一些标记开始
<button class="cybr-btn">
Beginning_
</button>
我们首先需要解决的是大小、颜色和字体问题。最好的解决方法是什么?深入研究源代码,看看它是如何实现的。从第一次检查开始,我们就发现它使用了自定义字体。
让我们抓住字体“Blender Pro Bold”字体并创建自定义@font-face
规则。
@font-face {
font-family: Cyber;
src: url("https://assets.codepen.io/605876/Blender-Pro-Bold.otf");
font-display: swap;
}
一旦有了这些,我们就可以设置基本的样式了。使用 CSS 变量来设置颜色和字体大小等属性,可以为我们后续提供更多可能性。这也是使用 HSL 颜色空间的原因。我们稍后会解释原因。
--primary: hsl(var(--primary-hue), 85%, calc(var(--primary-lightness, 50) * 1%));
--shadow-primary: hsl(var(--shadow-primary-hue), 90%, 50%);
--primary-hue: 0;
--primary-lightness: 50;
--color: hsl(0, 0%, 100%);
--font-size: 26px;
--shadow-primary-hue: 180;
把这些放在一起,我们就能找到这个起点。注意到我们用的是 inset box-shadow 而不是 border 来表示那条蓝线吗?这是因为 border 会使文本偏离中心。inset box-shadow 不会影响文本对齐。
这个按钮的一个显著特点是那个被剪裁过的角。我最初的想法是使用 clip-path。但令我惊讶的是,网站上按钮的形状是用 background-image 实现的。
我们可以使用 来剪切角落clip-path
。
clip-path: polygon(-10% -10%, 110% -10%, 110% 110%, 10% 110%, -10% 40%);
请注意,我们没有裁剪按钮的边缘。我们给按钮留出了 10% 的浮动空间。这是因为我们需要考虑“R25”标签,以及故障效果溢出按钮的情况。这是一个巧妙的技巧clip-path
。我们可以将其用作受控的overflow: hidden
。我们说:“是的,你可以稍微溢出一点。但只能溢出这么多。”
将其添加到我们的按钮上可以实现我们想要的剪辑效果。
接下来,让我们创建“R25”标签。我们可以在这里使用伪元素并使用 content 属性。事实上,网站上就是这样做的。不过,这种方法需要注意一些事项。屏幕阅读器可能会读出它。实际的按钮文本也是如此。网站上的每个按钮的文本后都带有下划线。我们希望屏幕阅读器读出这些文本吗?如果是,那么我们可以保持原样。我们假设它们是为了装饰目的。我们可以更新标记并使用,aria-hidden
以便屏幕阅读器只读取按钮的文本。
<button class="cybr-btn">
Clipped<span aria-hidden="true">_</span>
<span aria-hidden="true" class="cybr-btn__tag">R25</span>
</button>
要为标签添加样式,我们可以为其指定absolute
定位。这需要我们relative
在按钮上设置定位。与按钮本身一样,该标签也使用inset box-shadow
。
.cybr-btn {
--label-size: 9px;
--shadow-secondary-hue: 60;
--shadow-secondary: hsl(var(--shadow-secondary-hue), 90%, 60%);
position: relative;
}
.cybr-btn__tag {
position: absolute;
padding: 1px 4px;
letter-spacing: 1px;
line-height: 1;
bottom: -5%;
right: 5%;
background: var(--shadow-secondary);
color: hsl(0, 0%, 0%);
font-size: var(--label-size);
box-shadow: 2px 0 inset var(--shadow-primary);
}
我们在这里引入了一些 CSS 变量。虽然它们被标签使用,但我们将它们放在了按钮选择器下。这样做是有原因的。我们以后可能会决定利用作用域变量的功能。如果这样做,我们只需要在按钮选择器上设置变量。如果我们将变量保留在标签规则下,那么设置在按钮上的变量将无法在更低的作用域上生效。我们background-color
为标签设置了 。但是,很快就会发现,网站上并没有这样做。
有了标签,按钮现在就开始成形了。
是时候添加故障效果了。根据经验,我假设按钮是重复的。重复的按钮会应用某种形式的剪切动画。我们的首要任务是创建故障效果主体。还记得我们之前发现的背景图片的用途吗?很快就明白了为什么使用它。它用于为标签提供剪切区域。这意味着background-color
按钮的背面与标签相同。角的剪切也是用图片创建的。
注意到蓝色边框是如何沿着角落绕过“R25”的吗?我们使用 clip-path 切掉了那个角,并且没有勾勒出“R25”的轮廓。该网站的实现使用了drop-shadow
。
使用背景图片可以让我们重现这种效果。但是,如果我们想让按钮更灵活、可重复使用,就需要做出一些妥协。
例如,如果我们想改变按钮的颜色怎么办?我们是否需要为每种按钮颜色创建多张图片?如果我们改变了按钮的长宽比怎么办?图片就不再适合了。
动画虽然有点小故障,但速度很快。速度足够快,所以剪角几乎不会被察觉。为了获得更灵活、更易复用的样式,这点牺牲是值得的。
让我们继续这个解决方案。我们可以为这个故障添加一个新元素。它需要与按钮相同的文本,并且需要使用 aria-hidden 隐藏在屏幕阅读器中。
<button class="cybr-btn">
Glitch<span aria-hidden>_</span>
<span aria-hidden class="cybr-btn__glitch">Glitch_</span>
<span aria-hidden class="cybr-btn__tag">R25</span>
</button>
我们需要在这里复制文本,并且我们有一些选择。该网站使用伪元素来复制文本。但是,如果我们这样做,就意味着需要同时为两个元素设置动画才能达到效果。通过将文本移动到 glitch 元素中,我们只需要为一个元素设置动画。
.cybr-btn__glitch {
position: absolute;
height: 100%;
width: 100%;
top: 0;
left: 0;
box-shadow: 0 0 0 4px var(--shadow-primary);
text-shadow: 2px 2px var(--shadow-primary), -2px -2px var(--shadow-secondary);
}
应用一些样式,例如text-shadow
,让box-shadow
我们来到这里。
但是,我们对这种角裁剪并不满意。而且,我们用 来提供clip-path
“呼吸空间”的方式感觉不太好。我们可以用一个小技巧来解决这个问题。如果我们使用伪元素来给按钮着色,就不必裁剪整个按钮了!我们可以使用绝对定位,然后只裁剪伪元素。我们也不需要提供“呼吸空间”。这里还有一个好处,那就是我们已经在变量中设置了按钮的颜色。
.cybr-btn {
--clip: polygon(0 0, 100% 0, 100% 100%, 8% 100%, 0 70%);
}
.cybr-btn:before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: var(--primary);
clip-path: var(--clip);
z-index: -1;
}
我们可以从按钮中移除clip-path
,并将该剪辑放入可重复使用的变量中。我们需要将其应用于z-index: -1
伪元素,以便文本仍然显示。
.cybr-btn {
--border: 4px;
}
.cybr-btn__glitch {
position: absolute;
top: calc(var(--border) * -1);
left: calc(var(--border) * -1);
right: calc(var(--border) * -1);
bottom: calc(var(--border) * -1);
background: var(--shadow-primary);
text-shadow: 2px 2px var(--shadow-primary), -2px -2px var(--shadow-secondary);
clip-path: var(--clip);
}
.cybr-btn__glitch:before {
content: '';
position: absolute;
top: calc(var(--border) * 1);
right: calc(var(--border) * 1);
bottom: calc(var(--border) * 1);
left: calc(var(--border) * 1);
clip-path: var(--clip);
background: var(--primary);
z-index: -1;
}
然后,我们可以将这个剪辑复用到 glitch 元素的伪元素上。让 glitch 元素正确定位的技巧是将其绝对定位,就像边框一样。然后将伪元素叠加到它上面。将相同的剪辑应用于两个元素,就能得到一个沿着角点的整齐的蓝色边框。
这给了我们:
这有多棒?我们甚至可以调整 clip-path 来剪切“R25”周围的内容。如果我们调整clip-path
并删除标签样式:
.cybr-btn {
--clip: polygon(0 0, 100% 0, 100% 100%, 95% 100%, 95% 90%, 85% 90%, 85% 100%, 8% 100%, 0 70%);
}
.cybr-btn__tag {
position: absolute;
padding: 1px 4px;
letter-spacing: 1px;
line-height: 1;
bottom: -5%;
right: 5%;
color: hsl(0, 0%, 0%);
font-size: var(--label-size);
}
我们得到如下结果:
这正是我们有机会做一些更酷的事情的地方。当我检查按钮并发现背景图片时,我把它拉了下来。我发现,通过堆叠两张图片并平移底部的图片,可以实现边框。现在,我们使用了clip-path
,我们可以做同样的事情。
如果我们用:before
伪元素来表示按钮的蓝色,用:after
表示红色。然后,我们:before
根据边框大小平移伪元素,它就会显示边框。这样就不用应用 了border
。
.cybr-btn:after,
.cybr-btn:before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
clip-path: var(--clip);
z-index: -1;
}
.cybr-btn:before {
background: var(--shadow-primary);
transform: translate(var(--border), 0);
}
.cybr-btn:after {
background: var(--primary);
}
现在,我们为标签和按钮添加了阴影。标签将使用其背后的背景色。尝试将 更改background-color
为body
,您就会看到效果!
快完成了!坚持住。故障已经解决。所有需要的东西都准备好了。剩下的就是让它动起来:hover
。
这种故障效果是怎么产生的?诀窍在于只显示故障元素,:hover
并默认应用动画。我假设是在一组关键帧中使用transform
和clip-path
。结果证明我是对的!我是怎么发现的呢?检查按钮,并使用 Chrome 的“强制状态”将按钮设置为该:hover
状态。
然后,检查样式并找到动画。点击文件名,即可跳转到源文件。
这让我能够看到正在使用的关键帧。
@keyframes glitch-anim-1 {
0% {
opacity: 1;
-webkit-transform: translateZ(0);
transform: translateZ(0);
-webkit-clip-path: polygon(0 2%,100% 2%,100% 5%,0 5%);
clip-path: polygon(0 2%,100% 2%,100% 5%,0 5%)
}
2% {
-webkit-clip-path: polygon(0 78%,100% 78%,100% 100%,0 100%);
clip-path: polygon(0 78%,100% 78%,100% 100%,0 100%);
-webkit-transform: translate(-5px);
transform: translate(-5px)
}
6% {
-webkit-clip-path: polygon(0 78%,100% 78%,100% 100%,0 100%);
clip-path: polygon(0 78%,100% 78%,100% 100%,0 100%);
-webkit-transform: translate(5px);
transform: translate(5px)
}
8% {
-webkit-clip-path: polygon(0 78%,100% 78%,100% 100%,0 100%);
clip-path: polygon(0 78%,100% 78%,100% 100%,0 100%);
-webkit-transform: translate(-5px);
transform: translate(-5px)
}
9% {
-webkit-clip-path: polygon(0 78%,100% 78%,100% 100%,0 100%);
clip-path: polygon(0 78%,100% 78%,100% 100%,0 100%);
-webkit-transform: translate(0);
transform: translate(0)
}
10% {
-webkit-clip-path: polygon(0 54%,100% 54%,100% 44%,0 44%);
clip-path: polygon(0 54%,100% 54%,100% 44%,0 44%);
-webkit-transform: translate3d(5px,0,0);
transform: translate3d(5px,0,0)
}
13% {
-webkit-clip-path: polygon(0 54%,100% 54%,100% 44%,0 44%);
clip-path: polygon(0 54%,100% 54%,100% 44%,0 44%);
-webkit-transform: translateZ(0);
transform: translateZ(0)
}
13.1% {
-webkit-clip-path: polygon(0 0,0 0,0 0,0 0);
clip-path: polygon(0 0,0 0,0 0,0 0);
-webkit-transform: translate3d(5px,0,0);
transform: translate3d(5px,0,0)
}
15% {
-webkit-clip-path: polygon(0 60%,100% 60%,100% 40%,0 40%);
clip-path: polygon(0 60%,100% 60%,100% 40%,0 40%);
-webkit-transform: translate3d(5px,0,0);
transform: translate3d(5px,0,0)
}
20% {
-webkit-clip-path: polygon(0 60%,100% 60%,100% 40%,0 40%);
clip-path: polygon(0 60%,100% 60%,100% 40%,0 40%);
-webkit-transform: translate3d(-5px,0,0);
transform: translate3d(-5px,0,0)
}
20.1% {
-webkit-clip-path: polygon(0 0,0 0,0 0,0 0);
clip-path: polygon(0 0,0 0,0 0,0 0);
-webkit-transform: translate3d(5px,0,0);
transform: translate3d(5px,0,0)
}
25% {
-webkit-clip-path: polygon(0 85%,100% 85%,100% 40%,0 40%);
clip-path: polygon(0 85%,100% 85%,100% 40%,0 40%);
-webkit-transform: translate3d(5px,0,0);
transform: translate3d(5px,0,0)
}
30% {
-webkit-clip-path: polygon(0 85%,100% 85%,100% 40%,0 40%);
clip-path: polygon(0 85%,100% 85%,100% 40%,0 40%);
-webkit-transform: translate3d(-5px,0,0);
transform: translate3d(-5px,0,0)
}
30.1% {
-webkit-clip-path: polygon(0 0,0 0,0 0,0 0);
clip-path: polygon(0 0,0 0,0 0,0 0)
}
35% {
-webkit-clip-path: polygon(0 63%,100% 63%,100% 80%,0 80%);
clip-path: polygon(0 63%,100% 63%,100% 80%,0 80%);
-webkit-transform: translate(-5px);
transform: translate(-5px)
}
40% {
-webkit-clip-path: polygon(0 63%,100% 63%,100% 80%,0 80%);
clip-path: polygon(0 63%,100% 63%,100% 80%,0 80%);
-webkit-transform: translate(5px);
transform: translate(5px)
}
45% {
-webkit-clip-path: polygon(0 63%,100% 63%,100% 80%,0 80%);
clip-path: polygon(0 63%,100% 63%,100% 80%,0 80%);
-webkit-transform: translate(-5px);
transform: translate(-5px)
}
50% {
-webkit-clip-path: polygon(0 63%,100% 63%,100% 80%,0 80%);
clip-path: polygon(0 63%,100% 63%,100% 80%,0 80%);
-webkit-transform: translate(0);
transform: translate(0)
}
55% {
-webkit-clip-path: polygon(0 10%,100% 10%,100% 0,0 0);
clip-path: polygon(0 10%,100% 10%,100% 0,0 0);
-webkit-transform: translate3d(5px,0,0);
transform: translate3d(5px,0,0)
}
60% {
-webkit-clip-path: polygon(0 10%,100% 10%,100% 0,0 0);
clip-path: polygon(0 10%,100% 10%,100% 0,0 0);
-webkit-transform: translateZ(0);
transform: translateZ(0);
opacity: 1
}
60.1% {
-webkit-clip-path: polygon(0 0,0 0,0 0,0 0);
clip-path: polygon(0 0,0 0,0 0,0 0);
opacity: 1
}
to {
-webkit-clip-path: polygon(0 0,0 0,0 0,0 0);
clip-path: polygon(0 0,0 0,0 0,0 0);
opacity: 1
}
}
对于我们的动画,我们可以遵循相同的结构。但在我们的示例中,我们可以应用不同版本的clip-path
。
.cybr-btn {
--shimmy-distance: 5;
--clip-one: polygon(0 2%, 100% 2%, 100% 95%, 95% 95%, 95% 90%, 85% 90%, 85% 95%, 8% 95%, 0 70%);
--clip-two: polygon(0 78%, 100% 78%, 100% 100%, 95% 100%, 95% 90%, 85% 90%, 85% 100%, 8% 100%, 0 78%);
--clip-three: polygon(0 44%, 100% 44%, 100% 54%, 95% 54%, 95% 54%, 85% 54%, 85% 54%, 8% 54%, 0 54%);
--clip-four: polygon(0 0, 100% 0, 100% 0, 95% 0, 95% 0, 85% 0, 85% 0, 8% 0, 0 0);
--clip-five: polygon(0 0, 100% 0, 100% 0, 95% 0, 95% 0, 85% 0, 85% 0, 8% 0, 0 0);
--clip-six: polygon(0 40%, 100% 40%, 100% 85%, 95% 85%, 95% 85%, 85% 85%, 85% 85%, 8% 85%, 0 70%);
--clip-seven: polygon(0 63%, 100% 63%, 100% 80%, 95% 80%, 95% 80%, 85% 80%, 85% 80%, 8% 80%, 0 70%);
}
@keyframes glitch {
0% {
clip-path: var(--clip-one);
}
2%, 8% {
clip-path: var(--clip-two);
transform: translate(calc(var(--shimmy-distance) * -1%), 0);
}
6% {
clip-path: var(--clip-two);
transform: translate(calc(var(--shimmy-distance) * 1%), 0);
}
9% {
clip-path: var(--clip-two);
transform: translate(0, 0);
}
10% {
clip-path: var(--clip-three);
transform: translate(calc(var(--shimmy-distance) * 1%), 0);
}
13% {
clip-path: var(--clip-three);
transform: translate(0, 0);
}
14%, 21% {
clip-path: var(--clip-four);
transform: translate(calc(var(--shimmy-distance) * 1%), 0);
}
25% {
clip-path: var(--clip-five);
transform: translate(calc(var(--shimmy-distance) * 1%), 0);
}
30% {
clip-path: var(--clip-five);
transform: translate(calc(var(--shimmy-distance) * -1%), 0);
}
35%, 45% {
clip-path: var(--clip-six);
transform: translate(calc(var(--shimmy-distance) * -1%));
}
40% {
clip-path: var(--clip-six);
transform: translate(calc(var(--shimmy-distance) * 1%));
}
50% {
clip-path: var(--clip-six);
transform: translate(0, 0);
}
55% {
clip-path: var(--clip-seven);
transform: translate(calc(var(--shimmy-distance) * 1%), 0);
}
60% {
clip-path: var(--clip-seven);
transform: translate(0, 0);
}
31%, 61%, 100% {
clip-path: var(--clip-four);
}
}
这是最难理解的部分。这里到底发生了什么?我们的关键帧在故障元素上创建了一个剪辑路径动画。同时,我们让元素左右摆动。我们可以放慢动画速度来观察发生了什么。
我还制作了一个演示来展示剪辑的不同状态。
这将使我们更容易维护和调整不同的动画状态。
剩下要做的就是把它和 :hover 选择器绑定起来。默认情况下,我们隐藏 glitch 元素。然后当鼠标悬停时,我们显示它的动画。
.cybr-btn__glitch {
display: none;
}
.cybr-btn:hover .cybr-btn__glitch {
display: block;
}
这给了我们想要的结果。
就是这样!
这就是仅使用 CSS 重新创建 Cyberpunk 2077 按钮的方法!
记住,我们使用变量来表示颜色是有原因的。将 HSL 与变量结合,我们不仅可以轻松添加颜色变体,还可以添加 :active 颜色变化。
文章来源:https://dev.to/jh3y/css-cyberpunk-2077-buttons-takeing-your-css-to-night-city-43l0