头疼圆形头像列表咋整?,手把手带你用CSS玩转环绕布局还带炫酷切角

3,214字
14–20 分钟
in

网上那些横着排队的头像布局都看腻了,今天咱们来整点不一样的,搞一个圆形的头像列表。这玩意儿不光看着带劲,还能让鼠标悬停时头像“弹”出来,整个交互效果直接拉满。废话不多说,直接上干货。

目录

前言,圆形布局是啥新套路

圆形头像列表,顾名思义就是让一堆头像图片围成一个圈圈。跟那种横向一排的列表相比,这种布局在视觉上更抓人眼球,尤其适合做团队展示或者社交关系的可视化。实现它的核心思路,是让每个图片元素都沿着一个看不见的圆形轨迹来摆放位置。

扒一扒,实现圆形布局的两把刷子

搞这种环绕效果,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);

这里的XY坐标是重中之重,它们代表了当前图片指向相邻图片中心点的方向。计算过程得用三角函数算出相邻图片的相对偏移。

--_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,动画就完美了。