为您的头像制作精美的悬停效果

Avatar of Temani Afif
Temani Afif

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

您是否知道那种有人头从圆圈或洞里探出来的那种效果?著名的猪小弟动画片中他挥手告别,从一系列红环中探出来的场景就是完美的例子,而 Kilian Valkhof 实际上在 CSS-Tricks 上重新制作了这个动画

我有一个类似的想法,但采用不同的方法,并加入了一些动画。我认为这非常实用,并且可以制作一个简洁的悬停效果,您可以将其用于自己的头像等。

看到了吗?我们将制作一个缩放动画,让头像看起来像是从它所在的圆圈中弹出。很酷,对吧?先别看代码,让我们一步一步地一起构建这个动画。

HTML:只有一个元素

如果您还没有查看演示代码,并且想知道这需要多少个 div,那么现在就停下来吧,因为我们的标记只是一个简单的图像元素。

<img src="" alt="">

是的,只有一个元素!这个练习的难点在于使用尽可能少的代码。如果您已经 关注我一段时间了,您应该已经习惯了这一点。我努力寻找可以采用尽可能少、最易维护的代码实现的 CSS 解决方案。

我在 CSS-Tricks 上写了一系列文章 探讨了使用包含单个元素的相同 HTML 标记实现不同悬停效果的方法。我详细介绍了渐变、遮罩、剪裁、轮廓,甚至布局技巧。我强烈建议您查看这些内容,因为我将在本文中重新使用其中的许多技巧。

一个具有透明背景的正方形图像文件最适合我们的操作。如果您想使用这个文件,请使用我正在使用的这个文件。

设计者:Cang

我希望看到许多使用真实图像实现此效果的示例——因此,完成后请在评论区分享您的最终结果,以便我们共同创建一个合集!

在开始 CSS 之前,让我们先分析一下效果。图像在悬停时变大,因此我们一定会使用 transform: scale()。头像后面有一个圆圈,径向渐变可以解决这个问题。最后,我们需要一种方法来在圆圈底部创建一个边框,让它看起来像是头像在圆圈后面。

让我们开始吧!

缩放效果

让我们先添加变换

img {
  width: 280px;
  aspect-ratio: 1;
  cursor: pointer;
  transition: .5s;
}
img:hover {
  transform: scale(1.35);
}

还不算复杂,对吧?让我们继续。

圆圈

我们说背景将是一个径向渐变。这很完美,因为我们可以创建径向渐变颜色之间的硬性停止,这使得它看起来像我们正在使用实线绘制一个圆圈。

img {
  --b: 5px; /* border width */

  width: 280px;
  aspect-ratio: 1;
  background:
    radial-gradient(
      circle closest-side,
      #ECD078 calc(99% - var(--b)),
      #C02942 calc(100% - var(--b)) 99%,
      #0000
    );
  cursor: pointer;
  transition: .5s;
}
img:hover {
  transform: scale(1.35);
}

注意那里使用的 CSS 变量 --b。它代表“边框”的厚度,实际上它只是用于定义径向渐变红色部分的硬性颜色停止。

下一步是玩玩悬停时渐变的大小。圆圈需要保持其大小,因为图像会增长。由于我们正在应用 scale() 变换,实际上我们需要减小圆圈的大小,否则它会随着头像一起放大。因此,当图像放大时,我们需要渐变缩小。

让我们先定义一个 CSS 变量 --f 来定义“缩放因子”,并使用它来设置圆圈的大小。我使用 1 作为默认值,因为这是图像和圆圈的初始缩放比例,我们从这里进行变换。

这是一个演示,说明了这个技巧。悬停以查看幕后发生了什么

我在 radial-gradient 中添加了第三种颜色,以更好地识别悬停时渐变的区域

radial-gradient(
  circle closest-side,
  #ECD078 calc(99% - var(--b)),
  #C02942 calc(100% - var(--b)) 99%,
  lightblue
);

现在,我们必须将背景放置在圆圈的中心,并确保它占用了整个高度。我喜欢直接在 background 简写属性上声明所有内容,这样我们就可以添加背景定位,并确保它不会重复,只需在 radial-gradient() 后面添加这些值。

background: radial-gradient() 50% / calc(100% / var(--f)) 100% no-repeat;

背景放置在中心 (50%),宽度等于 calc(100%/var(--f)),高度等于 100%

--f 等于 1 时,没有任何东西会缩放——再次说明,这是我们的初始缩放比例。同时,渐变占据了容器的整个宽度。当我们增加 --f 时,元素的大小会增加——由于 scale() 变换——并且渐变的大小会减小。

以下是将所有这些应用到我们的演示中后得到的结果

我们越来越接近了!我们在顶部获得了溢出效果,但我们仍然需要隐藏图像的底部部分,这样看起来像是它从圆圈中弹出,而不是位于圆圈前面。这是整个事情中最棘手的一部分,也是我们接下来要做的。

底部边框

我最初尝试使用 border-bottom 属性来解决这个问题,但我无法找到一种方法让边框的大小与圆圈的大小匹配。这是我能获得的最佳效果,您会立即发现它不对

实际解决方案是使用 outline 属性。是的,是 outline,而不是 border。在 之前的一篇文章中,我展示了 outline 的强大功能,以及如何使用它来创建酷炫的悬停效果。结合 outline-offset,我们拥有了实现此效果所需的全部内容。

这个想法是在图像上设置一个 outline,并调整它的偏移量以创建底部边框。偏移量将取决于缩放因子,就像渐变大小一样。

现在,我们有了底部“边框”(实际上是一个 outline)与渐变创建的“边框”相结合,形成一个完整的圆圈。我们仍然需要隐藏 outline 的一部分(来自顶部和侧面),我们稍后会处理。

以下是迄今为止的代码,包括一些可以用来配置图像大小 (--s) 和“边框”颜色 (--c) 的 CSS 变量

img {
  --s: 280px; /* image size */
  --b: 5px; /* border thickness */
  --c: #C02942; /* border color */
  --f: 1; /* initial scale */

  width: var(--s);
  aspect-ratio: 1;
  cursor: pointer;
  border-radius: 0 0 999px 999px;
  outline: var(--b) solid var(--c);
  outline-offset: calc((1 / var(--f) - 1) * var(--s) / 2 - var(--b));
  background: 
    radial-gradient(
      circle closest-side,
      #ECD078 calc(99% - var(--b)),
      var(--c) calc(100% - var(--b)) 99%,
      #0000
    ) 50% / calc(100% / var(--f)) 100% no-repeat;
  transform: scale(var(--f));
  transition: .5s;
}
img:hover {
  --f: 1.35; /* hover scale */
}

由于我们需要一个圆形的底部边框,因此我们在底部添加了一个 border-radius,使 outline 匹配渐变的曲率。

outline-offset 上使用的计算比看起来要简单得多。默认情况下,outline 绘制在元素框的外部。在我们的例子中,我们需要它重叠元素。更确切地说,我们需要它遵循渐变创建的圆圈。

Diagram of the background transition.

当我们缩放元素时,我们看到圆圈和边缘之间的空间。别忘了,这个想法是在缩放变换运行后保持圆圈的大小不变,这样就留下了用于定义轮廓偏移量的空间,如上图所示。

别忘了,第二个元素也被缩放了,因此我们的结果也被缩放了……这意味着我们需要将结果除以 f 来获得真实的偏移值

Offset = ((f - 1) * S/2) / f = (1 - 1/f) * S/2

我们添加了一个负号,因为我们需要轮廓从外部到内部。

Offset = (1/f - 1) * S/2

这是一个快速演示,展示了轮廓如何跟随渐变

您可能已经看到了,但我们仍然需要让底部轮廓重叠圆圈,而不是让它穿透圆圈。我们可以通过从偏移量中减去边框的大小来做到这一点

outline-offset: calc((1 / var(--f) - 1) * var(--s) / 2) - var(--b));

现在,我们需要找到如何从轮廓中移除顶部部分。换句话说,我们只想要图像 outline 的底部部分。

首先,让我们使用填充在顶部添加空间,以帮助避免顶部重叠

img {
  --s: 280px; /* image size */
  --b: 5px;   /* border thickness */
  --c: #C02942; /* border color */
  --f: 1; /* initial scale */

  width: var(--s);
  aspect-ratio: 1;
  padding-block-start: calc(var(--s)/5);
  /* etc. */
}
img:hover {
  --f: 1.35; /* hover scale */
}

顶部填充没有特定的逻辑。这个想法是确保轮廓不触及头像的头部。我使用了元素的大小来定义这个空间,以始终保持相同的比例。

注意,我在 background 中添加了 content-box

background:
  radial-gradient(
    circle closest-side,
    #ECD078 calc(99% - var(--b)),
    var(--c) calc(100% - var(--b)) 99%,
    #0000
  ) 50%/calc(100%/var(--f)) 100% no-repeat content-box;

我们需要这个,因为我们添加了填充,并且我们只希望背景设置为内容框,因此我们必须明确告诉背景在那里停止。

将 CSS 遮罩加入混合

我们到达了最后一步!我们只需要隐藏一些部分,就完成了。为此,我们将依靠 mask 属性,当然还有渐变。

这是一张图,说明了我们需要隐藏的部分,或者更准确地说,我们需要显示的部分

Showing how the mask applies to the bottom portion of the circle.

左边的图像是我们目前拥有的,右边的图像是我们想要的。绿色部分说明了我们必须应用于原始图像的遮罩才能获得最终结果。

我们可以识别遮罩的两个部分

  • 底部有一个圆形部分,它与我们用来创建头像后面圆圈的径向渐变具有相同的尺寸和曲率
  • 顶部有一个矩形,它覆盖了轮廓内的区域。注意轮廓在顶部位于绿色区域的外部——这是最重要的部分,因为它允许剪切轮廓,使只有底部部分可见。

这是我们最终的 CSS。

img {
  --s: 280px; /* image size */
  --b: 5px; /* border thickness */
  --c: #C02942; /* border color */
  --f: 1; /* initial scale */

  --_g: 50% / calc(100% / var(--f)) 100% no-repeat content-box;
  --_o: calc((1 / var(--f) - 1) * var(--s) / 2 - var(--b));

  width: var(--s);
  aspect-ratio: 1;
  padding-top: calc(var(--s)/5);
  cursor: pointer;
  border-radius: 0 0 999px 999px;
  outline: var(--b) solid var(--c);
  outline-offset: var(--_o);
  background: 
    radial-gradient(
      circle closest-side,
      #ECD078 calc(99% - var(--b)),
      var(--c) calc(100% - var(--b)) 99%,
      #0000) var(--_g);
  mask:
    linear-gradient(#000 0 0) no-repeat
    50% calc(-1 * var(--_o)) / calc(100% / var(--f) - 2 * var(--b)) 50%,
    radial-gradient(
      circle closest-side,
      #000 99%,
      #0000) var(--_g);
  transform: scale(var(--f));
  transition: .5s;
}
img:hover {
  --f: 1.35; /* hover scale */
}

让我们分解一下mask 属性。首先,注意background 属性中的类似radial-gradient() 在那里。我创建了一个新的变量--_g 来表示公共部分,使代码更简洁。

--_g: 50% / calc(100% / var(--f)) 100% no-repeat content-box;

mask:
  radial-gradient(
    circle closest-side,
    #000 99%,
    #0000) var(--_g);

接下来,里面还有一个linear-gradient()

--_g: 50% / calc(100% / var(--f)) 100% no-repeat content-box;

mask:
  linear-gradient(#000 0 0) no-repeat
    50% calc(-1 * var(--_o)) / calc(100% / var(--f) - 2 * var(--b)) 50%,
  radial-gradient(
    circle closest-side,
    #000 99%,
    #0000) var(--_g);

这创建了蒙版的矩形部分。它的宽度等于径向渐变的宽度减去两倍的边框厚度。

calc(100% / var(--f) - 2 * var(--b))

矩形的高度等于元素大小的一半,即50%

我们还需要将线性渐变放置在水平中心 (50%) 并从顶部偏移与轮廓偏移相同的距离。我创建了另一个 CSS 变量--_o 来表示之前定义的偏移量。

--_o: calc((1 / var(--f) - 1) * var(--s) / 2 - var(--b));

这里令人困惑的一点是,我们需要对轮廓进行偏移(将其从外部移动到内部),但对渐变进行偏移(从顶部移动到底部)。所以,如果你想知道为什么我们要将偏移量--_o 乘以-1,现在你知道了!

这是一个演示,说明蒙版的渐变配置。

将鼠标悬停在上面,看看一切是如何一起移动的。中间的方框说明了由两个渐变组成的蒙版层。想象一下,它是左侧图像的可见部分,你就会得到右侧的最终结果!

总结

哦,我们完成了!我们不仅得到了一种流畅的悬停动画,而且我们使用单个 HTML <img> 元素就完成了所有操作。仅仅只有它,不到 20 行 CSS 技巧!

当然,我们依靠了一些小技巧和数学公式来实现如此复杂的效果。但我们确切地知道该怎么做,因为我们事先确定了所需的元素。

如果我们允许自己使用更多 HTML,是否可以简化 CSS?当然可以。但我们在这里学习新的 CSS 技巧!这是一个很好的练习,可以探索 CSS 渐变、蒙版、outline 属性的行为、转换等等。如果你在任何时候感到迷茫,那么一定要查看我的系列文章,它使用了相同的通用概念。有时,查看更多示例和用例有助于更好地理解。

我将用一个最后一个演示结束文章,它使用了流行 CSS 开发人员的照片。别忘了用你自己的图片展示一个演示,这样我就可以将其添加到收藏中!