马黑黑 发表于 2025-7-7 12:56

理解CSS 3d transform转换

本帖最后由 马黑黑 于 2025-7-7 20:42 编辑 <br /><br /><style>
        .artBox { font-size: 18px; }
        .artBox > p { margin: 10px 0; line-height: 30px; }
        .artBox mark { padding: 4px 6px; background: lightblue; }
        .tRed { color: red; }
        #prevBox { position: fixed; top: 0; right: 0; bottom: 0; left: 0; background: #eee; display: none; padding: 0; overflow: hidden; z-index: 1000; margin: 0; }
        #prevBox::after { position: absolute; content: '关闭预览'; bottom: 10px; left: calc(50% - 40px); padding: 0 4px; width: 80px; height: 30px; line-height: 30px; text-align: center; border: 1px solid #efe; border-radius: 6px; background: #eee; font-size: 14px; box-shadow: 2px 2px 6px rgba(0,0,0,.25); cursor: pointer; }
        iframe { position: relative; width: 100%; height: 100%; border: none; outline: none; box-sizing: border-box; margin: 0; }
</style>

<div id="prevBox"></div>
<div class="artBox">
        <p>当我们需要在Web中渲染HTML元素的3d效果,<mark>transform</mark>转换属性就能派上用场,该属性也有人称之为形变或变形属性。transform属性允许我们将HTML元素进行旋转(rotate)、缩放(scale)、移动(translate)和扭曲(skew)或以更复杂而精确的 matrix 方式实现前述的元素转换效果。CSS的transform属性提供两种转换场景:① 基于二维(xy)的2d转换,② 基于三维(xyz)的3d转换。</p>
        <p>通过transform实现3d转换,首先需要<span class="tRed">容器元素</span>设置有<mark>perspective</mark>属性,该属性用于定义三维透视视图,以营造景深效果,属性值为长度数值,例如,<mark>perspective: 1200px</mark>,这表示观者所能看到的场景其纵深为1200个像素。一般而言,perspective属性值越小3d效果越容易呈现出来但也容易失真、越大3d效果越不明显但总体效果更为细腻。以下代码演示如何在CSS中设置perspective属性:</p>
        <div class="hEdiv" data-name="CSS代码"><pre class="hEpre">
.wrapper {
        perspective: 1200px;
}
        </pre></div>
        <p>这样,假如有一个class="wrapper"的元素,则该元素内的子元素就具备了以3d渲染自己的机会,前提是它或它们要使用transform属性的3d相关的转换参数——</p>
        <div class="hEdiv" data-name="CSS+HTML代码"><pre class="hEpre">
&lt;style&gt;
        .wrapper {
                perspective: 1200px;
        }
        .inner {
                transform: rotateX(45deg) rotateY(15deg) rotateZ(60deg);
        }
&lt;/style&gt;

&lt;div class="wrapper"&gt;
        &lt;div class="inner"&gt;&lt;/div&gt;
&lt;/div&gt;
        </pre></div>
        <p>上面的代码由于缺少诸如尺寸、背景颜色等相关细节,运行起来不会有什么结果,这些代码只是演示元素基于3d转换的组织方式,其核心是,<span class="tRed">父元素即容器元素设置三维透视视图(perspective),子元素应用3d transform转换(例如上述代码中的 rotateX/Y/Z)</span>。</p>
        <p>元素实现3d转换还有两个重要的属性:</p>
        <div class="hEdiv" data-name="CSS代码"><pre class="hEpre">
.wrapper {
        /* 其它代码,例如 perspective: 1200px; */
        transfrom-origin: center; /* 转换原点 : 缺省默认是 center */
        transform-style: flat; /* 转换形态 : 缺省默认是 flat | 可选值 preserve-3d */
}
        </pre></div>
        <p>以上两个属性都需要在容器元素中设置。其中:</p>
        <p>① 属性 <span class="tRed">transform-origin</span> 转换原点可以使用单值、双值、三值,单值时表示xy都是用同一个值、z值为0,双值表示xy值各自设计、z值为0,三值表示xyz各自设计,属性值支持百分比、距离单位、关键字(left、right、top、bottom、center)。</p>
        <p>② 属性 <span class="tRed">transform-style</span> 转换形态只有两个值可选,属性缺省时默认值为<span class="tRed">flat</span>,可选值 <span class="tRed">preserve-3d</span>。flat意为平面,但这里它并不是表示2d的意思,它的确切含义是将Z轴挤压在xy组成的面上,呈现出来的结果是3d的效果,但由于挤压在一个平面之上,其效果并不真实自然,可以将之理解为伪3d;preserve-3d一旦启用,渲染结果则是真正的3d效果,其规范的是 <span class="tRed">在3d场景中建立3d对象的真实遮挡关系</span>而非将z轴挤压在xy平面之上。换言之,flat展现形态所渲染的3d效果没有真实3d世界对象间的遮挡关系,彼此间的遮挡行为所依循的是元素在代码流中的先后出现次序以及在CSS中所设置的 z-index 层级关系,而preserve-3d则模拟真实世界对象间的现实位置关系,其中元素对象的Z轴正负值是遮挡关系的主要依据(元素在代码流中的出现次序、CSS的z-index层级关系依然存在但仅在Z轴的值为0或彼此该值相等的时候有效)。</p>
        <p>最后我们给出一组可以在线运行的代码,通过比对代码和运行效果将有助于对本文介绍的知识的理解。代码中,有一个容器元素 id="papa",其内有两个子元素,其一是 id="daughter" 的长方形,运行时它保持固定不变,其二是 id="son" 的圆球,运行后可以通过滑杆操作它在xyz三轴上的位置,此外,3d场景展现模式默认为flat,可以在运行后启用preserve-3d:</p>
        <div class="hEdiv" data-name="CSS+HTML+JS代码"><pre class="hEpre" id="pre1">
&lt;style&gt;
        #papa {
                margin: 20px auto;
                width: 800px;
                height: 560px;
                border: 1px solid gray;
                transform-style: flat;
                perspective: 600px;
                display: grid;
                place-items: center;
                position: relative;
        }
        #son {
                position: absolute;
                width: 50px;
                height: 50px;
                border-radius: 50%;
                background: linear-gradient(35deg, green, skyblue);
                filter: drop-shadow(2px 4px 8px gray);
                transform: rotateZ(var(--a)) translate3d(var(--x), var(--y), var(--z));
                --x: 0;
                --y: 0;
                --z: 0;
                --a: 0;
        }
        #daughter {
                position: absolute;
                width: 400px;
                height: 160px;
                background: linear-gradient(olive, pink);
                opacity: 0.9;
                transform: rotateX(45deg) rotateY(15deg) rotateZ(30deg);
        }
        .wrap { margin: auto; width: 800px; height: fit-content; position: relative; }
        .wrap button, .wrap label, .wrap input { color: green; cursor: pointer; }
        .wrap output { color: red; }
&lt;/style&gt;

&lt;div id="papa"&gt;
        &lt;div id="daughter"&gt;&lt;/div&gt;
        &lt;div id="son"&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="wrap"&gt;
        &lt;label for="rangeX"&gt;小球 X 坐标: &lt;/label&gt;
        &lt;input id="rangeX" type="range" min="-400" max="400" value="0" /&gt;
        &lt;output&gt;0&lt;/output&gt;&lt;br&gt;
        &lt;label for="rangeY"&gt;小球 Y 坐标: &lt;/label&gt;
        &lt;input id="rangeY" type="range" min="-300" max="300" value="0" /&gt;
        &lt;output&gt;0&lt;/output&gt;&lt;br&gt;
        &lt;label for="rangeZ"&gt;小球 Z 坐标: &lt;/label&gt;
        &lt;input id="rangeZ" type="range" min="-600" max="600" value="0" /&gt;
        &lt;output&gt;0&lt;/output&gt;&lt;br&gt;
        &lt;label for="checkBox"&gt;启用 preserve-3d : &lt;/label&gt;
        &lt;input id="checkBox" type="checkbox" value="chk" /&gt;
        &lt;output&gt;transform-style: flat&lt;/output&gt;&lt;br&gt;
&lt;/div&gt;

&lt;script&gt;
        const ranges = [ , , ];
        const outputs = document.querySelectorAll('output');
        ranges.forEach( (range, idx) =&gt; {
                range.oninput = () =&gt; {
                        outputs.value = range.value + 'px';
                        son.style.setProperty(range, range.value + 'px');
                };
        });
        checkBox.onclick = () =&gt; {
                papa.style.setProperty('transform-style', checkBox.checked ? 'preserve-3d' : 'flat');
                outputs.value = 'transform-style: ' + (checkBox.checked ? 'preserve-3d' : 'flat');
        };
&lt;/script&gt;
        </pre></div>
        <blockquote><button id="btnPrev1">运行代码</button></blockquote>
        <p>注意:演示代码故意设置Z轴的最大值为600px,和景深设置值一致。调整Z轴的值为600px,将什么都看不到,原因是此时观者的眼睛和屏幕表面同在一个水平线上了。</p>
        <p>3d转换不止这些内容,本文仅揭示其冰山的一角,还有更多精彩留待大家探索。</p>
</div>

<script type="module">
        import hlight from 'https://638183.freep.cn/638183/web/helight/helight1.js';
        const pres = document.querySelectorAll('.hEpre');
        const divs = document.querySelectorAll('.hEdiv');
        divs.forEach( (div, key) => hlight.hl(div, pres));
       
        const preView = (htmlCode, targetBox) => {
                if (targetBox.innerHTML) return;
                const iframe = document.createElement('iframe');
                htmlCode = htmlCode + '<style>body {margin: 0; }</style>';
                iframe.srcdoc = htmlCode;
                targetBox.appendChild(iframe);
                targetBox.style.display = 'block';
                targetBox.onclick = () => {
                        targetBox.innerHTML = '';
                        targetBox.style.display = 'none';
                }
        };

        btnPrev1.onclick = () => preView(pre1.textContent, prevBox);
</script>

红影 发表于 2025-7-7 13:58

启用preserve-3d后,在例子里很容易看到遮挡效果呢。

红影 发表于 2025-7-7 14:01

原来Z轴的值和景深也有呼应关系。{:4_204:}

马黑黑 发表于 2025-7-7 16:45

红影 发表于 2025-7-7 14:01
原来Z轴的值和景深也有呼应关系。

z轴与xy面垂直:单独改变小球的z轴时更容易领会这一点。

xy面可以理解为就是屏幕。那么,z轴和我们眼睛看屏幕的方向是一致的,因此,z轴和景深向呼应。

花飞飞 发表于 2025-7-7 18:50

perspective属性值越小3d效果越容易呈现出来但也容易失真、越大3d效果越不明显但总体效果更为细腻.
本来对这句不是很理解,试着把600变成100看了一下,粒子扑面而来,果然效果更加明显。。
{:4_173:}

花飞飞 发表于 2025-7-7 19:02

这个动画明显看出启用 preserve-3d与否的遮挡差别,
天哪,还好每个教程都有画面演示{:4_173:},
这个做成滑杆操作的动画,边玩边学了,超级好教程

马黑黑 发表于 2025-7-7 19:34

花飞飞 发表于 2025-7-7 19:02
这个动画明显看出启用 preserve-3d与否的遮挡差别,
天哪,还好每个教程都有画面演示,
这个做 ...

我预设了加入旋转的变化。这个变化更为复杂,所以只预留,没去做演示

马黑黑 发表于 2025-7-7 19:36

花飞飞 发表于 2025-7-7 18:50
perspective属性值越小3d效果越容易呈现出来但也容易失真、越大3d效果越不明显但总体效果更为细腻.
本来对 ...

不知道你看过3d电影木有?铁达尼号里,船摇摆的时候,女角的bro颤动让很多人终身难忘,好像掠过自己脸庞一样。3d效果就是这么震撼。

杨帆 发表于 2025-7-7 20:05

谢谢马老师经典讲解与示范{:4_191:}

马黑黑 发表于 2025-7-7 20:05

杨帆 发表于 2025-7-7 20:05
谢谢马老师经典讲解与示范

{:4_190:}

花飞飞 发表于 2025-7-7 20:08

马黑黑 发表于 2025-7-7 19:36
不知道你看过3d电影木有?铁达尼号里,船摇摆的时候,女角的bro颤动让很多人终身难忘,好像掠过自己脸庞 ...

{:4_173:}你最会找重点了。3D的看过没哪部有印象

花飞飞 发表于 2025-7-7 20:10

马黑黑 发表于 2025-7-7 19:34
我预设了加入旋转的变化。这个变化更为复杂,所以只预留,没去做演示

旋转演示应该也很直观的吧,可以想象
毕竟贴子里基本都有旋转

马黑黑 发表于 2025-7-7 20:10

花飞飞 发表于 2025-7-7 20:08
你最会找重点了。3D的看过没哪部有印象
没被掠过{:4_170:}
其实被踹一脚也行的{:4_189:}

马黑黑 发表于 2025-7-7 20:11

花飞飞 发表于 2025-7-7 20:10
旋转演示应该也很直观的吧,可以想象
毕竟贴子里基本都有旋转

这个嘛,小球若旋转,xyz轴上的位置设定就没有那么直观,而这个演示重点是translate3d,先得照顾它们

花飞飞 发表于 2025-7-7 20:17

马黑黑 发表于 2025-7-7 20:10
没被掠过
其实被踹一脚也行的

{:4_186:}隔屏瘙痒{:4_170:}

花飞飞 发表于 2025-7-7 20:18

马黑黑 发表于 2025-7-7 20:11
这个嘛,小球若旋转,xyz轴上的位置设定就没有那么直观,而这个演示重点是translate3d,先得照顾它们

如果是干扰的话,那还是保证重点好了。。
不然还得想法子先让它停下来再研究。。{:4_173:}

马黑黑 发表于 2025-7-7 20:18

花飞飞 发表于 2025-7-7 20:17
隔屏瘙痒

不受用也不会受伤

马黑黑 发表于 2025-7-7 20:19

花飞飞 发表于 2025-7-7 20:18
如果是干扰的话,那还是保证重点好了。。
不然还得想法子先让它停下来再研究。。

不论2d3d,先旋转紧跟着平移,都会绕圈圈,3d下很多时候不容易理解,比如突然就不见了

红影 发表于 2025-7-7 20:35

马黑黑 发表于 2025-7-7 16:45
z轴与xy面垂直:单独改变小球的z轴时更容易领会这一点。

xy面可以理解为就是屏幕。那么,z轴和我们眼 ...

是的,Z轴就是通常的远近吧,迎面而来还是背向而去。

马黑黑 发表于 2025-7-7 20:38

红影 发表于 2025-7-7 20:35
是的,Z轴就是通常的远近吧,迎面而来还是背向而去。

对,就是酱紫
页: [1] 2 3 4
查看完整版本: 理解CSS 3d transform转换