头像悬停效果,不用JS怎么让头像从圈里蹦出来?

4,446字
19–28 分钟
in

这阵子刷到一种贼拉酷的鼠标悬停效果——头像原本老老实实待在圆圈里,鼠标一碰,头像突然放大,感觉像是从圆圈后面“噗”地弹出来,只留半截身子在圈里晃悠。就像动画片里猪小弟从红圈圈里探出脑袋挥手再见那味儿。这种效果其实只用一张图片加几行CSS就能搞定,连JavaScript都不用碰。下面就把这波骚操作的每个步骤掰开揉碎,从原理到代码挨个儿交代清楚。

目录

核心思路

整个效果拆开看就三块料:一个圆形的背景、一个会放大的头像、还有一条只出现在圆圈底部的“假边框”。背景用径向渐变(radial-gradient)画出来,头像放大靠transform: scale(),底部边框则用outline配合outline-offset来卡位。最 tricky 的是得把头像上半截露出来的部分藏好,不然就穿帮了。这事儿得靠mask属性来解决,像剪贴板一样把不需要的部分切掉。

动手实操

先搭个最简结构,一个<img>标签就够,连<div>都不用包。图片最好用正方形、背景透明的PNG,这样弹出来的时候不会带出奇奇怪怪的背景块。下面这张图是示例用的(设计来自Cang),当然可以换成自己的大头照。

<img src="avatar.png" alt="">

第一步:让图片变大

先给图片定个尺寸,比如宽280像素,高跟着宽走(aspect-ratio: 1)。再加个过渡动画,别让缩放显得太生硬。鼠标悬停时,把缩放系数拉到1.35倍。

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

这时候图片会整个放大,但背景还是空白的。接下来得把那个圆圈画出来。

第二步:画一个会伸缩的圆圈

背景用径向渐变实现,关键是把颜色过渡做成硬边,看起来像一个实心圆环。circle closest-side让渐变呈正圆形,calc(99% - var(--b))calc(100% - var(--b))这两档把颜色卡死,形成边框效果。这里定义了一个变量--b表示边框粗细。

img {
  --b: 5px;
  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);
}

问题来了——图片放大时,圆圈也跟着放大,那就不像“从圈里弹出来”,反而像整个圈一起变大。正确的效果应该是:图片变大,但圆圈保持原来的大小。所以需要在悬停时反过来把圆圈尺寸缩小。加一个缩放系数变量--f,默认值1,悬停时改成1.35。圆圈的宽度用calc(100% / var(--f))来控制,这样图片放大(--f变大)的同时,圆圈宽度反而缩小。背景定位和尺寸写在一起:50% / calc(100% / var(--f)) 100% no-repeat

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

这时候悬停效果已经有点意思了,头像从圆圈上方冒出来,但底部还是露馅——头像整个盖在圆圈前面,不像“从后面钻出来”。需要给圆圈加一个底部边框,让头像看起来被圆圈下半截挡住。

第三步:用outline造个假底边

border不好使,因为border-radius只能影响边框拐角,没法精确匹配圆圈的弧度。改用outline加上outline-offsetoutline默认画在元素边框外部,但这里需要它画到元素内部去,所以偏移量得算成负值。计算公式:(1/var(--f) - 1) * 宽度/2。别忘了再减去边框厚度--b,不然边框会突出来一块。

img {
  --s: 280px;
  --b: 5px;
  --c: #C02942;
  --f: 1;
  width: var(--s);
  aspect-ratio: 1;
  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;
}

这时候能看到底部有个圆弧形的边框跟着圆圈走了,但outline是个完整的闭合环,头像头顶也被框住了一圈,看着很别扭。解决办法:用padding-top在头顶加一段空白,然后靠mask把上半截的outline切掉。

第四步:mask上场,只留底边

mask属性可以理解为一个遮罩层——遮罩里黑色部分显示,白色部分隐藏。这里需要两个遮罩层叠在一起:一个圆形遮罩对应底部的圆弧,一个矩形遮罩把头顶和两侧的outline盖住。

先加个顶部内边距,让头像头顶离圆圈远一点,避免outline切到头发。padding-block-start: calc(var(--s)/5),这个比例试出来的,能保证大部分头像不撞头。

然后定义两个公共部分变量--_g--_o,减少代码重复。--_g包含背景定位和content-box(因为加了padding,背景只显示在内容区)。--_o就是刚才的outline-offset计算公式。

img {
  --s: 280px;
  --b: 5px;
  --c: #C02942;
  --f: 1;
  --_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-block-start: 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;
}

解释一下mask里的两个部分:

  • 第二个是圆形渐变,和背景里的径向渐变一模一样,用来保留底部圆圈的显示区域。
  • 第一个是线性渐变,画一个矩形遮罩。宽度是calc(100% / var(--f) - 2 * var(--b))(圆圈宽度减去两边边框厚度),高度50%,位置水平居中,垂直偏移量用calc(-1 * var(--_o))。因为outline-offset是负值时outline往里缩,而遮罩需要往上移动同样的距离才能盖住头顶那一圈。

第五步:微调与避坑

实际操作时有几个坑容易踩。图片原始比例不是1:1的话,aspect-ratio: 1会强制拉伸变形,所以准备素材时先用图片编辑器裁成正方形。padding-block-start的值可以根据头像构图微调,比如头特别大的可以加大到var(--s)/4。另外mask的线性渐变里的50%是垂直位置,如果头像露出来的部分不对称,可以改成calc(-1 * var(--_o) + 10px)之类的手动校准。浏览器兼容性方面,maskaspect-ratio在主流浏览器最新版都没问题,但IE早已入土,不用管。

完整代码打包

img {
  --s: 280px;
  --b: 5px;
  --c: #C02942;
  --f: 1;
  --_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-block-start: 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;
}

把这段CSS贴进去,再放一张正方形透明底的图片,悬停上去就能看到头像“biu”一下从圈里弹出来,底部还带个圆弧边框。拿这个效果当个人主页的头像悬停,或者作品集里的鼠标反馈,都挺带感。