├── 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 |
--------------------------------------------------------------------------------