这阵子刷到一种贼拉酷的鼠标悬停效果——头像原本老老实实待在圆圈里,鼠标一碰,头像突然放大,感觉像是从圆圈后面“噗”地弹出来,只留半截身子在圈里晃悠。就像动画片里猪小弟从红圈圈里探出脑袋挥手再见那味儿。这种效果其实只用一张图片加几行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-offset。outline默认画在元素边框外部,但这里需要它画到元素内部去,所以偏移量得算成负值。计算公式:(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)之类的手动校准。浏览器兼容性方面,mask和aspect-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”一下从圈里弹出来,底部还带个圆弧边框。拿这个效果当个人主页的头像悬停,或者作品集里的鼠标反馈,都挺带感。
