Gather ye rosebuds while ye may

「译」CSS 3D 注意事项


这一篇已经有人翻译过了:CSS 3D 应该注意的事项

原文链接:Things to Watch Out for When Working with CSS 3D · 作者 ANA TUDOR


我一直挺喜欢 3D 几何。在注意到 CSS 支持得越来越全面的同时,我也开始使用 CSS 3D 变换(transform)。但刚开始就有些东西难住了我:在创建多面体时,我自然地在 3D 中使用 transform 去创建 2D 图形并移动、旋转它们。我想我该记录下我遇见的奇特的部分,希望你能绕过这些障碍。

3D 渲染上下文

还记得那晚好奇心驱使我写了一个小小的演示,想看看浏览器如何处理平面的交叉。这个演示包括了两个平面元素:

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

它们大小相同,用绝对定位放在屏幕的中间,为了看到它们又加了个背景:

1
2
3
4
5
6
7
8
9
$dim: 40vmin;

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

这个场景就是整个body元素,让其覆盖整个视窗(viewport),然后给了一个perspective(透视)使得远一点的看起来小一点,近一点的显示的更大:

1
2
3
4
5
body {
margin: 0;
height: 100vh;
perspective: 40em;
}

为了测试平面相交的效果,第二个平面元素有一个rotateY()变换(transform),和一个不同的背景:

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

结果是令人失望的。似乎没有浏览器可以正确的处理平面相交:

See the Pen test plane intersection (WRONG!) by Ana Tudor (@thebabydino) on CodePen.

但是我错了。这些代码就应该显示成这个样子。我本应该将这两个平面放在同一个3D 渲染上下文中。鉴于有人不熟悉 3D 渲染上下文,简单来说它和堆叠上下文差不多。在不同的堆叠上下文中我们不能通过z-index来对元素进行排序,同样的,在不同的 3D 渲染上下文中,3D 变换后的元素不能进行 3D 排序或交叉。

将元素放在同一个 3D 渲染上下文中的方法也很简单,即放在另一个元素内:

1
2
3
4
<div class='assembly'>
<div class='plane'></div>
<div class='plane'></div>
</div>

然后把包裹元素用绝对定位放在场景中间,并为其设置transform-style: preserve-3d

1
2
3
4
5
6
div { position: absolute; }

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

这样就解决了问题:

See the Pen test plane intersection (CORRECT) by Ana Tudor (@thebabydino) on CodePen.

由于浏览器的原因,你仍不能在 Firefox 中看到本应正常的平面相交效果。但是你应该能在 Webkit 和 Edge 浏览器中看到。

你有可能问了,为什么要还加一个包裹元素呢,在上一级元素(上面的例子中的body)里添加transform-style: preserve-3d不是更简单么?好吧,在上面的特例中你确实可以这么做(除了 Firefox,因为 Firefox 在处理 3D 顺序和交叉上有问题):

See the Pen test plane intersection (working, BUT…) by Ana Tudor (@thebabydino) on CodePen.

但是在实际的工作环境中,场景不一定是body,我们也会为场景添加其他属性。这些其他属性则可能会干扰到展示效果。

破坏 3D (或造成扁平化)的情况

例子场景是页面中的另一个div,有其他元素环绕着它:

See the Pen two planes in smaller scene #0 by Ana Tudor (@thebabydino) on CodePen.

我为第二个平面添加了一些变换使其更加明显,但在这里它超出了场景。这并不是我想看到的。我希望我既能阅读文字,也能操作控件。

1)overflow

我最先想到的就是在场景中使用overflow: hidden。然而在使用之后,它失去了漂亮的 3D 交叉效果:

See the Pen two planes in smaller scene #2 by Ana Tudor (@thebabydino) on CodePen.

这是因为给任意元素一个非visibleoverflow属性都会强行将这个元素的transform-style设置为flat,即使它们已经被设置为了preserve-3d。所以我要使用元素来包裹他们,虽然多一点代码,却能少一点头疼。

See the Pen two planes in smaller scene #3 by Ana Tudor (@thebabydino) on CodePen.

这就是为什么即使场景没有进行 3D 变换,我也总是将场景放在一个包裹的元素当中。比如下面的例子:

See the Pen blue hex helix candy (pure CSS 3D) by Ana Tudor (@thebabydino) on CodePen.

每一列旋转的六边形都被放在.helix元素当中:

1
2
3
4
5
6
<div class='helix'>
<div class='col'>
<!-- all the hexagons inside a column -->
</div>
<!-- the other columns -->
</div>

.helix设置的属性只有两个作用:

  1. 保证整个部件被绝对定位于视窗中心
  2. 所有列都被放在同一个 3D 渲染上下文中
1
2
3
4
5
6
div {
position: absolute;
transform-style: preserve-3d;
}

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

这是因为我为场景(例子中的body)设置了overflow: hidden,同时六边形的大小也不由视窗决定,所以我不知道他们会不会向外延伸(产生我不想要的滚动条)。

我承认我被这个坑了很多次。在这里使用overflow: hidden让溢出的显示不那么明显。

要是一个元素设置了transform-style: preserve-3d,该属性就会告诉浏览器不应该把它(这个设置了transform-style: preserve-3d的元素)的子元素拍扁。所以在相同元素上设置overflow: hidden不会让 3D 元素在场景内被拍扁,也能防止子元素超出父元素平面,这在直觉上看也是合理的。

但有时一个 3D 变换的子元素还是会变成父元素中的平面。看看下面这个双面卡片的例子:

1
2
3
4
<div class='card'>
<div class='face'>front</div>
<div class='face'>back</div>
</div>

这里将其绝对居中于场景(例子中的body)里,给卡片和它的面设置相同的外观,为外部卡片设置transform-style: preserve-3d,为两面都设置backface-visibility: hidden,再将后面沿着纵轴转半圈:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$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;
}
}

下面是demo:

See the Pen card #0 by Ana Tudor (@thebabydino) on CodePen.

这两面都在父元素的平面内,而后面沿着纵轴旋转了半圈。背面虽然方向和正面相反,但是仍在同一个平面中。现在看来都挺好的。

假如我不想让面展现为长方形。最简单的方法就是为面设置border-radius: 50%。但是貌似完全没用

所以在卡片上设置overflow: hidden

See the Pen card #2 by Ana Tudor (@thebabydino) on CodePen.

但是这样破坏了我们的 3D 卡片。既然不能这样做,我们就在面上设置:

1
.face { border-radius: 50%; }

See the Pen card #3 by Ana Tudor (@thebabydino) on CodePen.

在这个例子里,解决问题的方法比造成问题的还要简单。但是如果有另外一个形状,比如正八边形?一个正八边形通常用两个元素(或一个元素及其伪元素)来实现:

1
2
3
<div class='octagon'>
<div class='inner'></div>
</div>

给它们设置相同的外观,将.inner元素旋转45deg,为了能看得见,给他设置一个背景,然后为.octagon设置overflow: hidden

1
2
3
4
5
6
7
8
9
10
$dim: 65vmin;

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

.octagon { overflow: hidden; }

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

结果在下面:

See the Pen how to: basic regular octagon (pure CSS) by Ana Tudor (@thebabydino) on CodePen.

如果我们添点文字的话……

1
2
3
<div class='octagon'>
<div class='inner'>octagon</div>
</div>

就显示不出来了。

这是因为文字在边缘之外,所以我们将文字变大,用text-align: center让他水平居中,再将它的行高设置为.octagon(或.inner)元素的高度以垂直居中:

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

现在看上去就好多了,但是文字随着我们对.inner元素的旋转而旋转了:

See the Pen octagon with text #1 by Ana Tudor (@thebabydino) on CodePen.

我们为.octagon元素也设置一个旋转(相同度数,相反方向,即负)来解决这个问题:

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

这样就是有文字的八边形了!

See the Pen octagon with text - final! by Ana Tudor (@thebabydino) on CodePen.

现在来研究一下八边形的卡片。我们不能为卡片本身(卡片是.octagon元素,.inner元素就是面)设置overflow: hidden,这样会破坏 3D 卡片的两个不同面:

See the Pen card #4 by Ana Tudor (@thebabydino) on CodePen.

所以要让.octagon作为面,然后用伪元素实现.inner元素:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
.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'
}
}
}

于是就有了下面的结果:

See the Pen card #5 by Ana Tudor (@thebabydino) on CodePen.

2)clip-path

另一个会造成相同问题的属性是clip-path

未完待续,话说有人已经翻译过了我的动力不强劲啊~~