网上那些横着排队的头像布局都看腻了,今天咱们来整点不一样的,搞一个圆形的头像列表。这玩意儿不光看着带劲,还能让鼠标悬停时头像“弹”出来,整个交互效果直接拉满。废话不多说,直接上干货。
前言,圆形布局是啥新套路
圆形头像列表,顾名思义就是让一堆头像图片围成一个圈圈。跟那种横向一排的列表相比,这种布局在视觉上更抓人眼球,尤其适合做团队展示或者社交关系的可视化。实现它的核心思路,是让每个图片元素都沿着一个看不见的圆形轨迹来摆放位置。
扒一扒,实现圆形布局的两把刷子
搞这种环绕效果,CSS里其实藏着两套方案。一套是用offset这个属性,它能给元素定义一条运动路径,让图片乖乖地沿着圆形走。另一套更骚,用的是transform的旋转加平移组合拳,通过三角函数计算出每个图片的位置。咱们今天主要玩后面这套,因为它后期调整动画更方便。
方案一:offset路径,简单粗暴
.container {
display: grid;
}
.container img {
grid-area: 1/1;
offset: circle(180px) calc(100%*sibling-index()/sibling-count()) 0deg;
}这代码看着有点懵,其实逻辑特简单。offset-path定义了圆形轨道,offset-distance负责告诉每个图片在轨道上走多远。这里用sibling-index()和sibling-count()这对好基友,自动算出每个兄弟元素该站的位置。比如有6张图,它们就会均匀分布在圆上,分别停在16.67%、33.33%、50%……这些点上。
实操细节:这个方案思路清晰,但得注意sibling-index()和sibling-count()这俩函数目前浏览器支持还不算完美,部分版本可能得等等。另外offset-rotate: 0deg这行别忘了加,不然图片头像会随着轨道歪着头,那就尴尬了。
方案二:transform旋转,灵活可控
.container {
display: grid;
}
.container img {
grid-area: 1/1;
--_i: calc(1turn*sibling-index()/sibling-count());
transform: rotate(calc(-1*var(--_i))) translate(180px) rotate(var(--_i));
}这套玩法的核心思路是:先把图片旋转到指定角度,然后平移一段距离(也就是圆的半径),最后再给它转回来。这样做的好处是,图片本身始终保持正立,而且旋转的角度变量(--_i)可以反复利用,为后面的动画效果省了不少事。
让圆形列表活起来,响应式安排上
光有一个圈还不够,得让它能跟着容器大小变化。这里就得请出容器查询这个大杀器了。
.container {
--s: 120px; /* 图片尺寸 */
aspect-ratio: 1;
container-type: inline-size;
}
.container img {
width: var(--s);
--_r: calc(50cqw - var(--s)/2);
--_i: calc(1turn*sibling-index()/sibling-count());
transform: rotate(calc(-1*var(--_i))) translate(var(--_r)) rotate(var(--_i));
}这里50cqw是容器宽度的50%,让圆的半径能根据容器动态缩放。但问题来了,容器一变大,图片就散得太开了,看着不紧凑。所以得加个限制,让它们保持最近距离,不能超过一个最大值。
--_r: min(50cqw - var(--s)/2, R);这个R就是让所有图片刚好挨在一起的最小半径,计算公式有点几何学内味儿:S/(2 x sin(.5turn/N))。实际写成CSS就是:
--_r: min(50cqw - var(--s)/2, var(--s)/(2*sin(.5turn/sibling-count())));这样,容器无论怎么缩放,图片们都会紧紧抱团,不会散架。
切角效果,让头像之间有呼吸感
这步是实现“挖空”效果的关键,让相邻图片边缘有个半圆缺口,看起来就像叠在一起似的。这得用上mask属性。
mask: radial-gradient(50% 50% at X Y, #0000 calc(100% + var(--g)), #000);这里的X和Y坐标是重中之重,它们代表了当前图片指向相邻图片中心点的方向。计算过程得用三角函数算出相邻图片的相对偏移。
--_i: calc(1turn*sibling-index()/sibling-count());
--_j: calc(1turn*(sibling-index() + 1)/sibling-count());
mask: radial-gradient(50% 50% at
calc(50% + var(--_r)*(cos(var(--_j)) - cos(var(--_i))))
calc(50% + var(--_r)*(sin(var(--_i)) - sin(var(--_j)))),
#0000 calc(100% + var(--g)), #000);看着挺复杂,其实逻辑就是:从当前图片的中心出发,先走到大圆的中心,再走到下一张图片的中心,最后这两段路径在X轴和Y轴上的投影加起来,就是切口的中心点坐标。
一个小技巧:如果想切角朝另一个方向,只需要把--_j的公式改成calc(1turn*(sibling-index() - 1)/sibling-count()),然后给容器加个.reverse类就行。
动画搞起,悬停时头像“弹出来”
想让鼠标指到哪个头像,哪个就完全展示出来,不跟别人叠一起。实现思路是让除了当前悬停的头像之外,所有其他头像都沿着圆旋转一个角度,直到当前头像完全露出。
.container img {
--_a: 20deg;
--_i: calc(1turn*sibling-index()/sibling-count() + var(--_ii, 0deg));
--_j: calc(1turn*(sibling-index() + 1)/sibling-count() + var(--_jj, 0deg));
}
.container:has(:hover) img {
--_ii: var(--_a);
--_jj: var(--_a);
}
.container img:hover {
--_ii: 0deg;
}这样,当悬停发生时,所有图片的--_i和--_j都会加上一个旋转角度,唯独悬停的那张--_ii被清零,所以它纹丝不动。
但这里有个坑:悬停的图片不动了,但它的相邻图片(切角关联的那张)的--_j还是带着偏移量,导致切角位置错乱。所以得额外用选择器把相邻图片的--_jj也清零。
.container:not(.reverse) img:has(+ :hover),
.container.reverse img:hover + * {
--_jj: 0deg;
}还有一个边缘情况:第一张和最后一张在视觉上是相邻的,但在HTML代码里却隔着十万八千里,用+选择器根本抓不到它们。所以还得手动加两个选择器处理首尾相接:
.container.reverse:has(:last-child:hover) img:first-child,
.container:not(.reverse):has(:first-child:hover) img:last-child {
--_jj: 0deg;
}最后,旋转的角度不能随便给个20度,得算个精确值,刚好让悬停的头像完全露出。这个值可以用三角函数算出来:
--_a: calc(2*asin((var(--s) + var(--g))/(2*var(--_r))) - 1turn/sibling-count());把这个值赋给--_a,动画就完美了。
