一堆宝丽来照片叠在一起,一张一张往外翻,翻到最后又自动循环回来,这种效果看着挺酷吧?其实不用JS,光靠CSS的z-index和transform就能整出来。这里头最关键的是搞懂堆叠顺序咋变化,不然动画就会翻车。
基本结构
HTML部分贼简单,一个容器里扔几张图片:
<div class="gallery">
<img src="photo1.jpg" alt="">
<img src="photo2.jpg" alt="">
<img src="photo3.jpg" alt="">
<img src="photo4.jpg" alt="">
</div>CSS先把这些图片叠到一起,用网格布局让它们全挤在同一个格子:
.gallery {
display: grid;
width: 220px; /* 控制尺寸 */
}
.gallery > img {
grid-area: 1 / 1;
width: 100%;
aspect-ratio: 1;
object-fit: cover;
border: 10px solid #f2f2f2;
box-shadow: 0 0 4px #0007;
}边框和阴影一加上,宝丽来那种厚实感就出来了。每张图片现在都叠在一块儿,最后那张在DOM里排最底下?不对,在网格里img标签按顺序堆叠,最后那张在视觉最上面。
堆叠顺序逻辑
这个效果的核心是操作z-index。一开始所有图片的z-index都是2,所以最后一张图在最上面。流程是这样:
- 把最上面的图往右滑出去,露出下面那张。
- 滑出去的同时把它的
z-index降到1,让它沉底。 - 再把它滑回来,这时候它已经跑到堆叠的最下面了。
- 重复这个过程,每张图轮流滑一遍。
听起来简单,但实际操作时z-index变化时机稍微偏一点就会出幺蛾子。比如滑完一张之后,另一张突然跳到最前面,整个顺序就乱了。
基础动画拆解
先搞定3张图的情况。动画总时长设为6秒,每张图的动画延迟不一样:
- 第1张延迟0秒
- 第2张延迟-2秒(提前开始)
- 第3张延迟-4秒
整个动画分成三段:向右滑、向左滑、原地不动。向右滑和向左滑各占1/6的总时长(即1秒),原地不动占剩下的4秒。
关键帧可以这么写:
@keyframes slide {
0% { transform: translateX(0%); z-index: 2; }
16.66% { transform: translateX(120%); z-index: 2; }
16.67% { transform: translateX(120%); z-index: 1; }
33.34% { transform: translateX(0%); z-index: 1; }
100% { transform: translateX(0%); z-index: 1; }
}这里16.66%到16.67%之间只差0.01%,用来瞬间把z-index从2降到1。120%的滑动距离是为了确保图片完全移出视野,留点余量。
但是跑起来会发现,滑完第3张之后,第2张又跳回最上面了。问题出在z-index重置的时机不对。需要在第1张开始滑动之前,提前把第2张的z-index拉回2。
修复跳动问题
调整关键帧,在动画进行到2/3的时候(即66.33%附近)把z-index重新设为2:
@keyframes slide {
0% { transform: translateX(0%); z-index: 2; }
16.66% { transform: translateX(120%); z-index: 2; }
16.67% { transform: translateX(120%); z-index: 1; }
33.34% { transform: translateX(0%); z-index: 1; }
66.33% { transform: translateX(0%); z-index: 1; }
66.34% { transform: translateX(0%); z-index: 2; }
100% { transform: translateX(0%); z-index: 2; }
}但最后一张图(第3张)还是会出状况,因为它后面没有别的图来“顶”它。需要单独给最后一张图写一套关键帧,把重置时机挪到5/6的位置(83.33%附近):
@keyframes slide-last {
0% { transform: translateX(0%); z-index: 2; }
16.66% { transform: translateX(120%); z-index: 2; }
16.67% { transform: translateX(120%); z-index: 1; }
33.34% { transform: translateX(0%); z-index: 1; }
83.33% { transform: translateX(0%); z-index: 1; }
83.34% { transform: translateX(0%); z-index: 2; }
100% { transform: translateX(0%); z-index: 2; }
}然后把动画绑定到图片上:
.gallery > img {
animation: slide 6s infinite;
}
.gallery > img:last-child {
animation-name: slide-last;
}
.gallery > img:nth-child(2) { animation-delay: -2s; }
.gallery > img:nth-child(3) { animation-delay: -4s; }代码瘦身与模块拆分
把滑动和z-index变化拆成两个独立动画,用steps(1)让z-index瞬间跳变:
.gallery > img {
z-index: 2;
animation:
slide 6s infinite,
z-order 6s infinite steps(1);
}
.gallery > img:last-child {
animation-name: slide, z-order-last;
}
@keyframes slide {
16.67% { transform: translateX(120%); }
33.33% { transform: translateX(0%); }
}
@keyframes z-order {
16.67%,
33.33% { z-index: 1; }
66.33% { z-index: 2; }
}
@keyframes z-order-last {
16.67%,
33.33% { z-index: 1; }
83.33% { z-index: 2; }
}这样代码清爽多了,后面要支持任意数量的图片也方便调整。
适配N张图片的通用方案
假设有N张图,动画总时长还是6秒。每张图滑动(向右+向左)总共占用100%/N的时间,其中向右滑占一半即50%/N,向左滑占另一半。原地不动的时间就是剩下的100% - 100%/N。
用Sass循环来生成延迟和关键帧百分比:
$n: 5; // 图片数量
@for $i from 2 to ($n + 1) {
.gallery > img:nth-child(#{$i}) {
animation-delay: calc(#{(1 - $i)/$n} * 6s);
}
}
@keyframes slide {
#{50/$n}% { transform: translateX(120%); }
#{100/$n}% { transform: translateX(0%); }
}
@keyframes z-order {
#{50/$n}%,
#{100/$n}% { z-index: 1; }
#{100 - 100/$n}% { z-index: 2; }
}
@keyframes z-order-last {
#{50/$n}%,
#{100/$n}% { z-index: 1; }
#{100 - 50/$n}% { z-index: 2; }
}拿5张图来测试,把$n改成5,算出来:
- 滑动关键点:
50/5=10%向右,100/5=20%向左 z-order重置点:100-100/5=80%z-order-last重置点:100-50/5=90%
跑起来溜溜的,翻完一轮自动续上,不带卡顿。
方案二:加个随机旋转,更带感
想让效果更骚气一点?每张图片加个轻微旋转,模仿真实照片堆叠的随意感。
在Sass循环里生成随机角度:
@for $i from 1 to ($n + 1) {
.gallery > img:nth-child(#{$i}) {
--r: #{(-20 + random(40)) * 1deg}; // -20deg 到 20deg 之间随机
}
}然后在滑动动画的transform里加上旋转:
@keyframes slide {
#{50/$n}% { transform: translateX(120%) rotate(var(--r)); }
#{100/$n}% { transform: translateX(0%) rotate(var(--r)); }
}注意旋转角度要一直带着,否则图片归位时会突然扭一下。不过加了旋转之后,偶尔会看到某张图片在z-index切换的瞬间闪一下,不影响整体观感,反而有种手抖的真实感。
实际操作中的几个坑
写这个效果的时候,动画延迟的计算容易算错。如果N不是3的倍数,calc表达式里括号不能省,比如calc(#{(1 - $i)/$n} * 6s),少了乘号或者括号Sass就会报错。
滑动距离用120%而不是100%,是因为图片滑出去之后,如果刚好卡在100%的位置,边缘可能还露一点点。多给20%的余量,确保完全移出父容器范围。
z-index动画必须用steps(1),如果用默认的ease,z-index会在两个关键帧之间平滑过渡,但z-index本身是离散值,浏览器只能取整,结果就是动画中途z-index乱跳。steps(1)强制在那一帧瞬间完成切换,干净利落。
最后一张图的特殊动画很容易被忽略。如果所有图都用同一套关键帧,翻到第一张之后,最后一张会提前冒出来,顺序就全乱了。单独处理最后一张,把重置点往后推,才能保持循环顺畅。
