用纯CSS实现单个div元素的3D立方体📦!(他们说这不可能!)
这看起来很简单,对吧?
我的意思是,有很多使用两个 div 的 3D CSS 立方体,也有很多使用一个 div 的静态立方体,那么在单个 div 中实现一个真正的 3D 旋转立方体又能有多难呢?
剧透……难多了!
事实上,我问过一些人是否认为这可行,得到的回答是“不可能”,因为“CSS 根本无法胜任”。
你需要知道的是,我喜欢做“不可能的事”,尤其是在 CSS 方面(因为我 CSS 很烂,而这能让我进步!)。
我的意思是,我用纯 CSS构建了一个语法高亮器,用 CSS 实现了冒泡排序,甚至用 CSS 构建了一个神经网络。
所以,我肯定可以用纯 CSS 创建一个 3D 立方体的单 div 元素,对吧?
说实话,这个挑战差点把我难倒。我已经尝试了三次,都失败了。
但今天,我终于做到了。
我真心相信这可能是我做过的最棒的一件事!
想看看吗?
当然,你来这里就是为了验证我是否在说谎,对吧?
瞧瞧这(威严的?混乱的?)纯 CSS 实现的 3D 立方体!(友情提示:CSS 代码可能会让你有点懵!)
快去看看 HTML、JS 和 CSS 标签页吧!
本文末尾还提供了一个交互式演示,您可以在其中使用滑块设置 X 轴和 Y 轴的旋转角度。
哦,还有,在你对 JS 发表任何评论之前——它纯粹是以一种跨浏览器的方式设置旋转位置,如果不是因为某些烦人的浏览器,我们可以使用 CSS Houdini props 来做到这一点,而无需使用 CSS。
它实际上只是使用 CSS props 来改变立方体的旋转角度而已!
为什么这么难?
简而言之,对于单个 div 元素,我只有 3 个方面可以操作(div 元素本身、和::before伪::after元素)。
如果你还不知道的话,立方体有6个面!
这意味着我们必须做一件看似简单的事:找出哪三个面朝向摄像机并展示出来。
然而,任何从事过 3D 工作的人都会告诉你,确定哪一面朝向摄像机并不简单。
我的意思是,用代码实现起来并不难,但是用 CSS 实现呢?那就麻烦了!
让我带你了解一下我遇到的一些挑战!
1. 计算要展示哪一面
这需要相当扎实的数学功底。
幸运的是,在我之前已经有很多人解决了这个问题,我只是找了一些 JS 代码并将其转换为 CSS。
这其中有很多门道,但关键在于,一旦你进行大量的正弦、余弦运算,你就能得到一个形状在三维空间中的 X、Y 和 Z 位置。
获取立方体旋转面的 X、Y 和 Z 坐标
这就是这段代码的作用:
/* the "normals" for this side, we repeat these for each side but change the value to reflect it's position in 3D space (so the back is at z = -1, the left is at x = -1 etc.) */
--normals-front-x: 0;
--normals-front-y: 0;
--normals-front-z: 1;
/* the calculations to adjust the 3D rotation back to X, Y, Z coordinates */
--front-y1: calc((var(--normals-front-y) * cos(var(--xRot))) - (var(--normals-front-z) * sin(var(--xRot))));
--front-z1: calc(var(--normals-front-y) * sin(var(--xRot)) + var(--normals-front-z) * cos(var(--xRot)));
--front-x2: calc(var(--normals-front-x) * cos(var(--yRot)) + var(--front-z1) * sin(var(--yRot)));
--front-z2: calc(var(--normals-front-x) * -1 * sin(var(--yRot)) + var(--front-z1) * cos(var(--yRot)));
--front-x3: calc(var(--front-x2) * cos(var(--zRot)) - var(--front-y1) * sin(var(--zRot)));
--front-y3: calc(var(--front-x2) * sin(var(--zRot)) + var(--front-y1) * cos(var(--zRot)));
--front-x: var(--front-x3);
--front-y: var(--front-y3);
--front-z: var(--front-z2);
/* repeated for each side of the cube */
获取z轴位置或“点积”
一旦我们有了这些 X、Y 和 Z 位置(相对于我们在 3D 空间中的位置),我们就可以计算它们的“点积”来得出形状中心 Z 位置相对于相机方向的位置。
这就是这一切的意义所在:
/* getting the magnitude of the camera position. It is worth noting that because I use camera position of X: 0, Y: 0 and Z: 1 this is not really needed as --normalised-cam-z = 1 and --normalised-cam-x and --normalised-cam-y = 0, however I left it in for completeness */
--magnitude-cam: sqrt(calc((var(--normals-camera-x) * var(--normals-camera-x)) + (var(--normals-camera-y) * var(--normals-camera-y)) + (var(--normals-camera-z) * var(--normals-camera-z))));
--normalised-cam-x: var(--normals-camera-x) / var(--magnitude-cam);
--normalised-cam-y: var(--normals-camera-y) / var(--magnitude-cam);
--normalised-cam-z: var(--normals-camera-z) / var(--magnitude-cam);
/* getting the dot-product of the camera normals and the sides X, Y and Z positions and adding them up. */
--dot-prod-front: calc((var(--normals-camera-x) * var(--front-x)) + (var(--normals-camera-y) * var(--front-y)) + (var(--normals-camera-z) * var(--front-z)));
/* repeated for each side */
该点积给出了沿相机可视距离的距离,或沿 Z 轴(深度)的距离。
好了,现在我们可以开始确定边的位置了!
虽然设置过程比较繁琐,但现在我们有了所需的一切,一种可以查看哪 3 张脸离摄像头最近的方法!
这就是这一段的全部意义所在:
--show-front: Min(1, Max(calc(var(--dot-prod-front) * 100 - var(--dot-prod-back) * 100), 0));
--show-back: calc(1 - var(--show-front));
--show-right-dot: Min(1, Max(calc(var(--dot-prod-right) * 100 - var(--dot-prod-left) * 100), 0));
--show-left-dot: calc(1 - var(--show-right-dot));
--show-right: calc(var(--show-right-dot) * var(--X-Not-between-90-270) + var(--show-left-dot) * var(--X-between-90-270)); --show-left: calc(1 - var(--show-right));
不过,这里发生的事情可能并不那么显而易见,所以让我们来分析一下。
我们取前面的 z 坐标和后面的 z 坐标,并将它们进行比较。
如果前面比后面更靠近我们(z 位置更大),那么我们想表达这一点;如果后面更靠近我们,那么我们也想表达这一点。
那么,这些关于最小值和最大值的胡言乱语到底是什么意思呢?
我们要将十进制数转换为布尔值。
所以我们的做法是:
- 将这两个数都乘以 100(以确保它们的差值可能大于 1)。
- 用“后半部分”减去“前半部分”,这样我们就得到了一个正数或一个负数。
- 取两个数和 0 中的最大值(因此,如果前 - 后 > 0,我们将得到一个正数,因为它大于 0;但如果前 - 后 < 0,我们将得到 0,因为它现在大于负数)。
- 然后我们取前一个输出和 1 中的最小值,这是为了将所有正数限制在 1 以内。
我们也可以这样做,clamp(0, front-back, 1)但不知为何我总是最后这样写!
我们终于有了布尔值!
呼,内容真不少,但现在我们有了一个布尔值,用来表示前面是否比后面更靠近摄像头,然后我们可以在前/后 CSS 中使用它来移动侧面的位置:
.cube {
/* other props */
translateZ(calc(
(var(--show-front) - var(--show-back)) * var(--cube-size) * 0.5
));
(1 - 0) * 100px * 0.5因此,如果正面朝上,则得到 (50px);(0 - 1) * 100px * 0.5如果背面朝上,则得到(-50px)。
然后我们将其应用于 z 轴,就像变魔术一样,我们可以根据哪个面向摄像机来移动形状的前后方向(当我们旋转形状时,点积会发生变化,使得后 > 前,因此我们交换位置,使其面向我们)!
我们可以对左右和上下进行类似的操作,但这种方法更简单,因为我们可以将边移动立方体的整个长度:
/* left / right adjustment to move it one cube length to switch sides*/
translateZ(calc(var(--show-right) * var(--cube-size)));
/* top / bottom adjustment */
translateZ(calc(var(--show-top) * var(--cube-size)));
应该就是这样了吧?
不,我们遇到了一些“难题”,这就是为什么这件事变得如此困难!
2. 前后位置变化
这让我措手不及!
因为我们只使用了一个div元素,所以我们遇到了一个特殊的问题。
你看,当我们移动正面/背面的位置时,它也会以相同的幅度移动左侧/右侧和顶部/底部的位置。
这完全破坏了一切,因为我们只希望前/后位置相对于用户发生变化。
这是因为::before伪::after元素是相对于主.cube元素定位的。.cube它们会移动,也会移动。
所以我们需要在 CSS 中考虑到这一点。
这就是为什么我们对原始元素::before和::after伪元素分别进行了两次变换的原因。
transform:
translate(-50%, 0%)
/* this transform accounts for the front / back changing position and moves this face by the same amount in the opposite direction so that it stays in the same location */
translateZ(calc(
-1 * (var(--show-front) - var(--show-back)) * var(--cube-size) / 2
))
rotateY(90deg)
translateZ(calc(
var(--show-right) * var(--cube-size)
));
当你看到它时,你会想“嗯,这很简单”,但为了使其如此简单,我不得不多次彻底改变旋转和定位立方体侧面的方式,这真的让我很头疼,因为我试图在 3D 空间中将其可视化(很糟糕!我尝试了大约 10 次!)。
总之,这个问题解决了,我们应该可以结束了吧?
3. 旋转方向改变为左/右和上/下
差不多,这是最后一个问题了!
当一个形状绕 X 轴旋转超过 90 度(但小于 270 度)时,就会出现问题。
这是因为我们在 3D 空间中不断变换前后位置和旋转,导致左变成右,右变成左。
我们遇到同样的问题,当绕 y 轴旋转 90 度和 270 度时,顶部和底部位置会发生变化(相对于正面/背面)。
这就是这场小混乱的起因:
--X-above-90: Min(1, Max(calc(var(--rotX) - 90), 0));
--X-below-90: calc(1 - var(--X-above-90));
--X-above-270: Min(1, Max(calc(var(--rotX) - 270), 0));
--X-below-270: calc(1 - var(--X-above-270));
--X-between-90-270: Max(0, 1 - Max(var(--X-below-90), var(--X-above-270)));
--X-Not-between-90-270: calc(1 - var(--X-between-90-270));
/* repeated for the Y axis rotation */
它可以计算 X 轴旋转是否在 90 到 270 之间,以便我们可以反转左右位置;Y 轴旋转也是如此,以便我们可以反转上下位置。
这就是为什么我们--show-right-dot需要--show-right进行这些变换:
--show-right-dot: Min(1, Max(calc(var(--dot-prod-right) * 100 - var(--dot-prod-left) * 100), 0)
);
--show-left-dot: calc(1 - var(--show-right-dot));
--show-right: calc(var(--show-right-dot) * var(--X-Not-between-90-270) + var(--show-left-dot) * var(--X-between-90-270));
--show-left: calc(1 - var(--show-right));
关键在于--show-right我们如何进行无分支 if 语句!
--show-right: calc(var(--show-right-dot) * var(--X-Not-between-90-270) + var(--show-left-dot) * var(--X-between-90-270));相当于:
right(1) * not between(1) + left(0) * between(0)如果 x < 90 或 x > 270,并且我们的点积表明应该显示右侧(输出为 1)。right(1) * not between(0) + left(0) * between(1)如果 x 在 90 到 270 之间,则应该显示右侧(输出为 0 - 我们已经将右侧交换到左侧)。right(0) * not between(1) + left(1) * between(0)如果 x < 90 或 x > 270,并且我们的点积表明左边应该显示(输出为 0,即左边)。right(0) * not between(0) + left(1) * between(1)如果 x 在 90 到 270 之间,则应该显示右侧(输出为 1 - 我们现在使其为--show-right真,即使我们的点积表明应该显示左侧)。
就是这样!
差不多了,不过从这个解释来看,可能还是很难理解。
我觉得最简单的理解方法就是玩!
这里有一个演示,您可以使用滑块设置 X 和 Y 轴的旋转角度,然后进行检查。
底部还有一些有趣的东西!
这些红色和绿色矩形的边距由我们在应用程序中使用的 CSS 属性设置,因此您可以看到,当您移动滑块时,这些值会发生变化。如果您检查这些形状并查看边距,则可以看到每个属性的具体值。
显示正面、显示背面等。如果启用这些功能,则向右移动 300px(如果禁用,则保持在左侧)。
试试调整滑块,看看是不是一切都豁然开朗了!💗
文章来源:https://dev.to/grahamthedev/single-div-actually-3d-cube-in-pure-css-they-said-it-was-impossible-48m5