使用 CSS 重现乐一通中的猪小弟动画

Avatar of Kilian Valkhof
Kilian Valkhof

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

你知道,猪小弟从那些红色圆环中出现,宣布乐一通卡通的结束。 我们会做到这一点,但首先我们需要了解一些 CSS 概念。

CSS 中的所有内容都是一个盒子,或者说是矩形。 矩形可以堆叠,并且可以显示在其他矩形的顶部或底部。 矩形可以包含其他矩形,并且您可以对其进行样式设置,使内部矩形在外部矩形之外可见(因此它们溢出)或被外部矩形裁剪(使用overflow: hidden)。 到目前为止,一切都很好。

如果您希望矩形在其周围矩形之外可见,但仅在一侧可见,该怎么办? 那是不可能的,对吧?

The first rectangle contains an inner element that overflows both the top and bottom edges with the text "Possible" below it. The second rectangle clips the inner element on both sides, with "Also possible" below it. The third rectangle clips the inner element on the bottom, but shows it overflowing at the top, with the text "...Not possible" below it.

也许,当您查看上面的图片时,思路开始运转:如果我复制内部矩形并裁剪其一半,然后精确地定位它呢?。 但归根结底,您无法选择让元素在顶部溢出但在底部裁剪。

或者可以吗?

3D 变换

使用 3D 变换,您可以在 3D 空间中旋转、变换和平移元素。 这里有一组我收集的实用示例,展示了一些可能性。

为了让 3D 变换发挥作用,您需要两个 CSS 属性

  • perspective,使用像素值来确定 3D 效果的突出程度
  • transform-style: preserve-3d,告诉浏览器在 3D 空间中保持元素的位置。

即使 3D 变换具有良好的支持,但遗憾的是,您并没有在“野外”看到太多 3D 变换。 网站仍然是“2D”事物,一个可以滚动的平面页面。 但是当我开始使用 3D 变换并寻找示例时,我发现了一个就 3D 变换而言最有趣的示例

Three planes floating above each other in 3D space

图像清楚地显示了三个平面,但此效果是使用单个<div>实现的。 其他两个平面是::before::after伪元素,它们分别使用translate()向上和向下移动,以在 3D 空间中彼此堆叠。 这里值得注意的是,::after元素通常会定位在元素的顶部,但现在位于该元素的后面。 创建者能够通过添加transform: translateZ(-1px);来实现这一点。

即使这是我当时见过的众多 3D 变换之一,但它也是第一个让我意识到我实际上是在 3D 空间中定位元素的变换。 如果我能做到这一点,我也可以使元素相交

Two planes intersecting each other in 3D space

我无法想到这种方法有什么用,但后来我看到了猪小弟的卡通动画。 他从底部框架后面出现,但他的脸部重叠并在同一框架的顶部边缘堆叠——这与我们之前看到的完全相同的裁剪情况。 这时我的思路开始运转。 我可以仅使用 CSS 复制该效果吗? 为了获得额外的奖励,我可以用单个<div>复制它吗?

我开始尝试,并很快得到了以下结果

An orange rectangle that intersects through a blue frame. At the top of the image it's above the frame and at the bottom of the image it's below the blue frame.

这里我们有一个带有其::before::after伪元素的单个<div>。 div 本身是透明的,::before有一个蓝色边框,::after已沿 x 轴旋转。 因为 div 具有perspective,所以所有内容都位于 3D 中,并且因此,::after伪元素位于框架顶部边缘边框的上方,并且位于框架底部边缘边框的后面

以下是代码

div {
  transform: perspective(3000px);
  transform-style: preserve-3d;
  position: relative;
  width: 200px;
  height: 200px;
}

div::before {
  content: "";
  width: 100%;
  height: 100%;
  border:10px solid darkblue;
}

div::after {
  content: "";
  position: absolute;
  background: orangered;
  width: 80%;
  height: 150%;
  display: block;
  left: 10%;
  bottom: -25%;
  transform: rotateX(-10deg);
}

使用perspective,我们可以确定观察者距离“z=0”有多远,我们可以将其视为 CSS 3D 空间的“地平线”。 透视越大,3D 效果越不明显,反之亦然。 对于大多数 3D 场景,perspective值介于 500 和 1,000 像素之间效果最佳,尽管您可以对其进行调整以获得所需的精确效果。 您可以将其与透视绘图进行比较:如果您绘制两个彼此靠近的地平线点,您将获得非常强的透视效果;但如果它们相距较远,则事物看起来会更平坦。

从矩形到卡通

矩形很有趣,但我真正想要构建的是这样的东西

A film cell of Porky Pig coming out of a circle with the text "That's all folks."

我无法从该图像中找到或创建猪小弟的精美剪裁版本,但维基百科页面包含一个不错的替代方案,所以我们将使用它。

首先,我们需要将图像分成三个部分

  • <div>:猪小弟背后的蓝色背景
  • ::after:形成某种隧道的所有红色圆圈
  • ::before:猪小弟本人,以其全部荣耀设置为了背景图片

我们将从<div>开始。 它将作为背景,也是其他元素的基础。 它还将包含我之前提到的perspectivetransform-style属性,以及一些尺寸和背景颜色

div {
  transform: perspective(3000px);
  transform-style:preserve-3d;
  position: relative;
  width: 200px;
  height: 200px;
  background: #4992AD;
}

好的,接下来,我们将转向红色圆圈。 元素本身必须是透明的,因为那是猪小弟出现的开口。 那么我们应该如何处理呢? 我们可以像本文前面示例中那样使用边框,但我们只有一个边框,它可以具有纯色。 我们需要一堆可以接受渐变的圆圈。 我们可以使用box-shadow代替,在属性值中链接多个阴影。 这为我们提供了所有需要的圆圈,并且通过对具有较大扩展半径的模糊半径值使用0,我们可以创建多个“边框”的外观。

box-shadow: <x-offset> <y-offset> <blur-radius> <spread-radius> <color>;

我们将使用一个与<div>本身一样大的border-radius,使::before成为一个圆圈。 然后我们将添加阴影。 当我们添加几个具有较大扩展的红色圆圈并添加模糊的白色时,我们会得到一个非常类似于猪小弟隧道的效果。

box-shadow: 0 0 20px   0px #fff, 0 0 0  30px #CF331F,
            0 0 20px  30px #fff, 0 0 0  60px #CF331F,
            0 0 20px  60px #fff, 0 0 0  90px #CF331F,
            0 0 20px  90px #fff, 0 0 0 120px #CF331F,
            0 0 20px 120px #fff, 0 0 0 150px #CF331F;

在这里,我们添加了五个圆圈,每个圆圈宽30px。 每个圆圈都有一个纯红色的背景。 并且,通过在顶部使用模糊半径为20px的白色阴影,我们创建了渐变效果。

The background and circles in pure CSS without Porky

在背景和圆圈排序后,我们现在将添加猪小弟。 让我们首先将他添加到我们希望他最终到达的位置,现在位于圆圈的上方。

div::before {
  position: absolute;
  content: "";
  width: 80%;
  height: 150%;
  display: block;
  left: 10%;
  bottom: -12%;
  background: url("Porky_Pig.svg") no-repeat center/contain;
}

您可能已经注意到“center/contain”中用于background的斜杠。 这是在background速记 CSS 属性中同时设置位置(center)和大小(contain)的语法。 斜杠语法也用于font速记 CSS 属性中,它用于设置font-sizeline-height,如下所示:<font-size>/<line-height>

斜杠语法将在 CSS 的未来版本中得到更多使用。 例如,更新的rgb()hsl()颜色语法可以在后面加上一个斜杠,然后跟一个数字来指示不透明度,如下所示:rgb(0 0 0 / 0.5)。 这样,就无需在rgb()rgba()之间切换。 这已经在所有浏览器中都能正常工作,除了 Internet Explorer 11。

Porky Pig positioned above the circles

此处的大小和定位有点随意,因此请根据需要进行调整。 我们离想要的结果已经很接近了,但现在需要让猪小弟的底部部分位于红色圆圈后面,而他的上半身仍然可见。

技巧

我们需要在 3D 空间中转换圆圈和猪小弟。 如果我们想旋转猪小弟,我们需要满足一些要求

  • 他不能穿过背景裁剪。
  • 我们不应该旋转得太厉害,以至于图像变形。
  • 他的下半身应该在红色圆圈下方,上半身应该在红色圆圈上方。

为了确保猪小弟不会穿过背景裁剪,我们首先沿 Z 方向移动圆圈,使它们看起来更靠近观察者。 因为应用了preserve-3d,所以这意味着它们也会稍微放大,但如果我们只移动一点点,则放大效果不会很明显,并且我们会在背景和圆圈之间留下足够的空间

transform: translateZ(20px);

现在是猪小弟。 我们将围绕 X 轴旋转他,导致他的上半身向我们靠近,下半身远离我们。 我们可以用以下方法做到这一点

transform: rotateX(-10deg);

起初这看起来很糟糕。 猪小弟的部分身体隐藏在蓝色背景后面,而且他还在以奇怪的方式穿过圆圈裁剪。

Porky Pig partially clipped by the background and the circles

我们可以通过使用translateZ()将猪小弟“靠近”我们(就像我们对圆圈所做的那样)来解决此问题,但更好的解决方案是更改旋转点的位 置。 现在它发生在图像的中心,导致图像的下半部分旋转远离我们。

如果我们将旋转的起点移到图像的底部,甚至稍微低于底部,那么整个图像都会向我们旋转。 因为我们已经将圆圈移近我们,所以最终一切看起来都应该正确

transform: rotateX(-10deg);
transform-origin: center 120%;
Porky Pig emerges from the circle, with his legs behind the circles but his head above them.

要了解 3D 中所有内容的工作原理,请在以下 Pen 中点击“显示调试”

动画

如果我们保持现状——静态图像——那么我们就无需经历所有这些麻烦。但是,当我们为事物添加动画时,就可以展现分层效果并增强视觉效果。

这是我想要实现的动画:猪小弟一开始很小,位于圆圈后面底部,然后放大,从蓝色背景中浮现出来,覆盖在红色圆圈上。他会在那里停留一段时间,然后再次移出。

我们将使用transform来实现动画,以获得最佳性能。并且由于我们正在这样做,因此也需要确保rotateX保留在其中。

@keyframes zoom {
  0% {
    transform: rotateX(-10deg) scale(0.66);
  }
  40% {
    transform: rotateX(-10deg) scale(1);
  }
  60% {
    transform: rotateX(-10deg) scale(1);
  }
  100% {
    transform: rotateX(-10deg) scale(0.66);
  }
}

很快,我们将能够直接设置不同的变换,因为浏览器已开始将它们作为单独的 CSS 属性实现。这意味着重复rotateX(-10deg)最终将变得不必要;但目前,我们存在一些重复。

我们使用scale()函数进行放大和缩小,并且由于我们已经设置了transform-origin,因此缩放会从图像的中心底部开始,这正是我们想要的效果!我们将动画的缩放比例放大到猪小弟实际尺寸的 60%,并在最大点处有一个小停顿,此时他完全从圆形框架中弹出来。

动画作用于::before伪元素。为了使动画看起来更自然一些,我们使用了ease-in-out时间函数,该函数会在动画开始和结束时减慢速度。

div::before {
  animation-name: zoom;
  animation-duration: 4s;
  animation-iteration-count: infinite;
  animation-fill-mode:forwards;
  animation-timing-function: ease-in-out;
}

减少运动怎么办?

很高兴你问到!对于对动画敏感并希望减少或不使用运动的人,我们可以使用prefers-reduced-motion媒体查询。我们不会完全移除动画,而是针对那些更喜欢减少运动的人,并使用更细微的淡入淡出效果,而不是完整的动画。

@media (prefers-reduced-motion: reduce) {
   @keyframes zoom {
    0% {
      opacity:0;
    }
    100% {
      opacity: 1;
    }
  }

  div::before {
    animation-iteration-count: 1;
  }
}

通过在媒体查询中覆盖@keyframes,浏览器会自动获取它。这样,我们仍然可以强调猪小弟从圆圈中浮现出来的效果。通过将animation-iteration-count设置为1,我们仍然让人们看到效果,但随后停止以防止持续运动。

收尾工作

我们还可以做两件事让这个动画更有趣

  • 我们可以通过在猪小弟后面添加一个阴影来创建更深的图像,随着他出现并看起来更靠近视野,阴影会逐渐变大。
  • 当猪小弟移动时,我们可以让他转动,以进一步美化弹出效果。

我们可以使用同一个动画中的rotateZ()来实现第二部分。非常简单。

但是第一部分需要额外的技巧。因为我们使用图像来显示猪小弟,所以我们不能使用box-shadow,因为这会在::before伪元素的框周围而不是猪小弟的形状周围创建阴影。

这时filter: drop-shadow() 可以派上用场。它会查看元素的不透明部分,并为其添加阴影,而不是在框周围添加阴影。

@keyframes zoom {
  0% {
    transform: rotateX(-10deg) scale(0.66);
    filter: drop-shadow(-5px 5px 5px rgba(0,0,0,0));
  }
  40% {
    transform: rotateZ(-10deg) rotateX(-10deg) scale(1);
    filter: drop-shadow(-10px 10px 10px rgba(0,0,0,0.5));
  }

  60% {
    transform: rotateZ(-10deg) rotateX(-10deg) scale(1);
    filter: drop-shadow(-10px 10px 10px rgba(0,0,0,0.5));
  }

  100% {
    transform: rotateX(-10deg) scale(0.66);
    filter: drop-shadow(-5px 5px 5px rgba(0,0,0,0));
  }
}

这就是我如何重新创建乐一通动画中的猪小弟动画。我现在所能说的就是,“这就是全部啦!”