阿里技术论坛好像没办法再文章里插入GIF。
如何实现
HTML
这个效果的结构元素不多,只需要一行代码就可以。
<img src="https://assets.codepen.io/1480814/av+1.png" alt="随机头像图片" >
CSS
开始写 CSS 之前,我们先来剖析一下效果。悬停时图像会变大,因此我们肯定会transform: scale()
在那里使用。头像后面有一个圆圈,径向渐变就可以了。最后,我们需要一种在圆圈底部创建边框的方法,以创建圆圈后面头像的外观。
缩放效果
我们可以通过添加transform
属性来实现缩放效果:
img {
width: 280px;
aspect-ratio: 1;
cursor: pointer;
transition: .5s;
}
img:hover {
transform: scale(1.35);
}
那么我们的效果就会是这样的:
圆形
我们说过背景将是一个径向渐变。可以在径向渐变的颜色之间创建硬边,使其看起来像是用实线绘制的圆形。
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);
}
请注意我在那里使用的 CSS 变量 --b
。它表示“边框”
的厚度,实际上只是用于定义径向渐变红色部分的硬颜色停止点。
在这段代码中,我们首先定义了一个名为--b
的 CSS 变量,用于表示边框的宽度。然后,我们设置了图像的宽度和纵横比。
在 background
属性中,我们使用 radial-gradient
创建了一个径向渐变背景。该 radial-gradient
的参数指定了以下内容:
circle closest-side
:以离元素最近的边为圆心的圆形渐变。#ECD078 calc(99% - var(--b))
:从 #ECD078 开始渐变,直到元素边界距离达到 99% 减去边框宽度 --b 的位置。#C02942 calc(100% - var(--b)) 99%
:从 #C02942 渐变到 #0000,从元素边界距离达到 100% 减去边框宽度 --b 的位置,一直到达到 99% 的位置。#0000
:最后一个颜色为透明。
这样,我们使用径向渐变实现了一个由两种颜色构成的圆形背景,就像是用实线绘制的圆形一样。
同时,我们仍然保留了之前的图像缩放效果,如图!!!
下一步是在悬停时调整渐变的大小。圆形需要保持其大小不变,即使图像增大。由于我们应用了 scale()
变换,实际上我们需要减小圆形的大小,因为否则它会随着头像的缩放一同增大。因此,在图像缩放的同时,我们需要使渐变缩小。
首先,让我们定义一个 CSS 变量 --f
,用于定义缩放参数
,并使用它来设置圆形的大小。我将默认值设置为 1,因为这是图像和我们从中进行变换的圆形的初始缩放比例。
下面是一个演示,悬停鼠标可以看到背后发生的事情:
我在径向渐变中添加了第三个颜色,以更好地标识悬停时渐变的区域:
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()
变换——而渐变的大小会减小。
为了创建一个完整的圆形,我们结合了 outline
(轮廓)和渐变创建的“边框”。我们仍然需要隐藏轮廓的部分内容(顶部和侧边),稍后会讲到这一点。
以下是到目前为止的代码,包括一些可以用来配置图片大小(--s)
和“边框”颜色(--c)
的 CSS
变量:
img {
--s: 280px; /* 图片大小 */
--b: 5px; /* 边框厚度 */
--c: #C02942; /* 边框颜色 */
--f: 1; /* 初始缩放比例 */
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; /* 悬停时的缩放比例 */
}
由于我们需要一个圆形的底部边框,我们在底部添加了一个 border-radius
,使得轮廓能够与渐变的曲线相匹配。
在 outline-offset
中使用的计算实际上比看起来更简单明了。默认情况下,轮廓是绘制在元素框的外部。而在我们的情况下,我们需要它与元素重叠。更准确地说,我们需要它按照渐变创建的圆形来呈现。
当我们对元素进行缩放时,我们会看到圆形和边缘之间的空白。不要忘记,我们的想法是在缩放变换运行后保持圆形的大小不变,这就给我们留下了用来定义轮廓偏移的空间,如上图所示。
不要忘记第二个元素被缩放了,所以我们的结果也被缩放了... 这意味着我们需要将结果除以 f 来获取实际的偏移值:
偏移量 = ((f - 1) * S/2) / f = (1 - 1/f) * S/2
由于我们需要轮廓从外部到内部,所以我们添加了一个负号:
偏移量 = (1/f - 1) * S/2
这一步的效果还是有点差别,但是我们仍然需要底部的轮廓重叠在圆形上,而不是让它透过圆形漏出来。我们可以通过从偏移量中减去边框的大小来实现:
outline-offset: calc((1 / var(--f) - 1) * var(--s) / 2 - var(--b));
现在我们需要找到如何从轮廓中移除顶部部分。换句话说,我们只想要图片轮廓的底部部分。
首先,我们可以通过使用 padding 在顶部添加空间来避免顶部的重叠:
img {
--s: 280px; /* 图片大小 */
--b: 5px; /* 边框厚度 */
--c: #C02942; /* 边框颜色 */
--f: 1; /* 初始缩放比例 */
width: var(--s);
aspect-ratio: 1;
padding-block-start: calc(var(--s)/5);
/* 其他属性 */
}
img:hover {
--f: 1.35; /* 悬停时的缩放比例 */
}
通过在顶部添加 padding-block-start
,我们为顶部留出了一些空间,避免了重叠。
这个顶部的 padding 没有特定的逻辑。主要是为了确保轮廓不会触碰到头像的头部。我使用元素的大小来定义这个空间,以保持相同的比例。
在背景中添加了 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;
我们需要这么做是因为我们添加了 padding,我们只想让背景设置到内容盒子(content box),所以我们必须明确告诉背景停止在那里。
接下来我们将使用 mask 属性来隐藏一些部分。为此,我们将依赖于渐变。以下是一个图示,说明我们需要隐藏或显示的部分:
左图是我们目前的样子,右图是我们想要的样子。绿色部分表示我们必须应用于原始图像的遮罩(mask),以获得最终结果。
我们可以确定遮罩的两个部分:
底部的圆形部分具有与我们用来创建头像后面的圆形渐变相同的尺寸和曲率。
顶部的矩形部分覆盖了轮廓内的区域。请注意,轮廓在顶部的绿色区域之外 - 这是最重要的部分,因为它允许轮廓被剪切,只显示底部部分。
下面是我们最最最后的 CSS 代码:
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-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; /* 鼠标悬停时的缩放比例 */
}
让我们解析一下 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
属性的行为、变形等等。如果你在任何时候感到迷茫,那么一定要查看我系列文章中使用相同的基本概念的例子。有时候看到更多的示例和用例有助于加深理解。