这不难吧?
换句话说,有很多使用两个div
的3D CSS立方体效果,也有很多只用一个div
的静态立方体,那么,用单个div
做出一个真正会旋转的3D立方体有多难呢?
剧透来了……可难多了!
我问了几个人是否认为这可行,他们都说“不可能,因为CSS根本不行”。
首先,你得明白,我喜欢做那些被认为不可能的事,特别是用CSS,因为我对CSS不擅长,但这反而让我更上手,这反而让我变得更擅长。
我是说,我做了一个仅用CSS的语法高亮器,用CSS实现了冒泡排序,甚至构建了一个纯CSS的神经网络(人工智能),比你想象的要简单得多。
那我肯定能用纯用CSS做一个单个div的3D立方体吧?
我来告诉你,这个挑战差点把我搞垮了,我已经尝试了三次,每次都没成功。
但是今天,我终于做到了这件事。
我真的觉得这可能是我做过最牛的事情!
想不想看?当然你会这么想,你来这里不就是想看看我在撒谎吗?
看看这个用纯CSS制作的3D立方体吧!(小心,这段CSS可能会让你有点晕,转转头吧!)
试试看,看看 HTML、JS 和 CSS 标签,
在本文的末尾还有一个互动示例,你可以通过滑块调整X和Y的旋转角度
哦,在你对JS有任何评论之前——那纯粹是为了以跨浏览器兼容的方式设置旋转位置,如果不是因为某些烦人的浏览器,我们本来可以用CSS Houdini属性来实现这一点,而无需使用CSS。
它只是通过使用CSS属性来旋转立方体而已,反正!
为什么这么难啊?简单来说,用一个div
我只有三个地方可以操作(div
本身,以及::before
伪元素和::after
伪元素)。
你知道吗?一个立方体(或正方体)其实有六个面!
这意味着我们得做看似简单的一步,找出哪三条边对着镜头并显示出来。
不过,任何从事过3D工作的人会告诉你,判断哪一面朝向镜头并不容易。
也就是说,从代码层面来说不算难,但用这种叫CSS的样式表?这就变得有点乱了!
让我告诉你几个我遇到的挑战!
1. 算算要展示哪一面.
这需要一些复杂的数学技巧。
幸好之前有很多人解决过这个问题,我只是借鉴了一些JS代码,然后将其转换成CSS。
这里有不少内容,最关键的是,通过一系列正弦和余弦的运算,就能得到该形状在三维空间中的X、Y和Z坐标。
获取立方体一个旋转面的三维坐标
这段代码是干啥的:
/* 这个面的法线向量,我们为每个面重复这些值,但更改数值以反映其在3D空间中的位置(例如,背面在z = -1,左边在x = -1等) */
--normals-front-x: 0;
--normals-front-y: 0;
--normals-front-z: 1;
/* 用于调整3D旋转以计算X, Y, Z坐标的公式 */
--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);
/* 对立方体的每个面重复上述步骤 */
进入全屏 退出全屏
获取z轴位置或通过点积计算
有了这些X、Y和Z坐标后,在3D空间中相对于我们,我们可以计算点积来找到形状相对于相机的中心Z位置。
这都是为了做
/* 获取相机位置的模长。值得注意的是,因为我使用的是X: 0,Y: 0 和 Z: 1,所以这实际上并不真正必要,因为--normalised-cam-z 为 1 和 --normalised-cam-x 和 --normalised-cam-y 为 0,但我为了完整保留了这部分计算 */
--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);
/* 计算相机法线与各个侧面X、Y、Z轴位置的点积并相加。 */
--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)));
/* 对每个面重复此操作 */
点击全屏进入,再次点击退出
这个点乘给出了沿相机视距的距离,也就是沿Z轴(即深度)的距离。
好了,我们现在可以开始看看哪里会冒出边了!
但这设置确实不少,我们现在有了方法,可以找出哪三个面离相机最近。
这一段话的核心就是:
--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 轴上前面的数值更大),我们就显示前面;如果后面更近,我们就显示后面。
所以究竟在瞎扯些什么最大最小值?
我们想把小数转换成布尔值。
所以我们这样做:
- 将两个数都乘以100(以确保它们的差值很可能大于1)。
- 用后面的数减去前面的数,这样我们要么得到一个正数,要么得到一个负数。
- 取两个数中的较大值与0中的较大值(如果前面的数 - 后面的数 > 0,我们得到一个正数,因为它比0大;但如果前面的数 - 后面的数 < 0,我们得到0,因为0比任何负数都大)。
- 然后取之前的计算结果和1中的较小值,以此确保任何正数不超过1。
我们也可以用 clamp(0, front-back, 1)
,但是不知道为什么我总是这样写!不过这样做!
我们终于有了一个布尔型的!
哇,这可真多,但现在我们有了一个布尔值,用来判断前部是否比后部更靠近相机,这样我们就可以在CSS中调整前后位置了。
.cube {
/* 其他样式属性 */
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轴,就像施了魔法一样,我们可以根据相机面对形状的方向移动形状的前后部分(当我们转动形状时,点积会改变,使得背面转到前面,因此我们会调整位置,让它面向我们)。
我们也可以对左右和上下进行类似的处理,但这个更简单,因为可以通过整个立方体的边长来移动侧面。
/* 将它移动一个立方体长度以切换到另一侧 */
translateZ(calc(var(--show-right) * var(--cube-size)));
/* 将它移动一个立方体长度以调整上下位置 */
translateZ(calc(var(--show-top) * var(--cube-size)));
切换到全屏 退出全屏
那应该就行了对吧?
其实这里有几个“坑”,所以这件事才变得这么棘手。
2. 前后调换
这事儿让我意外了!
因为我们使用了一个div,所以有个特别的地方。
你知道,当我们移动前面或后面的位置时,也会以同样的距离移动左右和上下位置。
这完全打乱了所有东西,因为我们只想让用户看到前端和后端相对于用户的互换位置。
这是因为 ::before
和 ::after
伪元素是相对于主要的 .cube
元素定位的。当 .cube
移动时,它们也移动。
所以我们得在 CSS 中处理这一点。
这就是为什么我们在 ::before
和 ::after
伪元素上应用了两个转换。
transform:
translate(-50%, 0%)
/* 此变换考虑了前后位置的变化,并将其以相同数量但相反方向移动,使其位置不变 */
translateZ(calc(
-1 * (var(--show-front) - var(--show-back)) * var(--cube-size) / 2
))
rotateY(90deg)
/* 旋转Y轴90度 */
translateZ(calc(
var(--show-right) * var(--cube-size)
));
点击全屏 恢复原大小
看到它你会觉得“这挺简单的”,但实际上我为了把它变得这么简单,不断调整立方体各个面的旋转和定位方式,这真的让我费了很大劲儿,因为我花了好几次尝试才勉强在脑海里把三维空间想象出来(尝试了足足10次!)
不管了,这个问题也搞定了吧?
3. 更改旋转为左右或上下。
差不多了,这就是最后一个问题啦!
当形状沿X轴旋转超过90度但不超过270度时,就会出状况。
这是因为我们在三维空间中前后位置互换并且旋转,左边变为了右边,右边变为了左边。
我们在 y 轴旋转 90 和 270 度时也遇到了同样的情况,因此我们的顶部和底部位置(相对位置)会变化(相对于正面/背面)。
这就是这小小的混乱所为何。
--X-above-90: Min(1, Max(calc(var(--rotX) - 90), 0)); /* 计算并限制在0到1之间 */
--X-below-90: calc(1 - var(--X-above-90));
--X-above-270: Min(1, Max(calc(var(--rotX) - 270), 0)); /* 计算并限制在0到1之间 */
--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))); /* 计算并限制在0到1之间 */
--X-Not-between-90-270: calc(1 - var(--X-between-90-270));
/* 对Y轴旋转进行相同的计算 */
全屏切换。
它让我们计算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
参数,因为我们使用了无分支判断。
相当于:
--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
设置为 true,即使点积结果表明应该显示左)。
差不多,但从那句话来看,跟上可能还是很棘手。
我发现玩是最好理解的方法。
这里有一个演示,你可以用滑块来调整X和Y的旋转角度,然后检查一下。
底下也有一些挺有趣的东西!
红色和绿色的矩形的间距是由我们在应用程序中使用的CSS属性设置的,因此可以看到这些值随滑块移动而变化。如果你检查这些形状并查看它们的间距,可以看到每个属性的具体值。
显示正面,显示背面等。如果它们在(开启)状态,则向右移动300像素;如果关闭,则保持在左侧不动。
试试调整这些滑块,看看是否一切都开始变得有道理了!💗
共同学习,写下你的评论
评论加载中...
作者其他优质文章