产品经理要求做个鼠标悬停,头像要跳出来的感觉效果

252 阅读9分钟

最近产品经理有个需求,要求把鼠标放到头像上需要变大,圆圈保持不变的特效,如何我们挣扎了很久,最终想到了解决方案,我们用一个img实现一悬停效果,达不到不一样的视觉体验。

image.png

HTML:只有一个元素

<img src="" alt=""> 是的,一个元素!那到底找你们实现的呢?接下来一起看看

增加hover后放大图片效果

让我们首先添加一个hover事件:

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(从顶部和侧面),稍后我们会讲到。

下面是目前为止的代码,其中包括一些 CSS 变量,你可以使用这些变量来配置图像大小 ( --s) 和“边框”颜色 ( --c):

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绘制在元素框的外面。在我们的例子中,我们需要它与元素 重叠。更准确地说,我们需要它遵循渐变创建的圆圈。

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

不要忘记第二个元素是经过缩放的,所以我们的结果也是经过缩放的......这意味着我们需要将结果除以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 */
}

顶部填充没有特别的逻辑。这样做的目的是确保轮廓不会接触头像的头部。我使用元素的大小来定义该空间以始终具有相同的比例。

请注意,我已将content-box值添加到background

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,当然还有梯度。

下面是一张图来说明我们需要隐藏什么或者需要显示什么才能更准确

image.png 左边的图像是我们目前拥有的,右边是我们想要的。绿色部分表示我们必须对原始图像应用蒙版才能获得最终结果。

我们可以识别面具的两个部分:

  • 底部的圆形部分,其尺寸和曲率与我们在头像后面创建圆形时使用的径向渐变相同
  • 顶部的矩形覆盖了轮廓内的区域。请注意轮廓位于顶部绿色区域的外部 — 这是最重要的部分,因为它允许切割轮廓,以便仅显示底部。

以下是我们最终的 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属性。首先,请注意其中有一个与属性类似的属性。我为公共部分创建了一个新变量,radial-gradient()以使事情不那么混乱。background``--_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%),并与顶部偏移量的值与轮廓的偏移量相同。我--_o为我们之前定义的偏移量创建了另一个 CSS 变量 :

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

这里令人困惑的一点是,我们需要一个偏移量来表示轮廓(将其从外向内移动),但需要一个正偏移量来表示渐变(从上向下移动)。所以,如果你想知道为什么我们要将偏移量乘以 ,--_o那么-1现在你知道了!

将鼠标悬停在上面,看看一切是如何一起移动的。中间的框显示了由两个渐变组成的遮罩层。将其想象为左侧图像的可见部分,您将在右侧获得最终结果!

总结

通过不停的修改,试验,我们完成了!我们不仅制作了一个漂亮的悬停动画,而且我们只用一个 HTML<img>元素就完成了这一切。仅此而已,而且 CSS 代码不到 20 行!

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

如果我们允许自己使用更多 HTML,我们是否可以简化 CSS?当然可以。但我们来这里是为了学习新的 CSS 技巧!可以探索 CSS 渐变、遮罩、属性outline的行为、转换等等。

这效果放出来得到了产品经理的点点头非常认可的表情~

还有更好的办法可以一起分享分享,相互学习。

整体代码如下: