CSS Cyber​​punk 2077 按钮 - 带你的 CSS 去夜之城

2025-05-25

CSS Cyber​​punk 2077 按钮 - 带你的 CSS 去夜之城

如果你对电子游戏稍有兴趣,你肯定对《赛博朋克 2077》有所耳闻。它是 2020 年最受期待的游戏之一。它所描绘的世界独具风格。游戏官网出色地展现了这种美感。其设计在传达游戏观感方面也做得非常出色。你可以想象,这意味着一些界面元素看起来相当精致。

有人先联系我,问我如何创建网站上使用的图像效果。如果你把鼠标悬停在图片库里的图片上,它们就会呈现出这种漂亮的“噪点”效果。

画廊效果故障
画廊效果故障

我接受了挑战。我深入研究了网站的源代码。一番挖掘之后,我发现它是用着色器和 WebGL 实现的。我对编写着色器和 WebGL 完全陌生。这确实激励我尝试一下。但是,目前,我暂时把学习 WebGL 和着色器代码的工作放在了一边。

当我继续浏览我的直播网站时,真正吸引我们眼球的是那些漂亮的故障效果按钮。我对用 CSS 创建故障效果并不陌生。我们决定尝试重现它们。

所需按钮故障
所需按钮故障

以下是具体操作方法!


让我们从一些标记开始



<button class="cybr-btn">
  Beginning_
</button>


Enter fullscreen mode Exit fullscreen mode

我们首先需要解决的是大小、颜色和字体问题。最好的解决方法是什么?深入研究源代码,看看它是如何实现的。从第一次检查开始,我们就发现它使用了自定义字体。

让我们抓住字体“Blender Pro Bold”字体并创建自定义@font-face规则。



@font-face {
  font-family: Cyber;
  src: url("https://assets.codepen.io/605876/Blender-Pro-Bold.otf");
  font-display: swap;
}


Enter fullscreen mode Exit fullscreen mode

一旦有了这些,我们就可以设置基本的样式了。使用 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;


Enter fullscreen mode Exit fullscreen mode

把这些放在一起,我们就能找到这个起点。注意到我们用的是 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%);


Enter fullscreen mode Exit fullscreen mode

请注意,我们没有裁剪按钮的边缘。我们给按钮留出了 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>


Enter fullscreen mode Exit fullscreen mode

要为标签添加样式,我们可以为其指定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);
}


Enter fullscreen mode Exit fullscreen mode

我们在这里引入了一些 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>


Enter fullscreen mode Exit fullscreen mode

我们需要在这里复制文本,并且我们有一些选择。该网站使用伪元素来复制文本。但是,如果我们这样做,就意味着需要同时为两个元素设置动画才能达到效果。通过将文本移动到 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);
}


Enter fullscreen mode Exit fullscreen mode

应用一些样式,例如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;
}


Enter fullscreen mode Exit fullscreen mode

我们可以从按钮中移除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;
}


Enter fullscreen mode Exit fullscreen mode

然后,我们可以将这个剪辑复用到 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);
}


Enter fullscreen mode Exit fullscreen mode

我们得到如下结果:

这正是我们有机会做一些更酷的事情的地方。当我检查按钮并发现背景图片时,我把它拉了下来。我发现,通过堆叠两张图片并平移底部的图片,可以实现边框。现在,我们使用了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);
}


Enter fullscreen mode Exit fullscreen mode

现在,我们为标签和按钮添加了阴影。标签将使用其背后的背景色。尝试将 更改background-colorbody,您就会看到效果!


快完成了!坚持住。故障已经解决。所有需要的东西都准备好了。剩下的就是让它动起来:hover

这种故障效果是怎么产生的?诀窍在于只显示故障元素,:hover并默认应用动画。我假设是在一组关键帧中使用transformclip-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
    }
}


Enter fullscreen mode Exit fullscreen mode

对于我们的动画,我们可以遵循相同的结构。但在我们的示例中,我们可以应用不同版本的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);
  }
}


Enter fullscreen mode Exit fullscreen mode

这是最难理解的部分。这里到底发生了什么?我们的关键帧在故障元素上创建了一个剪辑路径动画。同时,我们让元素左右摆动。我们可以放慢动画速度来观察发生了什么。

我还制作了一个演示来展示剪辑的不同状态。

这将使我们更容易维护和调整不同的动画状态。


剩下要做的就是把它和 :hover 选择器绑定起来。默认情况下,我们隐藏 glitch 元素。然后当鼠标悬停时,我们显示它的动画。



.cybr-btn__glitch {
  display: none;
}
.cybr-btn:hover .cybr-btn__glitch {
  display: block;
}


Enter fullscreen mode Exit fullscreen mode

这给了我们想要的结果。


就是这样!

这就是仅使用 CSS 重新创建 Cyber​​punk 2077 按钮的方法!

记住,我们使用变量来表示颜色是有原因的。将 HSL 与变量结合,我们不仅可以轻松添加颜色变体,还可以添加 :active 颜色变化。

文章来源:https://dev.to/jh3y/css-cyberpunk-2077-buttons-takeing-your-css-to-night-city-43l0
PREV
不断学习,你是否应该投入自己的时间?
NEXT
使用 CSS 制作圆形文本?