CSS 无限滑块翻转极光图像

Avatar of Temani Afif
Temani Afif

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 $200 免费积分!

在上一篇文章中,我们制作了一个非常酷的小滑块(或者如果你愿意,也可以称之为“轮播”),它以圆形方向旋转。这次,我们将制作一个通过一堆极光图像翻转的滑块。

酷吧?现在先不要看代码,因为有很多东西要解开。跟我一起,好吗?

CSS 滑块系列

基本设置

此滑块的大多数 HTML 和 CSS 与我们上次制作的圆形滑块类似。实际上,我们使用的是完全相同的标记

<div class="gallery">
  <img src="" alt="">
  <img src="" alt="">
  <img src="" alt="">
  <img src="" alt="">
</div>

这是基本 CSS,它将父 .gallery 容器设置为一个网格,其中所有图像都一个叠在另一个上面

.gallery  {
  display: grid;
  width: 220px; /* controls the size */
}
.gallery > img {
  grid-area: 1 / 1;
  width: 100%;
  aspect-ratio: 1;
  object-fit: cover;
  border: 10px solid #f2f2f2;
  box-shadow: 0 0 4px #0007;
}

到目前为止,还没有复杂的东西。即使是图像的极光样式,我使用的也只是一些 borderbox-shadow。您可能能够做得更好,因此请随意玩弄这些装饰风格!我们将把重点放在动画上,这是最棘手的部分。

诀窍是什么?

此滑块的逻辑依赖于图像的堆叠顺序——因此,是的,我们将使用 z-index。所有图像都从相同的 z-index 值(2)开始,这在逻辑上将使堆栈中的最后一张图像位于顶部。

我们取最后一张图像,将其向右滑动,直到它露出堆栈中的下一张图像。然后,我们降低图像的 z-index 值,然后将其滑回牌组。由于它的 z-index 值低于其他图像,因此它成为堆栈中的最后一张图像。

这是一个简化的演示,展示了这个技巧。将鼠标悬停在图像上以激活动画

现在,想象一下将相同的技巧应用于所有图像。如果我们使用 :nth-child() 伪选择器来区分图像,那么这里就是模式

  • 我们滑动最后一张图像(N)。下一张图像可见(N - 1)。
  • 我们滑动下一张图像(N - 1)。下一张图像可见(N - 2
  • 我们滑动下一张图像(N - 2)。下一张图像可见(N - 3
  • (我们继续相同的过程,直到到达第一张图像)
  • 我们滑动第一张图像(1)。最后一张图像(N)再次可见。

这就是我们的无限滑块!

剖析动画

如果您还记得上一篇文章,我仅定义了一个动画并使用延迟来控制每个图像。我们在这里将做同样的事情。首先,让我们尝试可视化动画的时间线。我们将从三个图像开始,然后将其推广到任意数量(N)的图像。

Diagramming the three parts of the animation.

我们的动画分为三个部分:“向右滑动”、“向左滑动”和“不动”。我们可以轻松地识别出每个图像之间的延迟。如果我们假设第一张图像从 0s 开始,持续时间等于 6s,那么第二张图像将从 -2s 开始,第三张图像将从 -4s 开始。

.gallery > img:nth-child(2) { animation-delay: -2s; } /* -1 * 6s / 3 */
.gallery > img:nth-child(3) { animation-delay: -4s; } /* -2 * 6s / 3 */

我们还可以看到,“不动”部分占整个动画的三分之二(2*100%/3),而“向右滑动”和“向左滑动”部分一起占三分之一——因此,每个部分都等于总动画的 100%/6

我们可以这样编写动画关键帧

@keyframes slide {
  0%     { transform: translateX(0%); }
  16.67% { transform: translateX(120%); }
  33.34% { transform: translateX(0%); }
  100%   { transform: translateX(0%); } 
}

那个 120% 是一个任意值。我需要一个大于 100% 的值。图像需要从其他图像旁边向右滑动。为此,它至少需要移动其大小的 100%。这就是为什么我使用了 120%——以获得一些额外的空间。

现在我们需要考虑 z-index。不要忘记,我们需要在图像向右滑动到堆栈之外之后以及在将其滑回堆栈底部之前更新图像的 z-index 值。

@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; } /* we update the z-order here */
  33.34% { transform: translateX(0%);   z-index: 1; }
  100%   { transform: translateX(0% );  z-index: 1; }  
}

我们不是在时间线上的 16.67%100%/6)点定义一个状态,而是在几乎相同的位置(16.66%16.67%)定义两个状态,在将图像滑回牌组之前,z-index 值会降低。

当我们将所有这些结合在一起时,就会发生以下情况

嗯,滑动部分似乎工作正常,但堆叠顺序全部乱了!动画开始得很好,因为顶部的图像正在移动到后面……但随后的图像并没有随之移动。如果您注意,序列中的第二张图像在下一张图像闪烁到顶部之前,返回到堆栈的顶部。

我们需要仔细跟踪 z-index 的变化。最初,所有图像的 z-index 都是 2。这意味着堆叠顺序应该是……

Our eyes 👀 --> 3rd (2) | 2nd (2) | 1st (2)

我们滑动第三张图像并将其 z-index 更新为以下顺序

Our eyes 👀 --> 2nd (2) | 1st (2) | 3rd (1)

我们对第二张图像做同样的事情

Our eyes 👀 --> 1st (2) | 3rd (1) | 2nd (1)

……以及第一张图像

Our eyes 👀 --> 3rd (1) | 2nd (1) | 1st (1)

我们这样做,一切似乎都很好。但实际上,并非如此!当第一张图像移动到后面时,第三张图像将开始另一次迭代,这意味着它将返回到 z-index: 2

Our eyes 👀 --> 3rd (2) | 2nd (1) | 1st (1)

所以,实际上我们从未将所有图像都设置为 z-index: 2!当图像没有移动(即动画的“不动”部分)时,z-index1。如果我们滑动第三张图像并将它的 z-index 值从 2 更新为 1,它将停留在顶部!当所有图像具有相同的 z-index 时,DOM 中出现的最后一个图像——在本例中为我们的第三个图像——位于堆栈的顶部。滑动第三张图像会导致以下结果

Our eyes 👀 --> 3rd (1) | 2nd (1) | 1st (1)

第三张图像仍然在顶部,而且就在它之后,我们移动第二张图像到顶部,当它的动画在 z-index: 2 处重新开始时

Our eyes 👀 --> 2nd (2) | 3rd (1) | 1st (1)

一旦我们将其滑动,我们就会得到

Our eyes 👀 --> 3rd (1) | 2nd (1) | 1st (1)

然后第一张图像将跳到顶部

Our eyes 👀 --> 1st(2) | 3rd (1) | 2nd (1)

好吧,我迷路了。所有的逻辑都错了?

我知道,这令人困惑。但我们的逻辑并非完全错误。我们只需要稍微修正一下动画,才能使一切按我们想要的方式工作。诀窍是正确地重置 z-index

让我们看一下第三张图像位于顶部的场景

Our eyes 👀 -->  3rd (2) | 2nd (1) | 1st (1)

我们看到滑动第三张图像并更改它的 z-index 会使它保持在顶部。我们需要做的是更新第二张图像的 z-index。因此,在我们滑动第三张图像远离牌组之前,我们将第二张图像的 z-index 更新为 2

换句话说,我们在动画结束之前重置第二张图像的 z-index

Diagramming the parts of the animation with indicators for where z-index is increased or decreased.

绿色加号表示将 z-index 增加到 2,红色减号与 z-index: 1 相对应。第二张图像以 z-index: 2 开始,然后在我们将其滑动到牌组之外时,将其更新为 1。但在第一张图像滑动到牌组之外之前,我们将第二张图像的 z-index 更改回 2。这将确保两张图像具有相同的 z-index,但仍然,第三张图像会保持在顶部,因为它在 DOM 中出现得更晚。但在第三张图像滑动并更新其 z-index 之后,它会移动到底部。

这是动画的三分之二,因此让我们相应地更新关键帧

@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; } /* we update the z-order here */
  33.34% { transform: translateX(0%);   z-index: 1; }
  66.33% { transform: translateX(0%);   z-index: 1; }
  66.34% { transform: translateX(0%);   z-index: 2; } /* and also here */
  100%   { transform: translateX(0%);   z-index: 2; }  
}

好了一些,但仍然没有完全到位。还有一个问题……

哦,不,这永远不会结束!

别担心,我们不会再修改关键帧了,因为这个问题只在最后一幅图像出现时才会发生。我们可以针对最后一幅图像制作一个“特殊”的关键帧动画来解决问题。

当第一幅图像在最上面时,我们会遇到以下情况

Our eyes 👀 -->  1st (2) | 3rd (1) | 2nd (1)

考虑到我们之前做的调整,第三幅图像会在第一幅图像滑动之前跳到最上面。这只会发生在这种情况下,因为在第一幅图像之后移动的下一幅图像是在 DOM 中具有更高顺序的*最后一幅*图像。其他图像都很好,因为我们有 `N`,然后是 `N - 1`,然后从 `3` 到 `2`,从 `2` 到 `1`... 但然后从 `1` 到 `N`。

为了避免这种情况,我们将对最后一幅图像使用以下关键帧

@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; } /* we update the z-order here */
  33.34% { transform: translateX(0%);   z-index: 1; }
  83.33% { transform: translateX(0%);   z-index: 1; }
  83.34% { transform: translateX(0%);   z-index: 2; } /* and also here */
  100%   { transform: translateX(0%);   z-index: 2; }
}

我们在动画的 5/6 处(而不是三分之二)重置了 `z-index` 值,此时第一幅图像已移出堆栈。因此我们不会看到任何跳动!

瞧!我们的无限滑块现在完美了!以下是我们最终代码的全部内容

.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; }

@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; }
}
@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; }
}

支持任意数量的图像

现在我们的动画适用于三幅图像,让我们让它适用于任意数量(`N`)的图像。但首先,我们可以通过拆分动画来优化我们的工作,以避免冗余

.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;
}
.gallery > img:nth-child(2) { animation-delay: -2s; } 
.gallery > img:nth-child(3) { animation-delay: -4s; }

@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; }
}

现在代码少多了!我们为滑动部分制作了一个动画,另一个用于 `z-index` 更新。请注意,我们在 `z-index` 动画中使用了 `steps(1)`。这是因为我希望突然更改 `z-index` 值,与滑动动画不同,我们希望滑动动画是平滑的。

现在代码更易于阅读和维护,我们可以更好地了解如何支持任意数量的图像。我们需要做的是更新动画延迟和关键帧的百分比。延迟很容易,因为我们可以使用我们在上一篇文章中创建的相同循环来支持循环滑块中的多个图像

@for $i from 2 to ($n + 1) {
  .gallery > img:nth-child(#{$i}) {
    animation-delay: calc(#{(1 - $i)/$n}*6s);
  }
}

这意味着我们将从普通 CSS 转换为 Sass。接下来,我们需要想象时间线如何随着 `N` 个图像而变化。别忘了,动画分为三个阶段

Showing the three parts of the animation in a series of lines with arrows.

在“向右滑动”和“向左滑动”之后,图像应该保持静止,直到其他图像完成整个序列。因此,“不移动”部分需要与(`N - 1`)个“向右滑动”和“向左滑动”一样长。在一个迭代中,将有 `N` 个图像滑动。因此,“向右滑动”和“向左滑动”各占总动画时间线的 `100%/N`。图像从堆栈中滑出是在 `(100%/N)/2`,滑回是在 `100%/N`。

我们可以更改这个

@keyframes slide {
  16.67% { transform: translateX(120%); }
  33.33% { transform: translateX(0%); }
}

... 变成这样

@keyframes slide {
  #{50/$n}%  { transform: translateX(120%); }
  #{100/$n}% { transform: translateX(0%); }
}

如果我们将 `N` 替换为 `3`,在堆栈中有 `3` 个图像时,我们将得到 `16.67%` 和 `33.33%`。堆叠顺序的逻辑相同,我们将有

@keyframes z-order {
  #{50/$n}%,
  #{100/$n}% { z-index: 1; }
  66.33% { z-index: 2; }
}

我们还需要更新 `66.33%` 点。这应该是图像在动画结束前重置其 `z-index` 的位置。与此同时,下一幅图像开始滑动。由于滑动部分占用 `100%/N`,因此重置应该发生在 `100% - 100%/N`

@keyframes z-order {
  #{50/$n}%,
  #{100/$n}% { z-index: 1; }
  #{100 - 100/$n}% { z-index: 2; }
}

但为了使我们的 `z-order-last` 动画起作用,它应该在序列中稍微延迟一些。还记得我们对最后一幅图像进行的修复吗?重置 `z-index` 值需要在第一幅图像移出堆栈时发生,而不是在它开始滑动时发生。我们可以在关键帧中使用相同的推理

@keyframes z-order-last {
  #{50/$n}%,
  #{100/$n}% { z-index: 1; }
  #{100 - 50/$n}% { z-index: 2; }
}

我们完成了!以下是在使用五幅图像时的效果

我们可以添加一点旋转,让它更有趣

我所做的只是将 `rotate(var(--r))` 附加到 `transform` 属性中。在循环中,`--r` 使用随机角度定义

@for $i from 1 to ($n + 1) {
  .gallery > img:nth-child(#{$i}) {
    --r: #{(-20 + random(40))*1deg}; /* a random angle between -20deg and 20deg */
  }
}

旋转会产生一些小故障,因为我们有时可以看到一些图像跳到堆栈的后面,但这没什么大不了的。

总结

所有这些 `z-index` 工作都是一个巨大的平衡行为,对吧?如果你以前不确定堆叠顺序是如何工作的,那么现在你可能已经有了更好的理解!如果你发现某些解释难以理解,我强烈建议你重新阅读这篇文章,并使用纸笔绘制出所有内容。尝试使用不同数量的图像来演示动画的每个步骤,以便更好地理解这个技巧。

上次,我们使用了一些几何技巧创建了一个循环滑块,它在完成整个序列后会旋转回第一幅图像。这次,我们使用 `z-index` 完成了类似的技巧。在这两种情况下,我们都没有复制任何图像来模拟连续动画,也没有使用 JavaScript 来帮助进行计算。

下次,我们将制作 3D 滑块。敬请关注!