涉及属性

  • perspective
  • perspective-origin
  • transform-style
  • transform ( rotateX, rotateY, translateZ )
  • transform-origin

必要条件

要实现 3D 效果必须要先理解 3D 空间是怎么形成的,以及实现 3D 效果的必要条件是什么。

Z 轴

区别与 2D 场景,3D 场景与 2D 场景最大的区别是有了 Z 轴,那么 Z 轴在 css 中该如何产生呢?

这就涉及到上面列出的第一个属性: perspective(景深) MDN

首先我们必须要知道 Z 轴如果生成了会有什么效果,最直观的感受就是元素在 Z 轴 上面平移会有大小变化。

我们都说空间是由一个个平面构成,也就是说如果说我们规定一个与元素平行的平面,以这个平面为基础去定义元素平面的 Z轴 那么 3D 空间就产生了。这也是 perspective 的作用所在,因此 perspective 属性可以认为是开启 3D 场景。

perspective 规定了观察者距离垂直于元素 2D 平面下的距离。

简单的例子就是一张纸我们可以理解为一个简单的元素,纸面就是 2D 空间的平面,如果设定了 perspective: 100px; 就相当于你拿起了这张纸(纸面与你的面平行),那么这张纸和你这个人就够成了 3D 空间。

总结来说,3D 空间的构成必须要有一个参照平面,两个不同的平面才能照成视差,也就产生了 3D 的效果,而 perspective 就规定了这两个参照平面的距离。

需要注意的是:

当一个元素有了 perspective 属性后,元素的大小并不会发生变化。
所以当一个元素的 css

perspective: 100px;
width:100px;
height:100px;

可以理解为,在距离观察平面 100px 处有一个看上去100X100 的元素,注意是看上去,这和拍照是同一个道理,设置的大小就是照片拍出来后在照片中的大小。

perspective 仅仅支持正值,负值或是 0 不生效。

faq: 针对于网上一些文章中发现的问题

  1. 设置了 perspective 属性就相当于这距离元素多远处放置一只眼睛然后观察元素 这样的描述不太对,一只眼睛相当于一个点,而不是一个平面,这不仅仅确定了 Z 轴同时还确定了 XY 轴,但是产生的效果确实和这样的描述相符合。原因是因为,默认的 transform 变换点为元素的中心,这点不用设置 perspective 就可以确定,所以如果设置了 perspective-origin 这样的描述就不对了。所以可以这么认为 眼睛的位置由 perspective-origin 确定,而这只眼睛距离元素多远由 perspective 属性确定。
  2. 设置 perspective 属性就相当于产生了垂直于屏幕平面的 Z 轴。 这个描述也不对,仅仅特定的情况下生效,那就是元素的父元素没有 3D 变换,或是有 3D 变换但没有旋转 XY 轴。原因如下:一个元素在没有 3D 空间的情况下是永远和屏幕在同一个平面的,所以垂直于屏幕等效于垂直于元素,但是如果说元素的父元素处在 3D 空间内,并且进行了 X 轴 或 Y 轴 的旋转,那么这个观察平面是不会和屏幕平行,那么变换所依据的 Z 轴 也不会垂直于电脑平面。

transform 变换

有了 3D 空间,如果不进行变换的话是没有任何效果的,所以产生 3D 效果的第二个条件就是变换。

变换基于 3D 坐标轴,所以使用 transform 变换最重要的是确定 3D 坐标轴。

那么如何确定 3D 坐标轴?

很简单,我们找出坐标原点即可,就是 transform-origin MDN的值,默认值为 transform-origin: 50% 50% 0; 也就是元素的正中心。

需要注意的是如果没有 3D 空间,transform-origin 的第三个值是无效的,因为对于元素来说,它并不知道观察点距离自己多远,也就进行不了视差的变化。

faq: 必须要理解的几个问题

  1. transform 到底在改变什么? transform 改变的是坐标轴,而不是元素,元素的呈现是随便坐标轴的变化而变化的,比如 transform: scale(.5); 就是坐标轴缩小了,那么对应元素的显示就缩小了,并不是元素的大小缩小了,而坐标轴没变,元素的真实大小永远不会变,变的是元素在不同坐标轴下的呈现。 当然除非修改 transform-origin ,坐标轴对于元素来说永远不会变。理解了这个那么对于 transform 的应用应该也没有问题。
  2. 不同的元素是否在同一个坐标轴下? 不是。每个元素都有自己坐标轴,即使是父子元素也是处在不同的坐标轴下,但是子元素会受父元素影响,子元素的起始坐标轴和父元素的最终坐标轴一致。举个例子,当父元素的 X 轴旋转了 45deg ,那么子元素坐标轴的起始位置就已经旋转了 45deg

3D 效果的叠加

在上一节的 faq 中的第二点,如果实验的话,应该是有点小问题的,因为在浏览器中,一个元素的内容默认是以 2D 效果呈现的,也就是 transform-style: flat; 。关于这个属性我们来具体的实验一下,结果通过实验来获得。

实现的前提条件是:有一个 3 层嵌套的 div ,在最外层的 div 上设置 perspective 生成 3D 空间,最深一级的 div 上添加旋转,代码大致如下

<div class="box1">
    <div class="box2">
        <div class="box3"></div>
    </div>
</div>
.box1 {
    perspective: 300px;
}
.box3 {
    transform: rotateY(45deg);
}

接着我们来控制 box2

  1. box2 无变换,不添加 transform-style: preserve-3d

点击查看:jsfiddle
可以发现 box3 处在 box23D 空间中,box2 的背景成了空间的背景。

  1. box2 无变换,添加了 transform-style: preserve-3d

点击查看:jsfiddle
可以发现 box2 成了 3D 变换中的一员,由于 box3 的旋转,导致有一部分旋转到了 box2 的后面,结果就是看不见了。
换句话说 box2box13D 空间中,box3 也在 box13D 空间。

  1. box2 有变换,不添加 transform-style: preserve-3d

点击查看:jsfiddle
可以发现 box2 处在 3D 空间中,而 box3 处在 box2 的平面里。

  1. box2 有变换,添加了 transform-style: preserve-3d

点击查看:jsfiddle
结果和情况 2 呈现的效果一致,两个元素都处在 box13D 空间里。

分析:

  1. 前面我们说过:perspective 会开启一个 3D 空间,确定了其子元素的 Z 轴,因此 box2 是能做 3D 变换的。所以 3 4 情况中 box2 的状态我们可以确定,就是这样呈现的。
  2. 3 4 情况中的区别,在于 box3 的呈现,而代码上的区别在于 box2 有没有添加 transform-style: preserve-3d 这条属性,那么这条属性的效果就是将子元素放在 3D 空间中显示,这条属性的另一个值 flat 也就是默认值就是将子元素压平在父元素中显示,就好像往父元素中塞了一张照片。
  3. 对比 2 4 发现,在添加了 transform-style: preserve-3d 的情况下,父元素的变换并不会影响到子元素。但对比 1 2 发现,不添加 transform-style: preserve-3d 时,事情有点出乎意料。 box2 成了变换的 3D 空间,但是 box2 上却没有 perspective 属性。
  4. 对于第 3 点,我们是不是可以认为当子元素没有进行变换的时候,3D 空间是会被赋给子元素??这点不太确定,表现如此,但也找不到相关的内容,有待查证。

总结

  1. perspective 规定了一个与元素平行的平面,生成了 3D 空间,与 perspective-origin 配合确定了视觉的观察点。
  2. transform-origin 规定了一个元素的 3D 坐标原点,前提元素处在 3D 空间内。
  3. transform 可以将元素的坐标轴进行变换,不直接修改元素,元素只是对于坐标轴的呈现。
  4. transform-style preserve-3d: 将子元素以 3D 效果呈现。 flat: 将子元素沿着父元素的 Z 轴压扁,效果和拍照一致。
  5. 当祖父元素开启了 3D 空间,而父元素没有进行变换,3D 空间会延续到父元素上。
  6. 最后一点有待考证。

记在最后

如果在一个拥有 perspective 属性的元素内的子元素上在使用 perspective 如下的形式

<div class="box1">
    <div class="box2">
        <div class="box3"></div>
    </div>
</div>
.box1 {
    perspective: 100px;
}
.box2 {
    perspective: 100px;
}

那么 box3 的 Z 轴相关的变换就会发生变化,实验下来会被拉伸,以 translateZ() 为例,以下为测试内容。

测试数据基于

<div class="box1">
    <div class="box2">
        <div class="box3"></div>
    </div>
</div>
.box1 {
    width: 500px;
    height: 500px;
    position: relative;
    perspective: 100px;
}
.box2 {
    width: 100px;
    height: 100px;
    position: absolute;
    top: 200px;
    left: 200px;
    background: rgba(0, 0, 0, 0.3);
    transform-style: preserve-3d;
    perspective: 100px;
}
.box3 {
    width: 100px;
    height: 100px;
    background: blue;
    transform: translateZ(-200px);
    transform-style: preserve-3d;
}

我们根据数据来计算 box3 距离 box1 3D 空间观察点的距离(假设为 X):

计算方法为: (box3 呈现大小)/(box3 设置尺寸) = (box1 perspective)/(X)

根据 box3box1 3D 空间观察点的距离,我们可以得出 box3box1 空间内的 Z 轴偏移量。

计算方法为: (box1 perspective) - X

观察下表可以看出 box3 呈现出来的 Z 轴偏移量和设置的 translateZ 并不相等,将 7 组数据总结了之后,可以得出

公式: (box3 的呈现偏移量) = ((box1 perspective)/(box2 perspective) + 1) * (box3 translateZ)

公式所呈现的效果就是把 box3 的坐标轴沿着 Z 轴进行了拉伸,拉伸的倍数为 ((box1 perspective)/(box2 perspective) + 1)。

公式所代表的意思,暂时无法理解。但是效果就是如果叠加使用 perspective 那么第二层设置了 perspective 属性的元素中的子元素的 Z 轴会进行拉伸。

含义 1 2 3 4 5 6 7
box1 perspective 100 100 100 100 100 200 300
box2 perspective 100 100 100 200 300 100 100
box3 translateZ -100 -200 -300 -100 -100 -100 -100
box3 呈现大小 33.33 20 14.29 40 42.86 40 42.86
box3 距离 box1 观察点的距离 300 500 700 250 233.3 500 700
box3 偏移量 -200 -400 -600 -150 -133.3 -300 -400