├── assets └── bg.jpeg ├── index.html ├── 矩阵.js ├── style.css ├── note.md └── index.js /assets/bg.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GarvinPanda/-CSS3D-/HEAD/assets/bg.jpeg -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 移动的盒子 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 |
18 |
19 |
20 |
z
21 |
z
22 |
y
23 |
y
24 |
x
25 |
x
26 |
27 |
28 |
29 |
30 | 31 |
32 |
33 |
34 | 35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
0步
44 |
在0步内走到终点
45 |
提示
46 |
47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /矩阵.js: -------------------------------------------------------------------------------- 1 | 2 | function Matrix4() { 3 | this.elements = new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]); 4 | Matrix4.prototype.set = function(n11, n12, n13, n14, n21, n22, n23, n24, n31, n32, n33, n34, n41, n42, n43, n44) { 5 | var te = new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]); 6 | te[0] = n11; te[1] = n12; te[2] = n13; te[3] = n14; 7 | te[4] = n21; te[5] = n22; te[6] = n23; te[7] = n24; 8 | te[8] = n31; te[9] = n32; te[10] = n33; te[11] = n34; 9 | te[12] = n41; te[13] = n42; te[14] = n43; te[15] = n44; 10 | return te; 11 | } 12 | Matrix4.prototype.multiplyMatrices = function (a,b) { 13 | var ae = a; 14 | var be = b; 15 | var te = new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]); 16 | 17 | var a11 = ae[0], a12 = ae[4], a13 = ae[8], a14 = ae[12]; 18 | var a21 = ae[1], a22 = ae[5], a23 = ae[9], a24 = ae[13]; 19 | var a31 = ae[2], a32 = ae[6], a33 = ae[10], a34 = ae[14]; 20 | var a41 = ae[3], a42 = ae[7], a43 = ae[11], a44 = ae[15]; 21 | 22 | var b11 = be[0], b12 = be[4], b13 = be[8], b14 = be[12]; 23 | var b21 = be[1], b22 = be[5], b23 = be[9], b24 = be[13]; 24 | var b31 = be[2], b32 = be[6], b33 = be[10], b34 = be[14]; 25 | var b41 = be[3], b42 = be[7], b43 = be[11], b44 = be[15]; 26 | 27 | te[0] = a11 * b11 + a12 * b21 + a13 * b31 + a14 * b41; 28 | te[4] = a11 * b12 + a12 * b22 + a13 * b32 + a14 * b42; 29 | te[8] = a11 * b13 + a12 * b23 + a13 * b33 + a14 * b43; 30 | te[12] = a11 * b14 + a12 * b24 + a13 * b34 + a14 * b44; 31 | 32 | te[1] = a21 * b11 + a22 * b21 + a23 * b31 + a24 * b41; 33 | te[5] = a21 * b12 + a22 * b22 + a23 * b32 + a24 * b42; 34 | te[9] = a21 * b13 + a22 * b23 + a23 * b33 + a24 * b43; 35 | te[13] = a21 * b14 + a22 * b24 + a23 * b34 + a24 * b44; 36 | 37 | te[2] = a31 * b11 + a32 * b21 + a33 * b31 + a34 * b41; 38 | te[6] = a31 * b12 + a32 * b22 + a33 * b32 + a34 * b42; 39 | te[10] = a31 * b13 + a32 * b23 + a33 * b33 + a34 * b43; 40 | te[14] = a31 * b14 + a32 * b24 + a33 * b34 + a34 * b44; 41 | 42 | te[3] = a41 * b11 + a42 * b21 + a43 * b31 + a44 * b41; 43 | te[7] = a41 * b12 + a42 * b22 + a43 * b32 + a44 * b42; 44 | te[11] = a41 * b13 + a42 * b23 + a43 * b33 + a44 * b43; 45 | te[15] = a41 * b14 + a42 * b24 + a43 * b34 + a44 * b44; 46 | return te; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | html, 6 | body { 7 | font-size: 24px; 8 | width: 100%; 9 | height: 100%; 10 | } 11 | body{ 12 | background: linear-gradient(135deg,#e0e0e0 10%,#ffffff 90%); 13 | height: 100vh; 14 | overflow: hidden; 15 | display: flex; 16 | background: url("./assets/bg.jpeg") center center no-repeat; 17 | } 18 | .camera{ 19 | position: absolute; 20 | top: 20px; 21 | left: 20px; 22 | width: 1220px; 23 | height: 1080px; 24 | perspective: 1200px; 25 | -webkit-perspective: 1200px; 26 | perspective-origin: 500px -200px; 27 | transform-style: preserve-3d; 28 | border: 5px solid #f45262; 29 | } 30 | 31 | 32 | .camera .ground{ 33 | position: absolute; 34 | width: 900px; 35 | height: 900px; 36 | top: 50%; 37 | left: 50%; 38 | border: 2px solid #17b1e7; 39 | transform-origin: 50% 50%; 40 | transform-style: preserve-3d; 41 | transform: translateX(-50%) translateY(-80%) rotateX(90deg) rotateZ(220deg); 42 | } 43 | 44 | .box{ 45 | transform-style: preserve-3d; 46 | position: absolute; 47 | top: 50%; 48 | left: 50%; 49 | width: 750px; 50 | height: 750px; 51 | perspective: 800px; 52 | -webkit-perspective: 800px; 53 | perspective-origin: 400px 400px; 54 | border: 2px solid #17b1e7; 55 | transform: translateX(-50%) translateY(-50%) rotateZ(0deg); 56 | } 57 | 58 | .step{ 59 | position: absolute; 60 | right: 50px; 61 | width: auto; 62 | color: #23c1f0; 63 | 64 | } 65 | 66 | .per-step{ 67 | position: absolute; 68 | left: 50%; 69 | width: auto; 70 | color: #23c1f0; 71 | } 72 | .per-step span{ 73 | color: red; 74 | } 75 | 76 | .tip{ 77 | position: absolute; 78 | right: 20px; 79 | top: 100px; 80 | width: 100px; 81 | height: 40px; 82 | border-radius: 20px; 83 | background: #23c1f0; 84 | color: #fff; 85 | line-height: 40px; 86 | text-align: center; 87 | } 88 | 89 | .pan{ 90 | width: 100%; 91 | height: 100%; 92 | display: flex; 93 | flex-wrap: wrap; 94 | } 95 | 96 | .pan .pan-item{ 97 | width: 48px; 98 | height: 48px; 99 | border: 1px solid #17b1e7; 100 | color: #fff; 101 | } 102 | .pan .pan-item:last-child{ 103 | background: #fb7922; 104 | } 105 | .pan .pan-path{ 106 | background-color: #b6f977; 107 | animation: light 1s ease-in infinite; 108 | } 109 | 110 | .box-con{ 111 | position: absolute; 112 | top: 0; 113 | left: 0; 114 | width: 50px; 115 | height: 50px; 116 | /* border: 1px solid #f45262; */ 117 | transform-style: preserve-3d; 118 | transform-origin: 50% 50%; 119 | transform: translateZ(25px) ; 120 | transition: all 2s cubic-bezier(0.075, 0.82, 0.165, 1); 121 | } 122 | .block{ 123 | transform-style: preserve-3d; 124 | width: 48px; 125 | height: 48px; 126 | transform-origin: 25px 25px; 127 | transform: translateZ(-25px) ; 128 | transform: translateZ(25px) ; 129 | transition: all 2s cubic-bezier(0.075, 0.82, 0.165, 1); 130 | } 131 | 132 | .linex{ 133 | width: 200px; 134 | height: 2px; 135 | position: absolute; 136 | left: -75px; 137 | top: 25px; 138 | background: red; 139 | } 140 | .liney{ 141 | width: 200px; 142 | height: 2px; 143 | position: absolute; 144 | left: -75px; 145 | top: 25px; 146 | background: #0f0; 147 | transform: rotateZ(90deg); 148 | } 149 | .linez{ 150 | width: 200px; 151 | height: 2px; 152 | position: absolute; 153 | left: -75px; 154 | top: 25px; 155 | background: #00f; 156 | transform: rotateY(90deg); 157 | } 158 | 159 | 160 | .wall{ 161 | color: #fff; 162 | content: ""; 163 | position: absolute; 164 | font-size: 24px; 165 | width: 100%; 166 | height: 100%; 167 | display: flex; 168 | justify-content: center; 169 | align-items: center; 170 | border: 1px solid #fdd894; 171 | background-color: #fb7922; 172 | /* opacity: 0; */ 173 | color: #f9fde6; 174 | } 175 | 176 | .block .wall{ 177 | background-color: #152534; 178 | border: 1px solid #0c94e0; 179 | } 180 | 181 | .wall:nth-child(1) { 182 | transform: translateZ(25px); 183 | } 184 | .wall:nth-child(2) { 185 | transform: rotateX(180deg) translateZ(25px); 186 | } 187 | .wall:nth-child(3) { 188 | transform: rotateX(90deg) translateZ(25px); 189 | } 190 | .wall:nth-child(4) { 191 | transform: rotateX(-90deg) translateZ(25px); 192 | } 193 | .wall:nth-child(5) { 194 | transform: rotateY(90deg) translateZ(25px); 195 | } 196 | .wall:nth-child(6) { 197 | transform: rotateY(-90deg) translateZ(25px); 198 | } 199 | 200 | 201 | @keyframes light { 202 | 0% { 203 | opacity: 0.2; 204 | } 205 | 50%{ 206 | opacity: 1; 207 | } 208 | 100%{ 209 | opacity: 0.2; 210 | } 211 | } -------------------------------------------------------------------------------- /note.md: -------------------------------------------------------------------------------- 1 | # CSS 3D 之迷宫大作战. 2 | 3 | # 前言 4 | 偶然接触到CSS的3D属性, 就萌生了一种做「**3D游戏**」的想法. 5 | 6 | 了解过css3D属性的同学应该都了解过`perspective`、`perspective-origin`、`transform-style: preserve-3d`这个三个属性值, 7 | 它们构成了CSS的3d世界.
8 | 9 | 同时, 还有`transform`属性来对3D的节点进行平移、缩放、旋转以及拉伸.
10 | 11 | 属性值很简单, 在我们平时的web开发中也很少用到.
12 | 13 | **那用这些CSS3D属性可以做3D游戏吗?** 14 | 15 | 当然是可以的.
16 | 17 | 即使只有沙盒, 也有「我的世界」这种神作.
18 | 19 | 今天我就来带大家玩一个从未有过的全新3D体验.
20 | 21 | 废话不多说, 我们先来看下效果: 22 | 23 | ![图0000]() 24 | 25 | 我们要完成这个「迷宫大作战」,需要完成以下步骤: 26 | 27 | 1. 创建一个3D世界 28 | 2. 写一个3D相机的功能 29 | 3. 创建一座3D迷宫 30 | 4. 创建一个可以自由运动的玩家 31 | 5. 在迷宫中找出一条最短路径提示 32 | 33 | 我们先来看下一些前置知识. 34 | 35 | 36 | # 做一款CSS3D游戏需要的知识和概念 37 | 38 | ## CSS3D坐标系 39 | 在css3D中, 首先要明确一个概念, 「3D」坐标系.
40 | 使用「左手坐标系」, 伸出我们的左手, 大拇指和食指成「L」状, 其他手指与食指垂直, 如图: 41 | [11]() 42 | 43 | 大拇指为X轴, 食指为Y轴, 其他手指为Z轴.
44 | 这个就是CSS3D中的坐标系. 45 | 46 | ## 透视属性 47 | `perspective`为css中的透视属性.
48 | 49 | 这个属性是什么意思呢, 可以把我们的眼睛看作观察点, 眼睛到目标物体的距离就是视距, 也就是这里说的透视属性.
50 | 51 | 大家都知道, 「透视」+「2D」= 「3D」.
52 | 53 | ```css 54 | perspective: 1200px; 55 | -webkit-perspective: 1200px; 56 | ``` 57 | 58 | ## 3D相机 59 | 在3D游戏开发中, 会有相机的概念, 即是人眼所见皆是相机所见.
60 | 在游戏中场景的移动, 大部分都是移动相机.
61 | 例如赛车游戏中, 相机就是跟随车子移动, 所以我们才能看到一路的风景.
62 | 在这里, 我们会使用CSS去实现一个伪3d相机.
63 | 64 | 65 | 66 | ## 变换属性 67 | 在CSS3D中我们对3D盒子做平移、旋转、拉伸、缩放使用`transform`属性. 68 | 69 | * translateX 平移X轴 70 | * translateY 平移Y轴 71 | * translateZ 平移Z轴 72 | * rotateX 旋转X轴 73 | * rotateY 旋转Y轴 74 | * rotateZ 旋转Z轴 75 | * rotate3d(x,y,z,deg) 旋转X、Y、Z轴多少度 76 | 77 | > 注意:
78 | > 这里「先平移再旋转」和「先旋转再平移」是不一样的
79 | > 旋转的角度都是角度值. 80 | 81 | ## 矩阵变换 82 | 我们完成游戏的过程中会用到矩阵变换.
83 | 在js中, 获取某个节点的`transform`属性, 会得到一个矩阵, 这里我打印一下, 他就是长这个样子: 84 | ```js 85 | var _ground = document.getElementsByClassName("ground")[0]; 86 | var bg_style = document.defaultView.getComputedStyle(_ground, null).transform; 87 | console.log("矩阵变换---->>>",bg_style) 88 | ``` 89 | ![图片]() 90 | 91 | **那么我们如何使用矩阵去操作transform呢?** 92 | 在线性变换中, 我们都会去使用矩阵的相乘.
93 | CSS3D中使用4*4的矩阵进行3D变换.
94 | 下面的矩阵我均用二维数组表示.
95 | 例如`matrix3d(1,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,1)`可以用二维数组表示:
96 | ```js 97 | [ 98 | [1, 0, 0, 0], 99 | [0, 1, 0, 0], 100 | [0, 0, 1, 0], 101 | [0, 0, 0, 1] 102 | ] 103 | ``` 104 | 平移即使使用原来状态的矩阵和以下矩阵相乘, dx, dy, dz分别是移动的方向x, y, z.
105 | ```js 106 | [ 107 | [1, 0, 0, dx], 108 | [0, 1, 0, dy], 109 | [0, 0, 1, dz], 110 | [0, 0, 0, 1] 111 | ] 112 | ``` 113 | 绕X轴旋转𝞱, 即是与以下矩阵相乘.
114 | ```js 115 | [ 116 | [1, 0, 0, 0], 117 | [0, cos𝞱, sin𝞱, 0], 118 | [0, -sin𝞱, cos𝞱, 0], 119 | [0, 0, 0, 1] 120 | ] 121 | ``` 122 | 绕Y轴旋转𝞱, 即是与以下矩阵相乘.
123 | ```js 124 | [ 125 | [cos𝞱, 0, -sin𝞱, 0], 126 | [0, 1, 0, 0], 127 | [sin𝞱, 0, cos𝞱, 0], 128 | [0, 0, 0, 1] 129 | ] 130 | ``` 131 | 绕Z轴旋转𝞱, 即是与以下矩阵相乘.
132 | ```js 133 | [ 134 | [cos𝞱, sin𝞱, 0, 0], 135 | [-sin𝞱, cos𝞱, 0, 0], 136 | [0, 0, 1, 0], 137 | [0, 0, 0, 1] 138 | ] 139 | ``` 140 | 具体的矩阵的其他知识这里讲了, 大家有兴趣可以自行下去学习.
141 | 我们这里只需要很简单的旋转应用. 142 | 143 | # 开始创建一个3D世界 144 | 我们先来创建UI界面. 145 | * 相机div 146 | * 地平线div 147 | * 棋盘div 148 | * 玩家div(这里是一个正方体) 149 | 150 | > 注意
151 | > 正方体先旋转在平移, 这种方法应该是最简单的.
152 | > 一个平面绕X轴、Y轴旋转180度、±90度, 都只需要平移Z轴.
153 | > 这里大家试过就明白了.
154 | 155 | 我们先来看下html部分: 156 | ```html 157 |
158 | 159 |
160 |
161 |
162 |
z
163 |
z
164 |
y
165 |
y
166 |
x
167 |
x
168 |
169 |
170 |
171 |
172 | 173 |
174 |
175 |
176 |
177 | ``` 178 | 很简单的布局, 其中`linex`、`liney`、`linez`是我画的坐标轴辅助线.
179 | 红线为X轴, 绿线为Y轴, 蓝线为Z轴. 180 | 接着我们来看下正方体的主要CSS代码. 181 | ```css 182 | ... 183 | .box-con{ 184 | width: 50px; 185 | height: 50px; 186 | transform-style: preserve-3d; 187 | transform-origin: 50% 50%; 188 | transform: translateZ(25px) ; 189 | transition: all 2s cubic-bezier(0.075, 0.82, 0.165, 1); 190 | } 191 | .wall{ 192 | width: 100%; 193 | height: 100%; 194 | border: 1px solid #fdd894; 195 | background-color: #fb7922; 196 | 197 | } 198 | .wall:nth-child(1) { 199 | transform: translateZ(25px); 200 | } 201 | .wall:nth-child(2) { 202 | transform: rotateX(180deg) translateZ(25px); 203 | } 204 | .wall:nth-child(3) { 205 | transform: rotateX(90deg) translateZ(25px); 206 | } 207 | .wall:nth-child(4) { 208 | transform: rotateX(-90deg) translateZ(25px); 209 | } 210 | .wall:nth-child(5) { 211 | transform: rotateY(90deg) translateZ(25px); 212 | } 213 | .wall:nth-child(6) { 214 | transform: rotateY(-90deg) translateZ(25px); 215 | } 216 | ``` 217 | 粘贴一大堆CSS代码显得很蠢.
218 | 其他CSS这里就不粘贴了, 有兴趣的同学可以直接下载「源码」查看. 219 | 界面搭建完成如图所示: 220 | 221 | ![图片]() 222 | 223 | 接下来就是重头戏了, 我们去写js代码来继续完成我们的游戏. 224 | 225 | # 完成一个3D相机功能 226 | 相机在3D开发中必不可少, 使用相机功能不仅能查看3D世界模型, 同时也能实现很多实时的炫酷功能. 227 | 228 | **一个3d相机需要哪些功能?** 229 | 最简单的, 上下左右能够360度无死角观察地图.同时需要拉近拉远视距. 230 | 231 | **通过鼠标交互** 232 | 鼠标左右移动可以旋转查看地图; 233 | 鼠标上下移动可以观察上下地图; 234 | 鼠标滚轮可以拉近拉远视距. 235 | 236 | ✅ 1. 监听鼠标事件 237 | 238 | 首先, 我们需要通过监听鼠标事件来记录鼠标位置, 从而判断相机上下左右查看. 239 | ```js 240 | /** 鼠标上次位置 */ 241 | var lastX = 0, lastY = 0; 242 | /** 控制一次滑动 */ 243 | var isDown = false; 244 | /** 监听鼠标按下 */ 245 | document.addEventListener("mousedown", (e) => { 246 | lastX = e.clientX; 247 | lastY = e.clientY; 248 | isDown = true; 249 | }); 250 | /** 监听鼠标移动 */ 251 | document.addEventListener("mousemove", (e) => { 252 | if (!isDown) return; 253 | let _offsetX = e.clientX - lastX; 254 | let _offsetY = e.clientY - lastY; 255 | lastX = e.clientX; 256 | lastY = e.clientY; 257 | //判断方向 258 | var dirH = 1, dirV = 1; 259 | if (_offsetX < 0) { 260 | dirH = -1; 261 | } 262 | if (_offsetY > 0) { 263 | dirV = -1; 264 | } 265 | }); 266 | document.addEventListener("mouseup", (e) => { 267 | isDown = false; 268 | }); 269 | ``` 270 | 271 | ✅ 2. 判断相机上下左右 272 | 使用`perspective-origin`来设置相机的上下视线.
273 | 使用`transform`来旋转Z轴查看左右方向上的360度.
274 | 275 | ```js 276 | /** 监听鼠标移动 */ 277 | document.addEventListener("mousemove", (e) => { 278 | if (!isDown) return; 279 | let _offsetX = e.clientX - lastX; 280 | let _offsetY = e.clientY - lastY; 281 | lastX = e.clientX; 282 | lastY = e.clientY; 283 | var bg_style = document.defaultView.getComputedStyle(_ground, null).transform; 284 | var camera_style = document.defaultView.getComputedStyle(_camera, null).perspectiveOrigin; 285 | var matrix4 = new Matrix4(); 286 | var _cy = +camera_style.split(' ')[1].split('px')[0]; 287 | var str = bg_style.split("matrix3d(")[1].split(")")[0].split(","); 288 | var oldMartrix4 = str.map((item) => +item); 289 | var dirH = 1, dirV = 1; 290 | if (_offsetX < 0) { 291 | dirH = -1; 292 | } 293 | if (_offsetY > 0) { 294 | dirV = -1; 295 | } 296 | //每次移动旋转角度 297 | var angleZ = 2 * dirH; 298 | var newMartri4 = matrix4.set(Math.cos(angleZ * Math.PI / 180), -Math.sin(angleZ * Math.PI / 180), 0, 0, Math.sin(angleZ * Math.PI / 180), Math.cos(angleZ * Math.PI / 180), 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); 299 | var new_mar = null; 300 | if (Math.abs(_offsetX) > Math.abs(_offsetY)) { 301 | new_mar = matrix4.multiplyMatrices(oldMartrix4, newMartri4); 302 | } else { 303 | _camera.style.perspectiveOrigin = `500px ${_cy + 10 * dirV}px`; 304 | } 305 | new_mar && (_ground.style.transform = `matrix3d(${new_mar.join(',')})`); 306 | }); 307 | ``` 308 | 这里使用了矩阵的方法来旋转Z轴, 矩阵类`Matrix4`是我临时写的一个方法类, 就俩方法, 一个设置二维数组`matrix4.set`, 一个矩阵相乘`matrix4.multiplyMatrices`.
309 | 文末的源码地址中有, 这里就不再赘述了. 310 | 311 | ✅ 3. 监听滚轮拉近拉远距离 312 | 313 | 这里就是根据`perspective`来设置视距. 314 | ```js 315 | //监听滚轮 316 | document.addEventListener('mousewheel', (e) => { 317 | var per = document.defaultView.getComputedStyle(_camera, null).perspective; 318 | let newper = (+per.split("px")[0] + Math.floor(e.deltaY / 10)) + "px"; 319 | _camera.style.perspective = newper 320 | }, false); 321 | ``` 322 | 323 | > 注意:
324 | > perspective-origin属性只有X、Y两个值, 做不到和u3D一样的相机.
325 | > 我这里取巧使用了对地平线的旋转, 从而达到一样的效果.
326 | > 滚轮拉近拉远视距有点别扭, 和3D引擎区别还是很大. 327 | 328 | 完成之后可以看到如下的场景, 已经可以随时观察我们的地图了. 329 | 330 | ![兔兔]() 331 | 332 | 这样子, 一个3D相机就完成, 大家有兴趣的可以自己下去写一下, 还是很有意思的. 333 | 334 | 335 | # 绘制迷宫棋盘 336 | 337 | 绘制格子地图最简单了, 我这里使用一个15*15的数组.
338 | 「0」代表可以通过的路, 「1」代表障碍物. 339 | ```js 340 | var grid = [ 341 | 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 342 | 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 343 | 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 344 | 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 345 | 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 346 | 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 347 | 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 348 | 1, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 349 | 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 350 | 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 351 | 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 352 | 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 353 | 1, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 354 | 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 355 | 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0 356 | ]; 357 | ``` 358 | 然后我们去遍历这个数组, 得到地图.
359 | 写一个方法去创建地图格子, 同时返回格子数组和节点数组.
360 | 这里的`block`是在html中创建的一个预制体, 他是一个正方体.
361 | 然后通过克隆节点的方式添加进棋盘中.
362 | 363 | ```js 364 | /** 棋盘 */ 365 | function pan() { 366 | const con = document.getElementsByClassName("pan")[0]; 367 | const block = document.getElementsByClassName("block")[0]; 368 | let elArr = []; 369 | grid.forEach((item, index) => { 370 | let r = Math.floor(index / 15); 371 | let c = index % 15; 372 | const gezi = document.createElement("div"); 373 | gezi.classList = "pan-item" 374 | // gezi.innerHTML = `${r},${c}` 375 | con.appendChild(gezi); 376 | var newBlock = block.cloneNode(true); 377 | //障碍物 378 | if (item == 1) { 379 | gezi.appendChild(newBlock); 380 | blockArr.push(c + "-" + r); 381 | } 382 | elArr.push(gezi); 383 | }); 384 | const panArr = arrTrans(15, grid); 385 | return { elArr, panArr }; 386 | } 387 | const panData = pan(); 388 | ``` 389 | 可以看到, 我们的界面已经变成了这样. 390 | 391 | ![jhhh]() 392 | 393 | 接下来, 我们需要去控制玩家移动了. 394 | 395 | # 控制玩家移动 396 | 397 | 通过上下左右`w s a d`键来控制玩家移动.
398 | 使用`transform`来移动和旋转玩家盒子. 399 | 400 | ✅ 监听键盘事件 401 | 402 | 通过监听键盘事件`onkeydown`来判断`key`值的上下左右. 403 | ```js 404 | document.onkeydown = function (e) { 405 | /** 移动物体 */ 406 | move(e.key); 407 | } 408 | ``` 409 | 410 | ✅ 进行位移 411 | 412 | 在位移中, 使用`translate`来平移, Z轴始终正对我们的相机, 所以我们只需要移动X轴和Y轴.
413 | 声明一个变量记录当前位置.
414 | 同时需要记录上次变换的`transform`的值, 这里我们就不继续矩阵变换了.
415 | 416 | ```js 417 | /** 当前位置 */ 418 | var position = { x: 0, y: 0 }; 419 | /** 记录上次变换值 */ 420 | var lastTransform = { 421 | translateX: '0px', 422 | translateY: '0px', 423 | translateZ: '25px', 424 | rotateX: '0deg', 425 | rotateY: '0deg', 426 | rotateZ: '0deg' 427 | }; 428 | ``` 429 | 每一个格子都可以看成是二维数组的下标构成, 每次我们移动一个格子的距离. 430 | 431 | ```js 432 | switch (key) { 433 | case 'w': 434 | position.y++; 435 | lastTransform.translateY = position.y * 50 + 'px'; 436 | break; 437 | case 's': 438 | position.y--; 439 | lastTransform.translateY = position.y * 50 + 'px'; 440 | break; 441 | case 'a': 442 | position.x++; 443 | lastTransform.translateX = position.x * 50 + 'px'; 444 | break; 445 | case 'd': 446 | position.x--; 447 | lastTransform.translateX = position.x * 50 + 'px'; 448 | break; 449 | } 450 | //赋值样式 451 | for (let item in lastTransform) { 452 | strTransfrom += item + '(' + lastTransform[item] + ') '; 453 | } 454 | target.style.transform = strTransfrom; 455 | ``` 456 | 到这里, 我们的玩家盒子已经可以移动了. 457 | 458 | > 注意
459 | > 在css3D中的平移可以看成是世界坐标.
460 | > 所以我们只需要关心X、Y轴. 而不需要去移动Z轴. 即使我们进行了旋转. 461 | 462 | ✅ 在移动的过程中进行旋转 463 | 464 | 在CSS3D中, 3D旋转和其他3D引擎中不一样, 一般的诸如u3D、threejs中, 在每次旋转完成之后都会重新校对成世界坐标, 相对来说 465 | 就很好计算绕什么轴旋转多少度.
466 | 467 | 然而, 笔者也低估了CSS3D的旋转.
468 | 我以为上下左右滚动一个正方体很简单. 事实并非如此.
469 | 470 | CSS3D的旋转涉及到四元数和万向锁.
471 | 472 | 比如我们旋转我们的玩家盒子. 473 | 如图所示: 474 | !![十三水、]() 475 | 首先, 第一个格子(0,0)向上绕X轴旋转90度, 就可以到达(1.0); 向左绕Y轴旋转90度, 可以到达(0,1); 476 | 那我们是不是就可以得到规律如下: 477 | 478 | 如图中所示, 单纯的向上下, 向左右绕轴旋转没有问题, 但是要旋转到红色的格子, 两种不同走法, 到红色的格子之后旋转就会出现两种可能. 479 | 从而导致旋转出错. 480 | 481 | 同时这个规律虽然难寻, 但是可以写出来, 最重要的是, **按照这个规律来旋转CSS3D中的盒子, 是不对的** 482 | 483 | 那有人就说了, 这不说的屁话吗? 484 | 485 | 经过笔者实验, 倒是发现了一些规律. 我们继续按照这个规律往下走. 486 | 487 | * 旋转X轴的时候, 同时看当前Z轴的度数, Z轴为90度的奇数倍, 旋转Y轴, 否则旋转X轴. 488 | * 旋转Y轴的时候, 同时看当前Z轴的度数, Z轴为90度的奇数倍, 旋转X轴, 否则旋转Z轴. 489 | * 旋转Z轴的时候, 继续旋转Z轴 490 | 491 | 这样子我们的旋转方向就搞定了. 492 | 493 | ```js 494 | if (nextRotateDir[0] == "X") { 495 | if (Math.floor(Math.abs(lastRotate.lastRotateZ) / 90) % 2 == 1) { 496 | lastTransform[`rotateY`] = (lastRotate[`lastRotateY`] + 90 * dir) + 'deg'; 497 | } else { 498 | lastTransform[`rotateX`] = (lastRotate[`lastRotateX`] - 90 * dir) + 'deg'; 499 | } 500 | } 501 | if (nextRotateDir[0] == "Y") { 502 | if (Math.floor(Math.abs(Math.abs(lastRotate.lastRotateZ)) / 90) % 2 == 1) { 503 | lastTransform[`rotateX`] = (lastRotate[`lastRotateX`] + 90 * dir) + 'deg'; 504 | } else { 505 | lastTransform[`rotateZ`] = (lastRotate[`lastRotateZ`] + 90 * dir) + 'deg'; 506 | } 507 | } 508 | if (nextRotateDir[0] == "Z") { 509 | lastTransform[`rotate${nextRotateDir[0]}`] = (lastRotate[`lastRotate${nextRotateDir[0]}`] - 90 * dir) + 'deg'; 510 | } 511 | ``` 512 | 513 | 然而, 这还没有完, 这种方式的旋转还有个坑, 就是我不知道该旋转90度还是-90度了.
514 | 这里并不是简单的上下左右去加减.
515 | 具体代码可以查看源码. 516 | 517 | 旋转方向对了, 旋转角度不知该如何计算了. 518 | 519 | ⚠️⚠️⚠️ 同时这里会伴随着「万向锁」的出现, 即是Z轴与X轴重合了. 哈哈哈哈~~~
520 | ⚠️⚠️⚠️ 这里笔者还没有解决, 也希望万能的网友能够出言帮忙~
521 | ⚠️⚠️⚠️ 笔者后续解决了会更新的. 哈哈哈哈, 大坑.
522 | 523 | 好了, 这里问题不影响我们的项目. 524 | 我们继续讲如何找到最短路径并给出提示. 525 | 526 | # 最短路径的计算 527 | 528 | 在迷宫中, 从一个点到另一个点的最短路径怎么计算呢? 529 | 这里笔者使用的是广度优先遍历(BFS)算法来计算最短路径. 530 | 531 | 我们来思考: 532 | 1. 二维数组中找最短路径 533 | 2. 每一格的最短路径只有上下左右相邻的四格 534 | 3. 那么只要递归寻找每一格的最短距离直至找到终点 535 | 536 | 这里我们需要使用「队列」先进先出的特点. 537 | 538 | 我们先来看一张图: 539 | 540 | [hhh]() 541 | 542 | 很清晰的可以得到最短路径. 543 | 544 | > 注意
545 | > 使用两个长度为4的数组表示上下左右相邻的格子需要相加的下标偏移量.
546 | > 每次入队之前需要判断是否已经入队了.
547 | > 每次出队时需要判断是否是终点.
548 | > 需要记录当前入队的目标的父节点, 方便获取到最短路径.
549 | 550 | 我们来看下代码: 551 | ```js 552 | //春初路径 553 | var stack = []; 554 | /** 555 | * BFS 实现寻路 556 | * @param {*} grid 557 | * @param {*} start {x: 0,y: 0} 558 | * @param {*} end {x: 3,y: 3} 559 | */ 560 | function getShortPath(grid, start, end, a) { 561 | let maxL_x = grid.length; 562 | let maxL_y = grid[0].length; 563 | let queue = new Queue(); 564 | //最短步数 565 | let step = 0; 566 | //上左下右 567 | let dx = [1, 0, -1, 0]; 568 | let dy = [0, 1, 0, -1]; 569 | //加入第一个元素 570 | queue.enqueue(start); 571 | //存储一个一样的用来排查是否遍历过 572 | let mem = new Array(maxL_x); 573 | for (let n = 0; n < maxL_x; n++) { 574 | mem[n] = new Array(maxL_y); 575 | mem[n].fill(100); 576 | } 577 | while (!queue.isEmpty()) { 578 | let p = []; 579 | for (let i = queue.size(); i > 0; i--) { 580 | let preTraget = queue.dequeue(); 581 | p.push(preTraget); 582 | //找到目标 583 | if (preTraget.x == end.x && preTraget.y == end.y) { 584 | stack.push(p); 585 | return step; 586 | } 587 | //遍历四个相邻格子 588 | for (let j = 0; j < 4; j++) { 589 | let nextX = preTraget.x + dx[j]; 590 | let nextY = preTraget.y + dy[j]; 591 | 592 | if (nextX < maxL_x && nextX >= 0 && nextY < maxL_y && nextY >= 0) { 593 | let nextTraget = { x: nextX, y: nextY }; 594 | if (grid[nextX][nextY] == a && a < mem[nextX][nextY]) { 595 | queue.enqueue({ ...nextTraget, f: { x: preTraget.x, y: preTraget.y } }); 596 | mem[nextX][nextY] = a; 597 | } 598 | } 599 | } 600 | } 601 | stack.push(p); 602 | step++; 603 | } 604 | } 605 | /* 找出一条最短路径**/ 606 | function recall(end) { 607 | let path = []; 608 | let front = { x: end.x, y: end.y }; 609 | while (stack.length) { 610 | let item = stack.pop(); 611 | for (let i = 0; i < item.length; i++) { 612 | if (!item[i].f) break; 613 | if (item[i].x == front.x && item[i].y == front.y) { 614 | path.push({ x: item[i].x, y: item[i].y }); 615 | front.x = item[i].f.x; 616 | front.y = item[i].f.y; 617 | break; 618 | } 619 | } 620 | } 621 | return path; 622 | } 623 | 624 | ``` 625 | 这样子我们就可以找到一条最短路径并得到最短的步数.
626 | 然后我们继续去遍历我们的原数组(即棋盘原数组).
627 | 点击提示点亮路径.
628 | 629 | ```js 630 | var step = getShortPath(panArr, { x: 0, y: 0 }, { x: 14, y: 14 }, 0); 631 | console.log("最短距离----", step); 632 | _perstep.innerHTML = `请在${step}步内走到终点`; 633 | var path = recall({ x: 14, y: 14 }); 634 | console.log("路径---", path); 635 | /** 提示 */ 636 | var tipCount = 0; 637 | _tip.addEventListener("click", () => { 638 | console.log("9999", tipCount) 639 | elArr.forEach((item, index) => { 640 | let r = Math.floor(index / 15); 641 | let c = index % 15; 642 | path.forEach((_item, i) => { 643 | if (_item.x == r && _item.y == c) { 644 | // console.log("ooo",_item) 645 | if (tipCount % 2 == 0) 646 | item.classList = "pan-item pan-path"; 647 | else 648 | item.classList = "pan-item"; 649 | } 650 | }) 651 | }); 652 | tipCount++; 653 | }); 654 | ``` 655 | 这样子, 我们可以得到如图的提示: 656 | 657 | 658 | 大功告成. 659 | 660 | # 尾声 661 | 当然, 我这里的这个小游戏还有可以完善的地方 662 | 比如: 663 | * 可以增加道具, 拾取可以减少已走步数 664 | * 可以增加配置关卡 665 | * 还可以增加跳跃功能 666 | * ... 667 | 原来如此, CSS3D能做的事还有很多, 怎么用全看自己的想象力有多丰富了. 668 | 669 | 哈哈哈, 真想用CSS3D写一个「我的世界」玩玩, 性能问题恐怕会有点大. 670 | 671 | 本文例子均在PC端体验较好. 672 | 673 | 674 | 675 | 676 | 677 | 678 | 679 | 680 | 681 | 682 | 683 | 684 | 685 | 686 | 687 | 688 | 689 | 690 | 691 | 692 | 693 | 694 | 695 | 696 | 697 | 698 | 699 | 700 | 701 | 702 | 703 | 704 | 705 | 706 | 707 | 708 | 709 | 710 | 711 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | (function () { 3 | var _ground = document.getElementsByClassName("ground")[0]; 4 | var _camera = document.getElementsByClassName("camera")[0]; 5 | var _step = document.getElementsByClassName("step")[0]; 6 | var _perstep = document.getElementsByClassName("per-step")[0]; 7 | var _tip = document.getElementsByClassName("tip")[0]; 8 | 9 | /** 鼠标上次位置 */ 10 | var lastX = 0, lastY = 0; 11 | /** 步数 */ 12 | var stepCount = 0; 13 | /** 控制一次滑动 */ 14 | var isDown = false; 15 | /** 当前位置 */ 16 | var position = { x: 0, y: 0 }; 17 | /** 上次变换值 */ 18 | var lastTransform = { 19 | translateX: '0px', 20 | translateY: '0px', 21 | translateZ: '25px', 22 | rotateX: '0deg', 23 | rotateY: '0deg', 24 | rotateZ: '0deg' 25 | }; 26 | /** 下一步转向 */ 27 | var nextRotateDir = ["X", "Y"]; 28 | /** 所有的转向 */ 29 | var allRotateDir = ["X", "Y", "Z"]; 30 | /** 障碍物 */ 31 | var blockArr = []; 32 | /** 全部路径 */ 33 | var stack = []; 34 | 35 | /** 监听鼠标按下 */ 36 | document.addEventListener("mousedown", (e) => { 37 | lastX = e.clientX; 38 | lastY = e.clientY; 39 | isDown = true; 40 | }); 41 | 42 | /** 监听鼠标移动 */ 43 | document.addEventListener("mousemove", (e) => { 44 | if (!isDown) return; 45 | let _offsetX = e.clientX - lastX; 46 | let _offsetY = e.clientY - lastY; 47 | lastX = e.clientX; 48 | lastY = e.clientY; 49 | // _ground.style.transform = "translateX(-50%) translateY(-50%) rotateX(117deg) rotateZ(160deg)"; 50 | var matrix4 = new Matrix4(); 51 | var bg_style = document.defaultView.getComputedStyle(_ground, null).transform; 52 | var camera_style = document.defaultView.getComputedStyle(_camera, null).perspectiveOrigin; 53 | var _cy = +camera_style.split(' ')[1].split('px')[0]; 54 | console.log("矩阵变换---->>>",bg_style); 55 | 56 | var str = bg_style.split("matrix3d(")[1].split(")")[0].split(","); 57 | 58 | var oldMartrix4 = str.map((item) => +item); 59 | 60 | // console.log("old", oldMartrix4); 61 | var dirH = 1, dirV = 1; 62 | if (_offsetX < 0) { 63 | dirH = -1; 64 | } 65 | 66 | if (_offsetY > 0) { 67 | dirV = -1; 68 | } 69 | var angleZ = 2 * dirH; 70 | 71 | var newMartri4 = matrix4.set(Math.cos(angleZ * Math.PI / 180), -Math.sin(angleZ * Math.PI / 180), 0, 0, Math.sin(angleZ * Math.PI / 180), Math.cos(angleZ * Math.PI / 180), 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); 72 | // var newMartri4_1 = matrix4.set(1, 0, 0, 0, 0, Math.cos(angleX * Math.PI / 180), Math.sin(angleX * Math.PI / 180), 0, 0, -Math.sin(angleX * Math.PI / 180), Math.cos(angleX * Math.PI / 180), 0, 0, 0, 0, 1); 73 | // var newMartri4_1 = matrix4.set(Math.cos(angleX * Math.PI / 180), 0, -Math.sin(angleX * Math.PI / 180), 0, 0, 1, 0, 0, Math.sin(angleX * Math.PI / 180), 0, Math.cos(angleX * Math.PI / 180), 0, 0, 0, 0, 1); 74 | // console.log("new", newMartri4) 75 | 76 | var new_mar = null; 77 | if (Math.abs(_offsetX) > Math.abs(_offsetY)) { 78 | new_mar = matrix4.multiplyMatrices(oldMartrix4, newMartri4); 79 | } else { 80 | _camera.style.perspectiveOrigin = `500px ${_cy + 10 * dirV}px`; 81 | } 82 | // console.log(new_mar); 83 | // console.log(`matrix3d(${new_mar.join(',')});`); 84 | new_mar && (_ground.style.transform = `matrix3d(${new_mar.join(',')})`); 85 | }); 86 | document.addEventListener("mouseup", (e) => { 87 | isDown = false; 88 | }); 89 | 90 | //监听键盘 91 | document.onkeydown = function (e) { 92 | /** 移动物体 */ 93 | move(e.key); 94 | } 95 | 96 | //监听滚轮 97 | // document.addEventListener('mousewheel', (e) => { 98 | // var per = document.defaultView.getComputedStyle(_camera, null).perspective; 99 | // let newper = (+per.split("px")[0] + Math.floor(e.deltaY / 10)) + "px"; 100 | // _camera.style.perspective = newper 101 | // }, false); 102 | 103 | /** 移动盒子 */ 104 | function move(key) { 105 | if (isBlock(key)) return; 106 | const v = getNoneV2Arr(nextRotateDir, allRotateDir); 107 | if (key == "w" || key == "s") { 108 | nextRotateDir[1] = v; 109 | } else { 110 | nextRotateDir[0] = v; 111 | } 112 | 113 | var target = document.getElementsByClassName("box-con")[0]; 114 | // var target_style = document.defaultView.getComputedStyle(target, null).transform; 115 | // var str = target_style.split("matrix3d(")[1].split(")")[0].split(","); 116 | 117 | let lastRotateX = +lastTransform.rotateX.split('deg')[0]; 118 | let lastRotateY = +lastTransform.rotateY.split('deg')[0]; 119 | let lastRotateZ = +lastTransform.rotateZ.split('deg')[0]; 120 | let lastTranslateX = +lastTransform.translateX.split('px')[0]; 121 | let lastTranslateY = +lastTransform.translateY.split('px')[0]; 122 | let lastTranslateZ = +lastTransform.translateZ.split('px')[0]; 123 | 124 | let lastRotate = { 125 | lastRotateX, lastRotateY, lastRotateZ, lastTranslateX, lastTranslateY, lastTranslateZ 126 | } 127 | 128 | let dir = 1; 129 | console.log("8888-----", nextRotateDir) 130 | let strTransfrom = "" 131 | switch (key) { 132 | case 'w': 133 | position.y++; 134 | lastTransform.translateY = position.y * 50 + 'px'; 135 | if (nextRotateDir[0] == "X") { 136 | if (Math.floor(Math.abs(lastRotate.lastRotateZ) / 90) % 2 == 1) { 137 | dir = calRotateDir(lastRotateX, lastRotateZ); 138 | lastTransform[`rotateY`] = (lastRotate[`lastRotateY`] + 90 * dir) + 'deg'; 139 | } else { 140 | // dir = calRotateDir(lastRotateY, lastRotateZ); 141 | console.log("dir---88", dir) 142 | lastTransform[`rotateX`] = (lastRotate[`lastRotateX`] - 90 * dir) + 'deg'; 143 | } 144 | } 145 | 146 | if (nextRotateDir[0] == "Y") { 147 | if (Math.floor(Math.abs(Math.abs(lastRotate.lastRotateZ)) / 90) % 2 == 1) { 148 | dir = calRotateDir(lastRotateY, lastRotateZ); 149 | lastTransform[`rotateX`] = (lastRotate[`lastRotateX`] + 90 * dir) + 'deg'; 150 | } else { 151 | dir = calRotateDir(lastRotateX, lastRotateY); 152 | lastTransform[`rotateZ`] = (lastRotate[`lastRotateZ`] + 90 * dir) + 'deg'; 153 | } 154 | } 155 | 156 | if (nextRotateDir[0] == "Z") { 157 | dir = calRotateDir(lastRotateX, lastRotateY); 158 | lastTransform[`rotate${nextRotateDir[0]}`] = (lastRotate[`lastRotate${nextRotateDir[0]}`] - 90 * dir) + 'deg'; 159 | } 160 | break; 161 | case 's': 162 | position.y--; 163 | lastTransform.translateY = position.y * 50 + 'px'; 164 | if (nextRotateDir[0] == "X") { 165 | if (Math.floor(Math.abs(lastRotate.lastRotateZ) / 90) % 2 == 1) { 166 | dir = calRotateDir(lastRotateX, lastRotateZ); 167 | lastTransform[`rotateY`] = (lastRotate[`lastRotateY`] - 90 * dir) + 'deg'; 168 | } else { 169 | // dir = calRotateDir(lastRotateZ, lastRotateY); 170 | lastTransform[`rotateX`] = (lastRotate[`lastRotateX`] + 90 * dir) + 'deg'; 171 | } 172 | } 173 | 174 | if (nextRotateDir[0] == "Y") { 175 | if (Math.floor(Math.abs(lastRotate.lastRotateZ) / 90) % 2 == 1) { 176 | dir = calRotateDir(lastRotateZ, lastRotateY); 177 | lastTransform[`rotateX`] = (lastRotate[`lastRotateX`] - 90 * dir) + 'deg'; 178 | } else { 179 | // dir = calRotateDir(lastRotateX, lastRotateY); 180 | lastTransform[`rotateZ`] = (lastRotate[`lastRotateZ`] - 90 * dir) + 'deg'; 181 | } 182 | } 183 | 184 | if (nextRotateDir[0] == "Z") { 185 | dir = calRotateDir(lastRotateX, lastRotateY); 186 | lastTransform[`rotate${nextRotateDir[0]}`] = (lastRotate[`lastRotate${nextRotateDir[0]}`] + 90 * dir) + 'deg'; 187 | } 188 | // target.style.transform = `translateX(${position.x * 50}px) translateY(${position.y * 50}px) translateZ(25px) rotateX(${position.y * -90}deg)`; 189 | break; 190 | case 'a': 191 | position.x++; 192 | lastTransform.translateX = position.x * 50 + 'px'; 193 | 194 | if (nextRotateDir[1] == "X") { 195 | if (Math.floor(Math.abs(lastRotate.lastRotateZ) / 90) % 2 == 1) { 196 | dir = calRotateDir(lastRotateX, lastRotateZ); 197 | lastTransform[`rotateY`] = (lastRotate[`lastRotateY`] + 90 * dir) + 'deg'; 198 | } else { 199 | // dir = calRotateDir(lastRotateZ, lastRotateY); 200 | lastTransform[`rotateX`] = (lastRotate[`lastRotateX`] - 90 * dir) + 'deg'; 201 | } 202 | } 203 | 204 | if (nextRotateDir[1] == "Y") { 205 | if (Math.floor(Math.abs(lastRotate.lastRotateZ) / 90) % 2 == 1) { 206 | dir = calRotateDir(lastRotateZ, lastRotateY); 207 | lastTransform[`rotateX`] = (lastRotate[`lastRotateX`] - 90 * dir) + 'deg'; 208 | } else { 209 | // dir = calRotateDir(lastRotateX, lastRotateZ); 210 | console.log("dir---", dir) 211 | lastTransform[`rotateY`] = (lastRotate[`lastRotateY`] + dir * 90) + 'deg'; 212 | } 213 | } 214 | 215 | if (nextRotateDir[1] == "Z") { 216 | dir = calRotateDir(lastRotateX, lastRotateY); 217 | console.log("dir====", dir) 218 | lastTransform[`rotate${nextRotateDir[1]}`] = (lastRotate[`lastRotate${nextRotateDir[1]}`] - 90 * dir) + 'deg'; 219 | } 220 | break; 221 | case 'd': 222 | position.x--; 223 | lastTransform.translateX = position.x * 50 + 'px'; 224 | 225 | if (nextRotateDir[1] == "X") { 226 | if (Math.floor(Math.abs(lastRotate.lastRotateZ) / 90) % 2 == 1) { 227 | dir = calRotateDir(lastRotateX, lastRotateZ); 228 | lastTransform[`rotateY`] = (lastRotate[`lastRotateY`] + 90 * dir) + 'deg'; 229 | } else { 230 | // dir = calRotateDir(lastRotateZ, lastRotateY); 231 | lastTransform[`rotateX`] = (lastRotate[`lastRotateX`] - 90 * dir) + 'deg'; 232 | } 233 | } 234 | 235 | if (nextRotateDir[1] == "Y") { 236 | if (Math.floor(Math.abs(lastRotate.lastRotateZ) / 90) % 2 == 1) { 237 | dir = calRotateDir(lastRotateZ, lastRotateY); 238 | lastTransform[`rotateX`] = (lastRotate[`lastRotateX`] + 90 * dir) + 'deg'; 239 | } else { 240 | // dir = calRotateDir(lastRotateX, lastRotateZ); 241 | lastTransform[`rotateY`] = (lastRotate[`lastRotateY`] - 90 * dir) + 'deg'; 242 | } 243 | } 244 | 245 | if (nextRotateDir[1] == "Z") { 246 | dir = calRotateDir(lastRotateX, lastRotateY); 247 | lastTransform[`rotate${nextRotateDir[1]}`] = (lastRotate[`lastRotate${nextRotateDir[1]}`] + 90 * dir) + 'deg'; 248 | } 249 | 250 | // target.style.transform = `translateX(${position.x * 50}px) translateY(${position.y * 50}px) translateZ(25px) rotateY(${position.x * 90}deg)`; 251 | break; 252 | } 253 | //赋值样式 254 | for (let item in lastTransform) { 255 | strTransfrom += item + '(' + lastTransform[item] + ') '; 256 | } 257 | console.log("str--", strTransfrom); 258 | target.style.transform = strTransfrom; 259 | 260 | stepCount++; 261 | _step.innerHTML = `${stepCount}步`; 262 | } 263 | 264 | 265 | /** 棋盘 */ 266 | function pan() { 267 | var grid = [ 268 | 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 269 | 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 270 | 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 271 | 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 272 | 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 273 | 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 274 | 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 275 | 1, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 276 | 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 277 | 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 278 | 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 279 | 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 280 | 1, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 281 | 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 282 | 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0 283 | ]; 284 | console.log(grid.length); 285 | 286 | const con = document.getElementsByClassName("pan")[0]; 287 | const block = document.getElementsByClassName("block")[0]; 288 | let elArr = []; 289 | grid.forEach((item, index) => { 290 | let r = Math.floor(index / 15); 291 | let c = index % 15; 292 | const gezi = document.createElement("div"); 293 | gezi.classList = "pan-item" 294 | // gezi.innerHTML = `${r},${c}` 295 | con.appendChild(gezi); 296 | var newBlock = block.cloneNode(true); 297 | // console.log(panArr[r][c]) 298 | 299 | if (item == 1) { 300 | gezi.appendChild(newBlock); 301 | blockArr.push(c + "-" + r); 302 | } 303 | elArr.push(gezi); 304 | }); 305 | const panArr = arrTrans(15, grid); 306 | return { elArr, panArr }; 307 | } 308 | const panData = pan(); 309 | const { elArr, panArr } = panData; 310 | // console.log(elArr) 311 | 312 | var step = getShortPath(panArr, { x: 0, y: 0 }, { x: 14, y: 14 }, 0); 313 | console.log("最短距离----", step); 314 | _perstep.innerHTML = `请在${step}步内走到终点`; 315 | var path = recall({ x: 14, y: 14 }); 316 | console.log("路径---", path); 317 | 318 | /** 提示 */ 319 | var tipCount = 0; 320 | _tip.addEventListener("click", () => { 321 | console.log("9999", tipCount) 322 | elArr.forEach((item, index) => { 323 | let r = Math.floor(index / 15); 324 | let c = index % 15; 325 | path.forEach((_item, i) => { 326 | if (_item.x == r && _item.y == c) { 327 | // console.log("ooo",_item) 328 | if (tipCount % 2 == 0) 329 | item.classList = "pan-item pan-path"; 330 | else 331 | item.classList = "pan-item"; 332 | 333 | } 334 | }) 335 | }); 336 | tipCount++; 337 | }) 338 | 339 | 340 | /** 341 | * BFS 实现寻路 342 | * @param {*} grid 343 | * @param {*} start {x: 0,y: 0} 344 | * @param {*} end {x: 3,y: 3} 345 | */ 346 | function getShortPath(grid, start, end, a) { 347 | let maxL_x = grid.length; 348 | let maxL_y = grid[0].length; 349 | let queue = new Queue(); 350 | let step = 0; 351 | //上左下右 352 | let dx = [1, 0, -1, 0]; 353 | let dy = [0, 1, 0, -1]; 354 | //加入第一个元素 355 | queue.enqueue(start); 356 | 357 | //存储一个一样的用来排查是否遍历过 358 | let mem = new Array(maxL_x); 359 | for (let n = 0; n < maxL_x; n++) { 360 | mem[n] = new Array(maxL_y); 361 | mem[n].fill(10000); 362 | } 363 | 364 | while (!queue.isEmpty()) { 365 | // console.log(queue) 366 | // stack.push(queue); 367 | let p = []; 368 | for (let i = queue.size(); i > 0; i--) { 369 | // console.log(0) 370 | let preTraget = queue.dequeue(); 371 | p.push(preTraget); 372 | //找到目标 373 | 374 | if (preTraget.x == end.x && preTraget.y == end.y) { 375 | stack.push(p); 376 | return step; 377 | } 378 | 379 | for (let j = 0; j < 4; j++) { 380 | let nextX = preTraget.x + dx[j]; 381 | let nextY = preTraget.y + dy[j]; 382 | 383 | if (nextX < maxL_x && nextX >= 0 && nextY < maxL_y && nextY >= 0) { 384 | let nextTraget = { x: nextX, y: nextY }; 385 | // console.log("坐标", nextX, nextY, grid[1][0]) 386 | if (grid[nextX][nextY] == a && a < mem[nextX][nextY]) { 387 | queue.enqueue({ ...nextTraget, f: { x: preTraget.x, y: preTraget.y } }); 388 | mem[nextX][nextY] = a; 389 | } 390 | } 391 | } 392 | } 393 | stack.push(p); 394 | step++; 395 | } 396 | } 397 | 398 | /** 寻找到路径 */ 399 | function recall(end) { 400 | let path = []; 401 | let front = { x: end.x, y: end.y }; 402 | while (stack.length) { 403 | let item = stack.pop(); 404 | for (let i = 0; i < item.length; i++) { 405 | if (!item[i].f) break; 406 | if (item[i].x == front.x && item[i].y == front.y) { 407 | path.push({ x: item[i].x, y: item[i].y }); 408 | front.x = item[i].f.x; 409 | front.y = item[i].f.y; 410 | break; 411 | } 412 | } 413 | } 414 | return path; 415 | } 416 | 417 | 418 | /** 是否是障碍 */ 419 | function isBlock(dir) { 420 | let str; 421 | if (dir == "w") { 422 | if (position.y + 1 > 14) { 423 | return true; 424 | } 425 | str = position.x + "-" + (position.y + 1); 426 | } 427 | 428 | if (dir == "s") { 429 | if (position.y - 1 < 0) { 430 | return true; 431 | } 432 | str = position.x + "-" + (position.y - 1); 433 | } 434 | 435 | if (dir == "a") { 436 | if (position.x + 1 > 14) { 437 | return true; 438 | } 439 | str = (position.x + 1) + "-" + position.y; 440 | } 441 | 442 | if (dir == "d") { 443 | if (position.x - 1 < 0) { 444 | return true; 445 | } 446 | str = (position.x - 1) + "-" + position.y; 447 | } 448 | if (blockArr.indexOf(str) > -1) { 449 | return true; 450 | } 451 | return false; 452 | } 453 | })(); 454 | 455 | 456 | //确认旋转轴 457 | function calRotateDir(angle1, angle2) { 458 | console.log("两个角度----", angle1, angle2) 459 | if (angle2 + angle1 >= 0) return -1; 460 | if (angle2 + angle1 < 0) return 1; 461 | if (angle1 < 0 || angle2 < 0) return -1; 462 | let a = Math.floor(angle1 / 180); 463 | let b = Math.floor(angle2 / 180); 464 | console.log(a, b) 465 | return Math.abs(a + b) % 2 == 0 ? 1 : -1; 466 | } 467 | 468 | //获取数组中没有的v 469 | function getNoneV2Arr(arr1, arr2) { 470 | var v = ""; 471 | for (let i = 0; i < arr2.length; i++) { 472 | if (arr1.indexOf(arr2[i]) == -1) { 473 | v = arr2[i]; 474 | break; 475 | } 476 | } 477 | return v; 478 | } 479 | 480 | /** 对列 */ 481 | function Queue() { 482 | this.items = []; 483 | //1. 将元素加到元素中 484 | Queue.prototype.enqueue = function (el) { 485 | this.items.push(el); 486 | } 487 | //2.从队列中删除元素 488 | Queue.prototype.dequeue = function () { 489 | return this.items.shift(); 490 | } 491 | //3.查看元素 492 | Queue.prototype.front = function () { 493 | return this.items[0]; 494 | } 495 | //4.是否为空 496 | Queue.prototype.isEmpty = function () { 497 | return this.items.length == 0; 498 | } 499 | //5.长度 500 | Queue.prototype.size = function () { 501 | return this.items.length; 502 | } 503 | } 504 | 505 | /** 一维数组转二维数组 */ 506 | function arrTrans(num, arr) { 507 | const newArr = []; 508 | while (arr.length > 0) { 509 | newArr.push(arr.splice(0, num)); 510 | } 511 | return newArr; 512 | } 513 | --------------------------------------------------------------------------------