CSS宝丽来照片堆叠翻转效果,咋实现无限循环翻转?

3,989字
17–25 分钟
in

一堆宝丽来照片叠在一起,一张一张往外翻,翻到最后又自动循环回来,这种效果看着挺酷吧?其实不用JS,光靠CSS的z-indextransform就能整出来。这里头最关键的是搞懂堆叠顺序咋变化,不然动画就会翻车。

目录

基本结构

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,所以最后一张图在最上面。流程是这样:

  1. 把最上面的图往右滑出去,露出下面那张。
  2. 滑出去的同时把它的z-index降到1,让它沉底。
  3. 再把它滑回来,这时候它已经跑到堆叠的最下面了。
  4. 重复这个过程,每张图轮流滑一遍。

听起来简单,但实际操作时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),如果用默认的easez-index会在两个关键帧之间平滑过渡,但z-index本身是离散值,浏览器只能取整,结果就是动画中途z-index乱跳。steps(1)强制在那一帧瞬间完成切换,干净利落。

最后一张图的特殊动画很容易被忽略。如果所有图都用同一套关键帧,翻到第一张之后,最后一张会提前冒出来,顺序就全乱了。单独处理最后一张,把重置点往后推,才能保持循环顺畅。