圆形头像列表,CSS动画和mask怎么玩出花?

3,306字
14–21 分钟
in

想把一组头像排成一个完美的圆形,鼠标放上去还能有炫酷的展开动画?这期就手把手搞定它。不用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是下一个头像的角度。用cossin算出两个点在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,动画就既不会浪费空间,也不会显得突兀。