想把一组头像排成一个完美的圆形,鼠标放上去还能有炫酷的展开动画?这期就手把手搞定它。不用JavaScript,全凭现代CSS的几个新特性,就能让头像们乖乖听话,沿着圆圈排排坐,还能玩出交错剪影的效果。整个过程就像在画图纸上安排座位,用offset属性给每个头像指定跑道,再用mask悄悄挖掉一块,让前后头像有个酷酷的叠压关系。当鼠标悬停时,整个圈圈会轻微旋转,把被挡住的头像完全亮出来,整个过程丝滑得像转盘抽奖。哪怕你是第一次碰这些新属性,跟着步骤一步步来,也能轻松玩转。
两个路子实现圆形排布
路子一:offset属性一箭三雕
这招是给每个图片单独画条路,让它们在一条圆形跑道上按百分比站好位置。核心就三行代码,但威力不小。
.container { display: grid; } .container img { grid-area: 1/1; offset: circle(180px) calc(100% * sibling-index() / sibling-count()) 0deg; }
offset是个简写,拆开看更清楚:offset-path定义跑道是个半径180px的圆;offset-distance计算每个图片该跑多远,用的是sibling-index()(当前图片是第几个)和sibling-count()(总共有几个)两个函数;offset-rotate设为0deg,防止图片跟着跑道歪头。假设有6张图,距离就是16.67%、33.33%、50%、66.67%、83.33%、100%,刚好均匀分布在圆周上。这时候只需要改半径,就能控制整个圈的大小。
路子二:transform组合拳玩旋转
另一种玩法是用transform把旋转和位移结合起来,代码稍微复杂点,但好处是能复用旋转角度。
.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是每个图片的专属角度,先反方向旋转,再沿着半径方向平移180px,最后再转回来。虽然第一种写法更直观,但第二种因为旋转角度能重复使用,在后面做动画和剪影时会方便不少。
让圈圈跟着容器大小变
半径动态计算
想让圆形列表随着容器尺寸变化而缩放,得用到容器查询单位cqw(容器宽度的1%)。但光用容器宽度算半径,容器变大了图片会散得太开,不好看。所以得加个限制,让圆圈的半径刚好能装下所有图片,但又不会让图片之间有空隙。
.container { --s: 120px; aspect-ratio: 1; container-type: inline-size; } .container img { width: var(--s); --_r: min(50cqw - var(--s)/2, var(--s) / (2 * sin(.5turn / sibling-count()))); --_i: calc(1turn * sibling-index() / sibling-count()); transform: rotate(calc(-1 * var(--_i))) translate(var(--_r)) rotate(var(--_i)); }
--_r就是最终半径,min函数左边是容器宽度的一半减去图片半径,右边是根据图片数量和大小算出的最小半径。这样不管容器是宽是窄,图片们都会紧紧挨在一起,不会出现“稀稀拉拉”的情况。
加个缝隙更精致
光挨着还不够,想给图片之间加个缝,就在半径公式里把图片大小和缝隙加进去。
.container { --s: 120px; --g: 10px; } .container img { --_r: min(50cqw - var(--s)/2, (var(--s) + var(--g)) / (2 * sin(.5turn / sibling-count()))); }
这里的sin函数需要浏览器支持三角函数,现代浏览器基本都妥了。缝隙的存在让整个圆形看起来更有呼吸感,不会挤得慌。
剪影特效整起来
从当前到相邻的轨迹计算
要在两个相邻头像之间挖个半圆形缺口,得精确算出缺口中心的位置。思路是从当前头像中心出发,先走到大圆的圆心,再走到相邻头像的中心。用上三角函数,就能得到偏移量。
.container img { --_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); }
--_i是当前头像的角度,--_j是下一个头像的角度。用cos和sin算出两个点在X轴和Y轴上的差值,加到50% 50%上,就是缺口中心的位置。缺口半径用calc(100% + var(--_g)),让缺口刚好能容纳缝隙。
反向剪影也不放过
如果想让缺口方向反过来,比如从当前头像指向上一个,只需要把--_j改成前一个的角度就行。加个.reverse类切换。
.container.reverse img { --_j: calc(1turn * (sibling-index() - 1) / sibling-count()); }
悬停动画完美收尾
先让所有图片转起来
鼠标放上去,想看到除了当前图片外,其他图片都绕着圆心转,把当前图片完全露出来。思路是给所有图片加一个额外的旋转角度。
.container:has(:hover) img { --_i: calc(1turn * sibling-index() / sibling-count() + 20deg); --_j: calc(1turn * (sibling-index() + 1) / sibling-count() + 20deg); }
但这样会连当前图片一起转,所以得把当前图片的额外角度去掉。
.container img:hover { --_ii: 0deg; }
这里用上中间变量--_ii和--_jj来灵活控制。
处理相邻图片的牵连
当前图片不转,但其他图片转的时候,有些图片的--_j会依赖于当前图片的角度。比如上一个图片的--_j就是当前图片的角度,如果当前图片不转,那上一个图片的--_j也不能转。
.container:not(.reverse) img:has(+ :hover), .container.reverse img:hover + * { --_jj: 0deg; }
这行代码的意思是:在非反向列表里,如果有图片紧挨着悬停图片(+ :hover),就把它俩的额外角度清零;在反向列表里,悬停图片紧挨着的下一个图片也清零。
首尾相连的特殊处理
因为HTML结构不是闭环的,第一个和最后一个图片在视觉上相邻,但在代码里不是亲兄弟。得单独照顾一下。
.container.reverse:has(:last-child:hover) img:first-child, .container:not(.reverse):has(:first-child:hover) img:last-child { --_jj: 0deg; }
这就把首尾的相邻关系也补上了。
算出精确旋转角度
之前用的20deg是随便写的,实际得算出刚好能把所有图片推开的最小角度。这个角度取决于图片大小、缝隙和半径。
--_a: calc(2 * asin((var(--s) + var(--g)) / (2 * var(--_r))) - 1turn / sibling-count());
asin是反正弦函数,先算出单个图片加上缝隙占圆周的比例,再减去图片之间本来的角度差,得到的就是需要额外转动的角度。把这个角度赋给--_a,动画就既不会浪费空间,也不会显得突兀。
