CSS无限3D滑块

Avatar of Temani Afif
Temani Afif

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 200 美元的免费额度!

在本系列文章中,我们一直在使用仅 HTML 和 CSS 创建图像滑块。我们的想法是,无论我们添加多少图像,我们都可以使用相同的标记但不同的 CSS 来获得截然不同的结果。我们从一个无限旋转的圆形滑块开始,有点像一个装有图像的指尖陀螺。然后我们制作了一个可以翻阅照片堆栈的滑块。

这一次,我们将深入探索第三维度。乍一看似乎很难,但我们正在查看的许多代码与本系列前两篇文章中使用的代码完全相同,只是进行了一些修改。因此,如果您现在才开始阅读本系列文章,我建议您查看其他文章,以了解此处使用的概念。

CSS滑块系列

这是我们的目标

乍一看,它看起来像一个带有四张图像的旋转立方体。但实际上,我们总共处理六张图像。以下是滑块从另一个角度拍摄的照片

现在我们对图像的排列方式有了很好的视觉效果,让我们剖析代码,看看我们是如何实现的。

基本设置

与我们用于其他滑块的其余滑块相同的 HTML

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

再次强调,我们使用 CSS Grid 将图像放置在一个堆栈中,一个叠放在另一个上面

.gallery {
  display: grid;
}
.gallery > img {
  grid-area: 1 / 1;
  width: 160px;
  aspect-ratio: 1;
  object-fit: cover;
}

动画

此滑块的逻辑与第一篇文章中的圆形滑块非常相似。实际上,如果您再次查看上面的视频,您会发现图像的放置方式会创建一个多边形。完整旋转后,它会返回到第一张图像。

我们依赖 CSS 的transform-originanimation-delay 属性来创建第一个滑块。相同的动画应用于所有图像元素,这些元素围绕同一个点旋转。然后,通过使用不同的延迟,我们正确地将所有图像放置在一个大圆圈周围。

我们的 3D 滑块的实现将略有不同。使用transform-origin 在此处不起作用,因为我们正在 3D 中工作,因此我们将改为使用transform 正确放置所有图像,然后旋转容器。

我们再次使用 Sass,以便我们可以循环遍历图像数量并应用我们的转换

@for $i from 1 to ($n + 1) {
  .gallery > img:nth-child(#{$i}) {
     transform: 
       rotate(#{360*($i - 1) / $n}deg) /* 1 */
       translateY(50% / math.tan(180deg / $n)) /* 2 */ 
       rotateX(90deg); /* 3 */
  }
}

您可能想知道为什么我们直接跳到 Sass。在其他文章中,我们首先使用原生 CSS 使用固定数量的图像,然后使用 Sass 将代码泛化以适应任意数量 (N) 的图像。好吧,我想您现在明白了,我们可以省略所有这些发现工作,直接进入实际实现。

transform 属性采用三个值,我在这里进行了说明

Showing the three phases of the image slider layout.

我们首先将所有图像彼此旋转。旋转角度取决于图像的数量。对于N 张图像,我们的增量等于360deg/N。然后我们translate 所有图像相同的量,使它们的中心点在侧面相遇。

Showing the stack of images arranged flat in a circle with a red line running through the center point of the images.

有一些枯燥的几何知识可以帮助解释这一切是如何工作的,但距离等于50%/tan(180deg/N)。在制作圆形滑块时(transform-origin: 50% 50%/sin(180deg/N)),我们处理了类似的等式。

最后,我们将图像绕 x 轴旋转90deg 以获得我们想要的排列。以下视频说明了最后一次旋转的作用

现在我们要做的就是旋转整个容器来创建我们的无限滑块。

.gallery {
  transform-style: preserve-3d;
  --_t: perspective(280px) rotateX(-90deg);
  animation: r 12s cubic-bezier(.5, -0.2, .5, 1.2) infinite;
}
@keyframes r {
  0%, 3% {transform: var(--_t) rotate(0deg); }
  @for $i from 1 to $n {
    #{($i/$n)*100 - 2}%, 
    #{($i/$n)*100 + 3}% {
      transform: var(--_t) rotate(#{($i / $n) * -360}deg);
    }  
  }
  98%, 100% { transform: var(--_t) rotate(-360deg); }
}

这段代码可能难以理解,所以让我们实际后退一步,重新审视我们为圆形滑块制作的动画。这是我们在第一篇文章中编写的代码

.gallery {
  animation: m 12s cubic-bezier(.5, -0.2, .5, 1.2) infinite;
}
@keyframes m {
  0%, 3% { transform: rotate(0); }
  @for $i from 1 to $n {
    #{($i / $n) * 100 - 2}%,
    #{($i / $n) * 100 + 3}% { 
      transform: rotate(#{($i / $n) * -360}deg);
    }  
  }
  98%, 100% { transform: rotate(-360deg); }
}

关键帧几乎相同。我们有相同的百分比值、相同的循环和相同的旋转。

为什么两者都相同?因为它们的逻辑相同。在这两种情况下,图像都排列在一个圆形形状周围,我们需要旋转整个形状以显示每个图像。这就是我能够复制圆形滑块的关键帧并将相同的代码用于我们的 3D 滑块的原因。唯一的区别是我们需要将容器沿 x 轴旋转-90deg 以查看图像,因为我们已经沿同一轴将它们旋转了90deg。然后我们添加一点perspective 来获得 3D 效果。

就是这样!我们的滑块完成了。以下是完整的演示。您只需添加任意数量的图像并更新一个变量即可启动它。

垂直3D滑块

既然我们在 3D 空间中玩耍,为什么不制作之前滑块的垂直版本呢?上一个滑块沿 z 轴旋转,但如果需要,我们也可以沿 x 轴移动。

如果您比较这两个滑块版本的代码,您可能不会立即发现差异,因为它只有一个字符!我在关键帧和图像transform 中将rotate() 替换为rotateX()。就是这样!

需要注意的是,rotate() 等效于rotateZ(),因此通过将轴从Z 更改为X,我们将滑块从水平版本转换为垂直版本。

立方体滑块

在 CSS 中谈论 3D 而不谈论立方体是不可能的。是的,这意味着我们将制作滑块的另一个版本。

此滑块版本背后的理念是使用图像创建实际的立方体形状,并在不同的轴周围旋转整个形状。由于它是立方体,因此我们处理六个面。我们将使用六张图像,每个立方体面一个。因此,没有 Sass,而是返回到原生 CSS。

这个动画有点让人不知所措,对吧?你甚至从哪里开始?

我们有六个面,因此我们需要执行至少六次旋转,以便每个图像都能轮到。实际上,我们需要五次旋转——最后一次旋转将我们带回到第一个图像面。如果您拿起一个魔方——或其他立方体形状的物体,如骰子——并用手旋转它,您就会对我们正在做的事情有一个很好的了解。

.gallery {
  --s: 250px; /* the size */

  transform-style: preserve-3d;
  --_p: perspective(calc(2.5*var(--s)));
  animation: r 9s infinite cubic-bezier(.5, -0.5, .5, 1.5);
}

@keyframes r {
  0%, 3%   { transform: var(--_p); }
  14%, 19% { transform: var(--_p) rotateX(90deg); }
  31%, 36% { transform: var(--_p) rotateX(90deg) rotateZ(90deg); }
  47%, 52% { transform: var(--_p) rotateX(90deg) rotateZ(90deg) rotateY(-90deg); }
  64%, 69% { transform: var(--_p) rotateX(90deg) rotateZ(90deg) rotateY(-90deg) rotateX(90deg); }
  81%, 86% { transform: var(--_p) rotateX(90deg) rotateZ(90deg) rotateY(-90deg) rotateX(90deg) rotateZ(90deg); }
  97%, 100%{ transform: var(--_p) rotateX(90deg) rotateZ(90deg) rotateY(-90deg) rotateX(90deg) rotateZ(90deg) rotateY(-90deg); }
}

transform 属性从零旋转开始,并且在每个状态下,我们在特定轴上附加一个新的旋转,直到我们达到六次旋转。然后我们回到第一张图像。

别忘了我们图像的放置。每个图像都使用transform 应用于立方体的一个面上

.gallery img {
  grid-area: 1 / 1;
  width: var(--s);
  aspect-ratio: 1;
  object-fit: cover;
  transform: var(--_t,) translateZ(calc(var(--s) / 2));
}
.gallery img:nth-child(2) { --_t: rotateX(-90deg); }
.gallery img:nth-child(3) { --_t: rotateY( 90deg) rotate(-90deg); }
.gallery img:nth-child(4) { --_t: rotateX(180deg) rotate( 90deg); }
.gallery img:nth-child(5) { --_t: rotateX( 90deg) rotate( 90deg); }
.gallery img:nth-child(6) { --_t: rotateY(-90deg); }

您可能认为我在那里使用的值背后存在奇怪的复杂逻辑,对吧?不,没有。我所做的只是打开 DevTools 并对每个图像使用不同的旋转值进行尝试,直到我得到正确的结果。这听起来可能很愚蠢,但嘿,它有效——尤其是在我们有固定数量的图像并且我们不需要支持N 个图像的东西时。

事实上,忘记我使用的值,并尝试自己完成放置作为练习。从所有图像堆叠在一起开始,打开 DevTools,然后开始吧!您最终可能会得到不同的代码,这完全没问题。图像的位置可以有多种方式。

var() 内部的逗号有什么诀窍?是打错字了吗?

这不是打错字,所以不要删除它!如果您确实删除了它,您会注意到它会影响第一张图像的位置。您可以在我的代码中看到,我为除第一张图像之外的所有图像定义了--_t,因为我只需要为此图像进行平移。该逗号使变量回退到空值。 如果没有逗号,我们将没有回退,并且整个值将无效。

来自规范

注意:也就是说,var(--a,) 是一个有效的函数,它指定如果 --a 自定义属性无效或缺失,则 var() 应替换为空。

随机魔方滑块

一点随机性可以很好地增强这种动画效果。因此,与其按顺序旋转魔方,不如说我们掷骰子,让魔方随意滚动。

很酷吧?我不知道你们是怎么想的,但我更喜欢这个版本!它更有趣,过渡也令人满意。猜猜怎么着?您可以玩弄这些值来创建您自己的随机魔方滑块!

逻辑实际上一点也不随机——它只是看起来那样。您在每个关键帧上定义一个 transform,以便显示一个面……嗯,就是这样!您可以选择任何您想要的顺序。

@keyframes r {
  0%, 3%   { transform: var(--_p) rotate3d( 0, 0, 0,  0deg); }
  14%,19%  { transform: var(--_p) rotate3d(-1, 1, 0,180deg); }
  31%,36%  { transform: var(--_p) rotate3d( 0,-1, 0, 90deg); }
  47%,52%  { transform: var(--_p) rotate3d( 1, 0, 0, 90deg); }
  64%,69%  { transform: var(--_p) rotate3d( 1, 0, 0,-90deg); }
  81%,86%  { transform: var(--_p) rotate3d( 0, 1, 0, 90deg); }
  97%,100% { transform: var(--_p) rotate3d( 0, 0, 0,  0deg); }
}

这次我使用了 rotate3d(),但仍然依赖于 DevTools 来找到对我来说“合适”的值。不要试图找到关键帧之间的关系,因为根本不存在。我正在定义单独的转换,然后观察“随机”结果。确保第一张图片分别是第一帧和最后一帧,并在其他每一帧上显示不同的图片。

您没有义务像我一样使用 rotate3d() 转换。您也可以像我们在前面的示例中那样链接不同的旋转。玩一玩,看看你能想出什么!我将等待您在评论区与我分享您的版本!

总结

希望您喜欢这个小系列。我们在构建一些有趣的(和滑稽的)滑块的同时,也学习了各种 CSS 概念——从网格布局和堆叠顺序到动画延迟和转换。我们甚至还玩了一点 Sass 来循环遍历元素数组。

而且我们都是使用完全相同的 HTML 为每个滑块创建的。这有多酷?CSS 非常强大,能够在没有 JavaScript 的帮助下完成很多事情。