最近被问到一个问题,关于楼层模型可视化,就是想建立一栋楼的模型,然后可以细分看到每一层的区域,可以旋转、移动,我第一时间拿到看到这个问题想到的就是用3D方式,用three.js吧,但是后面又慢慢分析了下,用three稍微有点用牛刀的感觉,然后想能不能简单点,用CSS3的3d属性来解决这个问题。
首先我们来先构建一个立方体,立方体是由6个面,我们简称1(上)、2(左)、3(后)、4(下)、5(右)、6(前)组成。我们先来看一下立方体的最终效果图:
原理就是我们先把6个面正对我们放置,参考下面面6的位置,面6只是在Z轴上面正方面移动了N像素,那么我们可以把面6当成一个基础参考面,
其他面都是通过移动XYZ轴和翻转的方式,把6个面凑成一个立方体,比如说面1,这个时候面1和面6是相互重叠的,那怎么样把面1从面6的位置移动到顶部面1的位置呢?其实就是在基础位置上往Y轴的负方向上移动,然后再水平翻转90度(相对于X轴),对应的效果如下:
当面1和面6通过图形变换后连接成立方体的两个面之后,剩下的4个面按照面1的方式依次进行转换,就可以完成整个立方体的搭建,搭建完就成了如第一张图片所示,下图是我通过把整个立方体转换一下角度的效果:
但是一栋楼要如何构建又是一个问题,最开始我想的是如果能先生成一个立方体,那么一栋楼由N个立方体搭建堆砌起来就OK了,我按照这个想法先试了两个立方体,然后根据位置进行排列,其实这种方法从正面视觉上来看没问题的,如下图:
但是上面的操作方式有个问题,就是如果要通过样式去操作的话,从视角来看,只能单独一个个立方体操作,这样就达不到一个整体的要求,会出现下图的问题,通过旋转之后每个立方体都是单独的的变换,所以就会导致整体的位置偏差。
其实在这里的时候,稍微有点被卡住,但是总感觉离最终效果就差一步了,突然想起了真正的修楼步骤,其实楼栋的修建也是每一层每一个面进行的操作,并不是像集装箱那样搭建的,那么同理,这个程序也同样的,可以把每一个面为最小单元,上一层的每一个面只是下一层对应面在Y轴上面的移动复制,那么我可以从第一层搭建基础,一层一层一个面一个面的搭建,最终搭成整体的一个楼,而且这样代码量其实在一个立方体的基础上是很少的,只需要计算出当前层数的索引数。
我们依次来做面1到面6的复制和平移,依次得到如下效果:(PS这里在自查的时候发现一个逻辑问题,就是其实面1和面4的顺序应该调整一下,应该先修底面,最后才是顶部,不过对于程序的理解目前这种方式也没问题,所以就不再做调整)
通过这样的方式搭建的多层立方体,因为是以面为基础单位,那么多层就成了一个整体,这样在整体旋转的时候就很好办了,整体旋转效果图如下:
上面左图是水平旋转,右图是垂直旋转,这样使得多层立方体为一个整体。那么再简单的通过代码的调整通过改变一个参数就可以动态实现楼栋的层数,下图是7层和20层的效果,通过样式改变我对Y轴进行0.2和0.15的压缩,如果需要的话可以通过立方体的宽高再去做调整,而不用做压缩。
当然我们也可以通过改变底图来做一些特殊效果,比如说我做成长条形的方块,如下图:
当然这个只是其中的一个应用,因为整体的空间已经搭建完成,我们还可以在这个空间里面做一些贴图,这样就更像是一个立体的空间,我们也可以拉近视角,进入到楼栋里面,再深入一些就成了现在比较流行的全景,当然如果用这种方式来处理,在涉及到一些超大图片时,应该还是比较消耗性能,比较流行的解决方案还是通过three.js来处理,等后面如果有机会再来一起探讨下。下面贴出这次全部的代码,因为涉及到标签的动态增加,所以用了vue来写,这样代码看起来更简单,如果对vue不是很熟悉的,可以看下css代码,用原生js或者jquery也是能达到同样效果的。
1 <!DOCTYPE html> 2 <html lang="en"> 3 4 <head> 5 <meta charset="UTF-8"> 6 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 7 <title>Document</title> 8 <style> 9 .main { 10 height: 900px; 11 width: 900px; 12 position: absolute; 13 border: 0px solid black; 14 transform: rotateX(-10deg) rotateY(10deg) scaleY(0.3) ; 15 transform-style: preserve-3d; 16 left: 200px; 17 top: 200px; 18 } 19 20 .main>div { 21 position: absolute; 22 left: 50%; 23 top: 50%; 24 height: 450px; 25 width: 450px; 26 display: flex; 27 justify-content: center; 28 align-items: center; 29 font-size: 60px; 30 font-weight: bold; 31 color: white; 32 } 33 34 .main2 { 35 top: 690px 36 } 37 </style> 38 </head> 39 <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> 40 <body> 41 <div class="box" id="app"> 42 <div class="main main1" id="targetObj"> 43 <div v-for="c in count" :style="`${getStyle(c)}`"> 44 {{c}} 45 <div v-if="c % 6 == 4" 46 style=" 100%;height:100%;background-color:black;position: absolute;left: 0;top: 0;display: flex;"> 47 <div v-for="i in 10" style="flex:1;background-color: aquamarine;margin: 5px;"></div> 48 </div> 49 </div> 50 </div> 51 </div> 52 53 </body> 54 <script> 55 new Vue({ 56 el: "#app", 57 data: { 58 count: 42,//楼层数 59 }, 60 methods: { 61 getStyle(index) { 62 switch (index % 6) { 63 case 0: { 64 //首先样式是一个平面的背景色,最主要的就是一个索引的计算,index是请求的面索引,通过面索引除以6,根据结果取整得到层索引,进而得到移动的像素值。 65 return `backgroundColor:brown;transform: translate(-50%, -50%) translateY(${parseInt((index / 6)-1)*(-450)}px) translateZ(225px);opacity:0.8` 66 } 67 case 1: { 68 return `backgroundColor:pink;transform: translate(-50%, -50%) translateY(${-225-parseInt(index/6)*(450)}px) rotateX(90deg);` 69 } 70 case 2: { 71 return `backgroundColor:blue;transform: translate(-50%, -50%) translateX(-225px) translateY(${parseInt(index / 6)*(-450)}px) rotateY(90deg);opacity:0.8` 72 } 73 case 3: { 74 return `backgroundColor:green;transform: translate(-50%, -50%) translateY(${parseInt(index / 6)*(-450)}px) translateZ(-225px);opacity:0.4` 75 } 76 case 4: { 77 return `backgroundColor:yellow;transform: translate(-50%, -50%) translateY(${225-parseInt(index/6)*(450)}px) rotateX(90deg);` 78 } 79 case 5: { 80 return `backgroundColor:black;transform: translate(-50%, -50%) translateX(225px) translateY(${parseInt(index / 6)*(-450)}px) rotateY(90deg);opacity:0.4` 81 } 82 } 83 } 84 }, 85 }) 86 </script> 87 88 89 </html>