使用 CSS 3D 时要注意的事项

Avatar of Ana Tudor
Ana Tudor 发布

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

我一直很喜欢 3D 几何图形。 我一发现 CSS 对 3D 变换的支持变得越来越好,就开始使用它了。 虽然用变换来创建 2D 形状,然后在 3D 中移动/旋转它们来创建多面体感觉很自然,但最初有一些东西让我很头疼。 我认为我可以写一些关于让我感到惊讶的事情以及我遇到的挑战,这样你就可以避免这些问题了。

3D 渲染上下文

我清楚地记得,我第一次遇到这个问题是在一个晚上,当时我突然好奇,就想写一个快速测试,看看浏览器如何处理平面交集。 测试包含两个平面元素

<div class='plane'></div>
<div class='plane'></div>

它们的大小相同,绝对定位在屏幕中央,并设置了背景,以便它们可见

$dim: 40vmin;

.plane {
  position: absolute;
  top: 50%; left: 50%;
  margin: -.5*$dim;
  width: $dim; height: $dim;
  background: #ee8c25;
}

场景是整个 body 元素,覆盖整个视窗,并设置了 perspective,以便更远的元素看起来更小,更近的元素看起来更大

body {
  margin: 0;
  height: 100vh;
  perspective: 40em;
}

为了真正测试平面交集,第二个平面元素被赋予了 rotateY() 变换和一个不同的背景

.plane:last-child {
  transform: rotateY(60deg);
  background: #d14730;
}

结果令人失望。 似乎没有一个浏览器可以正确地处理平面交集

查看 CodePen 上 Ana Tudor (@thebabydino) 制作的 测试平面交集(错误!)

但我错了。 这正是我的代码应该产生的结果。 我应该做的是将这两个平面放在同一个 3D 渲染上下文 中。 如果你不熟悉 3D 渲染上下文,它们与 堆叠上下文 没有太大区别。 就像我们不能通过 z-index 来排列不在同一个堆叠上下文中的元素一样,如果 3D 变换的元素不在同一个 3D 渲染上下文,就不能在 3D 中排列或使它们相交。

确保它们在同一个 3D 渲染上下文中最简单的方法是将它们放在另一个元素中

<div class='assembly'>
    <div class='plane'></div>
    <div class='plane'></div>   
</div>

然后将这个容器元素绝对定位在场景中央,并在它上面设置 transform-style: preserve-3d

div { position: absolute; }

.assembly {
    top: 50%; left: 50%;
    transform-style: preserve-3d;
}

这解决了问题

查看 CodePen 上 Ana Tudor (@thebabydino) 制作的 测试平面交集(正确)

如果你使用 Firefox 查看上面的演示,你仍然看不到平面如预期的那样相交,因为 Firefox 仍然没有解决这个问题。 但是你应该看到它们在 WebKit 浏览器和 Edge 中相交。 **更新**:此问题已在 Firefox 55+ 中修复。

现在你可能想知道为什么要费心添加那个容器元素,仅仅在场景(在本例中是 body 元素)上添加 transform-style: preserve-3d 不行吗? 好吧,在这个特定的例子中,如果我们在初始演示中只添加这一条规则,而不添加其他任何规则,它确实有效(除非你在 Firefox 54 或更早版本中查看它)

查看 CodePen 上 Ana Tudor (@thebabydino) 制作的 测试平面交集(有效,但…)

如果我们要在实际网页上使用 3D,我们的场景可能不会是 body 元素,而且我们可能想在场景中添加其他属性。 这些属性可能会干扰 3D 效果。

破坏 3D(或导致扁平化)的事项

假设我们的场景应该是页面中的另一个 div,并且我们周围还有其他东西

查看 CodePen 上 Ana Tudor (@thebabydino) 制作的 两个平面在较小的场景中 #0

我还添加了第二个平面上的几个变换,以便更清楚地表明它正在从场景中出来。 这正是我们不希望发生的。 我们希望能够阅读文本,与可能存在的控件进行交互,等等。

1) overflow

第一个想到的方法是简单地在场景上设置 overflow: hidden。 但是,当我们这样做的时候,我们失去了美丽的 3D 交集

问题。

这是因为给 overflow 设置除 visible 以外的任何值,都会强制 transform-style 的值变为 flat,即使我们已经明确地将其设置为 preserve-3d。 因此,使用容器确实意味着要写更多的代码,但可以为我们节省很多麻烦。

解决方案。

这就是为什么我现在将场景中的所有东西都放在一个容器元素中,即使该元素没有在 3D 中变换。 例如,考虑以下演示

查看 CodePen 上 Ana Tudor (@thebabydino) 制作的 蓝色六边形螺旋糖果(纯 CSS 3D)

所有旋转的六边形柱都放在一个 .helix 元素中

<div class='helix'>
    <div class='col'>
        <!-- all the hexagons inside a column -->
    </div>
    <!-- the other columns -->
</div>

这个 .helix 元素没有其他样式(直接设置或继承),除了确保整个组件绝对定位在视窗中央,并且所有柱都处于同一个 3D 渲染上下文中

div {
    position: absolute;
    transform-style: preserve-3d;
}

.helix { top: 50%; left: 50%; }

这是因为我将 overflow: hidden 设置在场景(在本例中是 body 元素)上,因为六边形的大小不依赖于视窗,所以我不知道它们是否会延伸到外面(并导致滚动条,我不希望出现这种情况)。

我承认,在我吸取教训之前,曾不止一次遇到过这个问题。 说句公道话,在某些情况下,overflow: hidden 的效果可能并不明显。

transform-style: preserve-3d 告诉浏览器,它所设置的元素的 3D 变换的子元素不应该被扁平化到其父元素(我们设置 transform-style: preserve-3d 的元素)的平面中。 因此,即使直观地来说,它也有一定的道理,即在同一个元素上设置 overflow: hidden 也会撤销这种效果,并阻止子元素突破其父元素的平面。

但是,有时 3D 变换的子元素仍然可以在其父元素的平面内。 考虑以下情况:我们有一张卡片,上面有两面

<div class='card'>
    <div class='face'>front</div>
    <div class='face'>back</div>
</div>

我们将它们都绝对定位在场景(在本例中是 body 元素)的中央,给卡片及其面设置相同的尺寸,在卡片上设置 transform-style: preserve-3d,在面上设置 backface-visibility: hidden,并围绕其垂直轴旋转第二个面半圈

$dim: 40vmin;

div {
    position: absolute;
    width: $dim; height: $dim;
}

.card {
    top: 50%; left: 50%;
    margin: -.5*$dim;
    transform-style: preserve-3d;
}

.face {
    backface-visibility: hidden;
    background: #ee8c25;

    &:last-child {
        transform: rotateY(.5turn);
        background: #d14730;
    }
}

演示可以在下面看到

查看 CodePen 上 Ana Tudor (@thebabydino) 制作的 卡片 #0

两面仍然在其父元素的平面内,只是背面围绕其垂直轴旋转了半圈。 它面对相反的方向,但仍然在同一个平面内。 到目前为止,一切看起来都很棒。

现在假设我们不希望面是矩形的。 改变它的最简单方法是在卡片上设置 border-radius: 50%。 但是,这似乎什么也没做

因此,我们在上面设置 overflow: hidden

查看 CodePen 上 Ana Tudor (@thebabydino) 制作的 卡片 #2

糟糕,这破坏了我们的 3D 卡片! 由于我们不能这样做,我们需要对面的角进行圆角处理

.face { border-radius: 50%; }

查看 CodePen 上 Ana Tudor (@thebabydino) 制作的 卡片 #3

在这种情况下,解决问题的方法甚至比造成问题的方法更简单。 但是,如果我们想要另一个形状,例如正八边形呢? 用两个元素(或一个元素和一个伪元素)就可以很容易地实现正八边形

<div class='octagon'>
    <div class='inner'></div>
</div>

我们给它们都设置相同的尺寸,将 .inner 元素旋转 45deg,赋予它一个背景,以便我们能看到它,然后在 .octagon 元素上设置 overflow: hidden

$dim: 65vmin;

div { width: $dim; height: $dim; }

.octagon { overflow: hidden; }

.inner {
    transform: rotate(45deg);
    background: #ee8c25;
}

结果可以在下面的 CodePen 中看到

查看 CodePen 上 Ana Tudor (@thebabydino) 制作的 如何:基本正八边形(纯 CSS)

如果我们添加文本…

<div class='octagon'>
  <div class='inner'>octagon</div>
</div>

它根本没有显示出来。

问题是它在一个角落被剪掉了,所以我们把它放大,用 text-align: center 水平对齐它,并通过给它一个与 .octagon(或 .inner)元素的尺寸相同的行高,将其垂直居中

.inner {
    font: 10vmin/ #{$dim} sans-serif;
    text-align: center;
}

现在它看起来好多了,但文本仍然是旋转的,因为我们在 .inner 元素上设置了旋转

查看 CodePen 上 Ana Tudor (@thebabydino) 制作的 带文本的八边形 #1

为了解决这个问题,我们在 .octagon 元素上添加了一个反向旋转(角度相同,但方向相反,因此是负数)

.octagon { transform: rotate(-45deg); }

我们得到了一个带文本的八边形!

查看 CodePen 上 Ana Tudor (@thebabydino) 制作的 带文本的八边形 - 最终版!

现在让我们看看如何将它应用到想要一个八面体面的卡片上。我们不能在卡片本身设置overflow: hidden(使其扮演.octagon元素的角色,而面部则像.inner元素一样),因为这会破坏东西,而且我们不再有一个漂亮的具有两个不同面的 3D 卡片了。

查看 CodePen 上 Ana Tudor(@thebabydino)的 卡片 #4

相反,我们需要让每个面部扮演.octagon元素的角色,并使用伪元素扮演内部元素的角色。

.face {
    overflow: hidden;
    transform: rotate(45deg);
    backface-visibility: hidden;

    &:before {
        left: 0;
        transform: rotate(-45deg);
        background: #ee8c25;
        content: 'front';
    }

    &:last-child {
        transform: rotateY(.5turn) rotate(45deg);

        &:before {
            background: #d14730;
            content: 'back'
        }
    }
}

这给了我们我们一直想要的结果。

查看 CodePen 上 Ana Tudor(@thebabydino)的 卡片 #5

2) clip-path

另一个可能导致类似问题的属性是clip-path。回到我们的卡片示例,我们不能通过在.card元素本身应用clip-path来使其呈三角形,因为我们需要它有一个 3D 变换后的子元素,即第二个面。我们应该将其应用于卡片面部而不是。

.face { clip-path: polygon(100% 50%, 0 0, 0 100%); }

请注意,clip-path属性仍然需要-webkit-前缀才能用于 WebKit 浏览器,在 Firefox 47-53 中将about:config中的layout.css.clip-path-shapes.enabled标志设置为true(该标志在 Firefox 54+ 中默认设置为true),并且在 Edge 中尚不支持(但您可以投票实施)。

添加上面的代码行后的结果将如下所示。

查看 CodePen 上 Ana Tudor(@thebabydino)的 卡片 #6

没有 3D 问题,但看起来真的很别扭。如果卡片从正面看是一个指向右边的三角形,那么从背面看它应该指向左边。但事实并非如此,它也指向右边。解决这个问题的一个方法是为每个面使用不同的clip-path值。使用相同的指向右边的三角形剪裁正面,并使用另一个指向左边的三角形剪裁背面。

.face:last-child { clip-path: polygon(0 50%, 100% 0, 100% 100%); }

结果正是我们想要的。

查看 CodePen 上 Ana Tudor(@thebabydino)的 卡片 #7

请注意,我还更改了text-align值:正面的默认值为left,背面的值为right

或者,我们也可以在背面添加一个scaleX(-1)到变换链中(如果您需要提醒缩放是如何工作的,请查看此交互式演示)。

.face:last-child { transform: rotateY(.5turn) scaleX(-1); }

查看 CodePen 上 Ana Tudor(@thebabydino)的 卡片 #8

在这种情况下,形状看起来很好,但文本是反向的。这意味着我们实际上将文本和背景放置在一个伪元素上,在该伪元素上,我们在.face元素上反转了缩放比例。反转一个因子为f的缩放比例意味着设置另一个因子为1/f的缩放比例。在我们的例子中,f因子为-1,所以我们在伪元素上寻找的缩放值是1/-1 = -1

.face:last-child:before {
    transform: scaleX(-1);
    background: #d14730;
    text-align: right;
    content: 'back';
}

最终结果可以在此笔中看到。

查看 CodePen 上 Ana Tudor(@thebabydino)的 卡片 #9

设置为none以外的任何值的遮罩属性也可以强制transform-style的已用值为flat,就像overflowclip-path当设置为visiblenone以外的值时一样。

3) opacity

这是一个意料之外的。

它也是对规范的相对较新的更改,以便opacity小于1的效果对 3D 渲染上下文的影响与对堆叠上下文的影响一致。这就是为什么小于 1 的opacity实际上并没有强制 Edge 或 Safari 压平……直到现在!然而,它确实在 Chrome、Opera 和 Firefox 中产生了这种影响。

考虑以下演示,一群立方体在 3D 中一起旋转。

查看 CodePen 上 Ana Tudor(@thebabydino)的 立方体组合 #0

在结构上,这意味着一个包含一堆.cube元素的.assembly元素,每个元素都有6个面。

<div class='assembly'>
  <div class='cube'>
    <div class='cube__face'></div>
    <!-- five more cube faces -->
  </div>
  <!-- more cubes, each with 6 faces -->
</div>

现在假设我们想要使立方体半透明。我们不能这样做。

.cube { opacity: .5; }

这会导致.cube元素上的transform-style值被强制为flat,即使我们已经将其设置为preserve-3d,这会导致立方体面被压平到其.cube父元素的平面中。目前只在 Chrome、Opera 和 Firefox 中,但其他浏览器将来也会实施这一点。

当不强制压平时结果(Edge、Safari)。
当强制压平时结果(Chrome、Firefox、Opera,根据新的规范更改)。

我们也不能在.assembly元素上设置opacity: .5,因为我们也已经在其上设置了transform-stylepreserve-3d。所以,同样,结果将在不同浏览器之间不一致,因为新规范强制压平,而一些仍然遵循旧规范(没有强制压平)。

我们可以做的是在立方体面的元素上设置opacity: .5,这样就不会遇到任何麻烦。

查看 CodePen 上 CSS-Tricks(@css-tricks)的
立方体组合 #3

CodePen 上。

我们也可以将其设置在场景元素上,但请注意,这也会影响我们可能拥有的任何场景background或伪元素。它也不会使各个立方体或面部半透明,而是整个组合。它也不允许我们为不同的立方体设置不同的不透明度值。

在各个立方体面上设置 opacity: .5 的结果。
在场景上设置 opacity: .5 的结果。

4) filter

这又是另一个让我感到惊讶的地方,与opacity不同,它并不新,而且结果在所有浏览器中都是一致的。让我们再看看立方体示例。假设我们想要通过hue-rotate()为每个立方体设置一个随机不同的色调。在立方体或组合上设置filter值为none以外的值会导致扁平化的表示。

$n: 20; // number of cubes

@for $i from 0 to $n {
    $angle: random(360)*1deg;

    .cube:nth-child(#{$i + 1}) {
        filter: hue-rotate($angle);
    }
}

请注意,直到最近,filter仍然需要-webkit-前缀才能用于所有 WebKit 浏览器,而且,虽然目前所有主要桌面浏览器现在都支持它没有前缀,但大多数移动浏览器仍然需要此前缀

这确实可以为每个立方体设置随机色调,但它也会将它们压平

在这种情况下,解决方案是在循环中在立方体面上设置filter

$n: 20; // number of cubes

@for $i from 0 to $n {
    $angle: random(360)*1deg;

    .cube:nth-child(#{$i + 1}) .cube__face {
        filter: hue-rotate($angle);
    }
}

这给了我们我们想要的东西,立方体以随机色调呈现,并且仍然是 3D 的,而不是扁平化的。

查看 CodePen 上 CSS-Tricks(@css-tricks)的
立方体组合 #6

CodePen 上。

我们也不能在整个组合上设置filter。考虑我们想要模糊整个组合的情况。假设我们这样做。

.assembly { filter: blur(4px); }
结果是,整个东西都被压平到组合的平面中,并且还被模糊了。Edge 是一个例外,所有东西都消失了。

我们可以做的是尝试在面部元素上应用blur()过滤器,尽管结果不会完全符合预期,因为我们模糊的是各个面,而不是立方体本身。它看起来也很糟糕,Blink 浏览器出现了一些闪烁、丢失面部,并且blur()过滤器的速度明显降低,而 Edge 则完全搞乱了。Firefox 在这里似乎表现最好。

我们也可以尝试将其应用于场景,尽管看起来很糟糕(有时会出现闪烁,面部在 Chrome 中消失,在 Firefox 中消失,整个组合也完全消失,而 Edge 则根本不显示任何东西)。

我感到惊讶,因为下一个更简单的旋转立方体演示也对场景应用了blur()过滤器,并且它在 Blink 浏览器和 Edge 中似乎大部分情况下都能正常工作。然而,在 Firefox 中什么都没有显示。

查看 CodePen 上 CSS-Tricks(@css-tricks)的
粘性立方体(纯 CSS 3D,不含 Firefox)

CodePen 上。

总的来说,过滤器与 3D 的组合通常会出现问题,因此我建议谨慎使用。

5) mix-blend-mode

假设我们有一个带有彩虹色背景的.container元素。在这个元素内部,我们有一个带有图像背景的.mover元素,比如一个黑莓派。类名可能已经泄露了这个信息,但我们对.mover元素的位置进行动画,并将其设置为mix-blend-mode: overlay。这使得我们的移动元素看起来不同,具体取决于它父元素的背景的哪一部分在它上面。

查看CodePen上的示例:轨道上的派,混合模式(混合模式!),由Ana Tudor(@thebabydino)创建。

Edge 浏览器尚不支持混合模式,因此本节中的演示示例在 Edge 浏览器中无法正常运行。但是,您可以mix-blend-mode实现投票。另外请注意,目前,由于Blink 浏览器的一个错误,您可能不应该将.container元素设置为bodyhtml元素。这个错误会导致当.container元素是bodyhtml元素时,.mover元素上的混合模式被忽略。Firefox 和 Safari 浏览器没有这个问题。

好吧,但这仅仅是二维的。如果我们的移动元素是一个带有图像面的立方体,一个在三维空间中旋转的立方体呢?

查看CodePen上的示例:
轨道上的派立方体,混合模式 #0
,由CSS-Tricks(@css-tricks)创建。
CodePen 上。

到目前为止,一切都很好,但我们还没有任何混合效果。我们在立方体上设置mix-blend-mode: overlay,现在我们有了混合效果,但它破坏了我们的三维效果,立方体的面被扁平化到立方体的平面上了!

查看CodePen上的示例:
轨道上的派立方体,混合模式 #1
,由CSS-Tricks(@css-tricks)创建。
CodePen 上。

同样,这是因为我们在立方体上应用三维变换并对其子元素进行三维变换,因此我们希望立方体对于transform-style属性的值为preserve-3d。但是,在立方体上设置mix-blend-mode: overlay会强制transform-style属性的实际值为flat,因此立方体的面会被扁平化到其父元素的平面。不过,这种情况在 Firefox 浏览器中不会发生,尽管规范中说明应该会发生这种情况

我们可以尝试在立方体的面上设置mix-blend-mode: overlay,但这似乎不起作用。立方体被扁平化了,也没有混合效果。

另一个解决方案是在容器和移动的立方体之间添加一个.scene元素,并在该元素上设置perspectivemix-blend-mode属性。

查看CodePen上的示例:轨道上的派立方体,混合模式 #3,由Ana Tudor(@thebabydino)创建。

这似乎解决了所有问题!