元素的相对、绝对定位和元素的3D变换

TailwindCSS

复习一下CSS中有关指定元素位置的样式

CSS有个样式叫position,它决定了浏览器应当如何排版所有元素。

首先介绍的是这个样式的默认值,即position: static: 它表浏览器将以默认的,从左到右,从上到下,从内到外的方式去排列所有元素。比如我们如下正常写三个排排坐的<div>

    <div class="size-20 bg-red-300"></div>
    <div class="size-20 bg-yellow-300"></div>
    <div class="size-20 bg-cyan-300"></div>

由于div是块元素,自己默认独占一行,所以视觉上三个方块是竖向排列的,浏览器渲染结果如下图所示:

position_static.png

再来看第二个值:position:relative,它的效果其实是在static的基础上,根据样式中给出的top/bottom/right/left/z-index再去调整元素的视觉位置。

比如我们对上面的代码做如下修改:

     <div class="size-20 bg-red-300"></div>
-    <div class="size-20 bg-yellow-300"></div>
+    <div class="size-20 bg-yellow-300" style="position: relative; top: 20px; left: 20px;"></div>
     <div class="size-20 bg-cyan-300"></div>

浏览器的渲染结果就会变成下面这样:

position_relative.png

你可以简单的把position: relative的排版过程理解为两步走:

  1. 先按static先排版
  2. 排完版后,浏览器再去挨个看position: relative的元素,然后按照样式中的top/bottom/right/left/z-index对各个元素的位置进行调整

第三个是值 position: absolute,它的排版过程可以理解为以下三步走:

  1. 浏览器还是要先按static排版,但在这一步排版的过程中,浏览器先把所有的带position: absolute的元素先扔出去,它们不参与这一步的排版,后续另安排
  2. 现在浏览器要开始安排position: absolute元素了,但在安排之前,浏览器要先看看元素的爹,爷爷,祖宗,一路向上找,看看能不能找到一个position样式不是static的先人,如果实在没有,一路都找不到,你就可以把这个先人理解为<body>元素
  3. 以找到的先人的位置(准确的来说是先人元素的左上角)为坐标原点,然后按元素的top/bottom/right/left/z-index的值,把元素放在正确的位置上
     <div class="size-20 bg-red-300"></div>
-    <div class="size-20 bg-yellow-300" style="position: relative; top: 20px; left: 20px;"></div>
+    <div style="position:relative; top: 100px; left: 100px;">
+        <div class="size-20 bg-yellow-300" style="position: absolute; top: 20px; left: 20px;"></div>
+    </div>
     <div class="size-20 bg-cyan-300"></div>

这个例子的渲染结果如下所示:

我来给你解释一下浏览器是怎么渲染上面这段代码的:

  1. 浏览器要先按static进行一次基本排版,上面也说了,在基本排版过程中,会把黄色的<div>给扔掉。而tricky的事情是,黄色<div>的父亲是没有显式写上宽高尺寸的,而黄色被扔掉后,黄色的父亲相当于内部没有内容了,所以宽高就变成了0。
  2. 黄色<div>不参与基本排版,但黄色<div>的父亲是参与的,因为父亲是position:relative
  3. 初步排版结果是:
    • 红色<div>正常排版,左上角的屏幕坐标是0, 0,宽高都是20个tailwind基本单位,即80像素
    • 黄色<div>的父亲也正常排版,左上角的屏幕坐标是0, 80px,但黄色的父亲在排版中被认为是一个空<div>,所以没有实际尺寸
    • 青色<div>正常排版,左上角的屏幕坐标依然是0, 80px: 因为黄色的<div>是没有尺寸的0 * 0的一个div
      • 更正一点:其实这个<div>的宽度并不是0,而是浏览器的最大宽度
  4. 初步排版结果结束后,开始安排黄色<div>的父亲,我们上面也说了,position: relative会在初步排版结束后再调整位置,于是黄色<div>的父亲的左上角坐标被调整成了(0, 80px) + (top: 100px; left: 100px),即100px, 180px
  5. 最后再安排黄色<div>:它以它爹的左上角坐标为基准,再加上自己的样式,得出它的左上角坐标是(100px, 180px) + (top: 20px; left:20px)120px, 200px

理解了这三个position样式值,浏览器的排版过程里最基本的逻辑你就理解了,再介绍两个另外的值

第一个是position: fixed,它和absolute很相似,不同的是在排版过程中这不找任何先人做基准,而是直接就以浏览器根元素的左上角坐标为基准点,是纯粹的绝对定位

     <div class="size-20 bg-red-300"></div>
     <div style="position:relative; top: 100px; left: 100px;">
-        <div class="size-20 bg-yellow-300" style="position: absolute; top: 20px; left: 20px;"></div>
+        <div class="size-20 bg-yellow-300" style="position: fixed; top: 20px; left: 20px;"></div>
     </div>
     <div class="size-20 bg-cyan-300"></div>

position_fixed.png

第二个是position: sticky,它的行为就比较难描述一点,它是用来在滚动过程中将元素固定在滚动框内的。下面是一个例子:

    <div class="size-20 bg-red-300"></div>
    <div class="size-40 bg-blue-300" style="overflow:auto">
        <div class="size-20 bg-yellow-300" style="position: sticky; top: 20px; left: 20px;"></div>
        <!-- 这里用lorem.100生成100个单词 -->
    </div>
    <div class="size-20 bg-cyan-300"></div>

效果如下:

position_sticky.gif

sticky的逻辑和relative有点像,或者说是加强版的relative,可以这样去理解:

  1. 第0步,浏览器先是预排版,就当positiontop/left/right/bottom四个样式都不存在一样
  2. 浏览器要看那个拥有position:sticky元素的祖上,有没有先人有滚动条:即overflow属性的值为auto, scrollhidden
    • 如果没有的话,那top/left/bottom/right四个属性中,只有left在某些情况下会生效,如果存在的话,转下一步
    • 这是一个比较奇怪的行为,实际开发中建议不要去试图理解它,你只需要确保,在你用position:sticky的时候,父级元素中一定存在一个overflow:auto/scroll/hidden
  3. 如果符合要求的先人存在,还分两种情况:
    • 在先人为overflow:autooverflow:scroll的情况下,浏览器会按top/bottom/right/left的值,对元素位置进行调整,然后重点来了:在先人存在滚动条且滚动的过程中,这个sticky元素和先人的相对位置,不会改变
    • 在先人为overflow:hidden的情况下,浏览器也会按照top/bottom/right/left的值对元素位置进行调整,但由于此时没有滚动条,所以看起来效果和relative是差不多的

以上这些样式在tailwind中是这样写的:

CSS样式 tailwind类名
position:static static
position:relative relative
position:absolute absolute
position:fixed fixed
position:sticky sticky
overflow:visible overflow-visible
overflow:auto overflow-auto
overflow:scroll overflow-scroll
overflow:hidden overflow-hidden
top:10px top-[10px]
top:calc(3/5 * 100%) top-3/5
top:-20px -top-5
top:100% top-full

其实CSS样式overflow还有一些其它知识点,包括:

  1. 还有overflow:clip这个值
  2. overflow其实是两个CSS属性:overflow-xoverflow-y的简写,也就是说,在横向和竖向两个方向上,可以分别设置滚动样式。这个了解一下就可以,使用到的时候查文档就可以

除了一些特殊的组件(比如用sticky实现的固定在视口的漂浮条啊等),这部分知识在实际工作中,用的最频繁的,其实是position:relativeposition:absolute的组合。

典型的就是画重叠元素的时候,就会用到这种技巧,来直接看例子(这回就直接用tailwind了):

    <div class="m-5 size-50 outline-1 outline-red-300"></div>
    <div class="m-5 size-50 outline-1 outline-purple-300 relative">
        <div class="size-30 top-3 left-3 bg-blue-300 absolute"></div>
        <div class="size-30 top-6 left-6 bg-cyan-300 absolute"></div>
        <div class="size-30 top-9 left-9 bg-yellow-300 absolute"></div>
    </div>
    <div class="m-5 size-50 outline-1 outline-red-300"></div>

效果如下:

position_most_used_trick.png

首先看三个空心框:三个框样式基本一样,只有一点特殊:

  • 第二个框带relative,但没有设置任何top/left/bottom/right属性。

用意并不是要对第二个框的位置做什么调整,而是使第二个框成为其子元素absolute时的先人,以第二个框自己的左上角位置为原点去定位。

然后在第二个框内部,三个实心的框,就可以直接使用absolute配合top/left来定位了

我们来看3D几何变换

来看下面这个稍微复杂一点的例子:

    <div class="m-5 size-50 outline-1 outline-black relative">
        <div id="blue_div" class="size-30 bg-blue-300 absolute top-5 left-5 "></div>
        <div id="red_div" class="size-30 bg-red-300 absolute top-15 left-15 "></div>
    </div>

    <div class="m-5 size-50 outline-1 outline-black relative transform-3d">
        <div id="blue_div_in_3d" class="size-30 bg-blue-300 absolute top-5 left-5 "></div>
        <div id="red_div_in_3d" class="size-30 bg-red-300 absolute top-15 left-15 "></div>
    </div>

    <div class="flex flex-col">
        <div>
            <label for="blue_div_rotate_x">Blue DIV Rotate X(0~180)</label>
            <input id="blue_div_rotate_x" type="range" min="0" max="180" step="1" value="0" onchange="UpdateRotation()">
        </div>
        <div>
            <label for="blue_div_rotate_y">Blue DIV Rotate Y(0~180)</label>
            <input id="blue_div_rotate_y" type="range" min="0" max="180" step="1" value="0" onchange="UpdateRotation()">
        </div>
        <div>
            <label for="blue_div_rotate_z">Blue DIV Rotate Y(0~180)</label>
            <input id="blue_div_rotate_z" type="range" min="0" max="180" step="1" value="0" onchange="UpdateRotation()">
        </div>
        <div>
            <label for="red_div_rotate_x">Red DIV Rotate X(0~180)</label>
            <input id="red_div_rotate_x" type="range" min="0" max="180" step="1" value="0" onchange="UpdateRotation()">
        </div>
        <div>
            <label for="red_div_rotate_y">Red DIV Rotate Y(0~180)</label>
            <input id="red_div_rotate_y" type="range" min="0" max="180" step="1" value="0" onchange="UpdateRotation()">
        </div>
        <div>
            <label for="red_div_rotate_z">Red DIV Rotate Y(0~180)</label>
            <input id="red_div_rotate_z" type="range" min="0" max="180" step="1" value="0" onchange="UpdateRotation()">
        </div>
    </div>

    <script>
        function RemoveRotateClasses(element) {
            const classesToBeRemove = [];
            element.classList.forEach(className => {
                if(className.startsWith('rotate-x') || className.startsWith('rotate-y') || className.startsWith('rotate-z')) {
                    classesToBeRemove.push(className);
                }
            });

            classesToBeRemove.forEach(className => {
                element.classList.remove(className);
            });
        }

        function AddRotateClasses(element, x, y, z) {
            element.classList.add(`rotate-x-${x}`);
            element.classList.add(`rotate-y-${y}`);
            element.classList.add(`rotate-z-${z}`);
        }

        function UpdateRotation() {
            const blueDiv = document.getElementById('blue_div');
            const blueDivIn3d = document.getElementById('blue_div_in_3d');
            const blueDivRotateX = document.getElementById('blue_div_rotate_x');
            const blueDivRotateY = document.getElementById('blue_div_rotate_y');
            const blueDivRotateZ = document.getElementById('blue_div_rotate_z');

            const redDiv = document.getElementById('red_div');
            const redDivIn3d = document.getElementById('red_div_in_3d');
            const redDivRotateX = document.getElementById('red_div_rotate_x');
            const redDivRotateY = document.getElementById('red_div_rotate_y');
            const redDivRotateZ = document.getElementById('red_div_rotate_z');

            RemoveRotateClasses(blueDiv);
            RemoveRotateClasses(blueDivIn3d);
            AddRotateClasses(blueDiv, blueDivRotateX.value, blueDivRotateY.value, blueDivRotateZ.value);
            AddRotateClasses(blueDivIn3d, blueDivRotateX.value, blueDivRotateY.value, blueDivRotateZ.value);

            RemoveRotateClasses(redDiv);
            RemoveRotateClasses(redDivIn3d);
            AddRotateClasses(redDiv, redDivRotateX.value, redDivRotateY.value, redDivRotateZ.value);
            AddRotateClasses(redDivIn3d, redDivRotateX.value, redDivRotateY.value, redDivRotateZ.value);
        }
    </script>

首先是HTML部分的DIV,首先是第一段:

    <div class="m-5 size-50 outline-1 outline-black relative">
        <div id="blue_div" class="size-30 bg-blue-300 absolute top-5 left-5 "></div>
        <div id="red_div" class="size-30 bg-red-300 absolute top-15 left-15 "></div>
    </div>

有了定位知识后,这段代码很容易理解:

  • 外层是一个带黑框线的,50*50的方形div
  • 内层是两个绝对定位的实心div,一个蓝色一个红色。绝对定位的锚点是父亲的左上角坐标

画出来长下面这样:

3d_example_1.png

然后把这个图形又画了一遍,不同的是在父元素上加了一个新的样式类,叫transform-3d,也改了两个子元素的ID以便在JS代码中区分

-    <div class="m-5 size-50 outline-1 outline-black relative">
+    <div class="m-5 size-50 outline-1 outline-black relative transform-3d">
         ...
     </div>

然后在下面写了六个input:range控件,每个都有自己的ID以便在JS代码中区分,取值范围都是0~180,步进为1,默认值为0

现在页面长下面这样:

3d_example_2.png

再接下来就是JS代码部分了,其实非常容易理解:每次改变控件的值的时候,都会按照取的值,去给两个蓝色或两个红色div修改对应的旋转属性。

唯一的新鲜知识就是,我们在2D平面变换的时候,只讲过rotate可以有rotate-xrotate-y,但现在多了rotate-z,有什么作用,把这个例子跑起来,你一眼就看明白了:

3d_example_3.gif

这个例子很直观的向我们展示了以下几点:

  1. 几何变换不光有平面变换,还有三维变换,其中旋转、缩放、平稳三个变换,即rotate/scale/translate都有对应的Z轴可以用
    • 注意:倾斜变换,即skew没有对应的Z轴版本可用
  2. 默认情况下,几何变换是先进行数学上的三维变换,然后把变换结果投影到二维平面上,即示例中第一个框中的效果
    • 这样做的后果是无论Z轴怎么变换,在数学上蓝红方块怎么交叉,渲染的时候都是挨个按z-index一个个的投影到父元素平面上去,所以视觉上是没有3D效果的
  3. 要想让变换结果在视觉上有3D效果,就需要在父元素上声明transform-3d,它对应的CSS样式是transform-style: preserve-3d
    • 这个CSS样式只有两个可取的值:flatpreserve-3d,其中flat是默认值,默认值也有对应的tailwindcss类:transform-flat,但鉴于这是默认值,所以一般用处不多

3D几何变换还有投影方式的区别

平常用不到的一些属性,不提了,查文档的话查perspective-originperspective