【自己给自己题目做】:如何在Canvas上实现魔方效果

  最终demo -> 3d魔方 

  体验方法:

  • 浮动鼠标找到合适的位置,按空格键暂停
  • 选择要翻转的3*3模块,找到相邻两个正方体,鼠标点击第一个正方体,并且一直保持鼠标按下的状态直到移到第二个正方体后放开,比如下图:

(鼠标点击1处,然后一直移动到2处松开,中间一行的3*3模块绕图示方向发生转动)

  • 按空格键,魔方恢复转动,继续寻找下一个要翻动的目标

  示意图如下(请尽量使用chrome):

  

 

正方体绘制回顾

  Canvas之蛋疼的正方体绘制体验 说到了如何用canvas在画布上绘制三维效果的正方体,并且最终给出了一个多正方体的demo -> 多正方体

  具体的过程可以参照前文,这里简要的再做个概括。

  代码定义了四个对象,分别是garden(场景)、cube(正方体)、face(面)、ball(点),从属关系如下:

  而魔方demo中,一个场景有27个正方体,每个正方体有6个面和8个点,每个面有4个点;每帧的渲染中先根据cube的体心排序(前文中说了这不是最佳方案),然后根据排序后的结果绘制每个cube的可见面。归根结底,每帧的渲染就是对每个正方体8个点的渲染!

  有了这部分经验,绘制一个无交互的魔方demo就可以手到擒来了 -> 无交互魔方

  无交互魔方demo和前面的多正方体demo最大的区别就是面的颜色,其实很简单,在初始化的时候可以传入一个color数组,比如这样:

// 红  橙  蓝  绿  黄  白
//  0  1   2   3   4   5
window.colors = ['#ff0000', '#ff6600', '#0000ff', '#00ff00', '#ffff00', '#ffffff'];
var color = [// 第一排[0, 5, 5, 3, 5, 5],[0, 5, 5, 5, 5, 5],[0, 2, 5, 5, 5, 5],[0, 5, 5, 3, 5, 5],[0, 5, 5, 5, 5, 5],[0, 2, 5, 5, 5, 5],[0, 5, 5, 3, 5, 4],[0, 5, 5, 5, 5, 4],[0, 2, 5, 5, 5, 4],// 第二排[5, 5, 5, 3, 5, 5],[5, 5, 5, 5, 5, 5],[5, 2, 5, 5, 5, 5],[5, 5, 5, 3, 5, 5],[5, 5, 5, 5, 5, 5],[5, 2, 5, 5, 5, 5],[5, 5, 5, 3, 5, 4],[5, 5, 5, 5, 5, 4],[5, 2, 5, 5, 5, 4],// 第三排[5, 5, 1, 3, 5, 5],[5, 5, 1, 5, 5, 5],[5, 2, 1, 5, 5, 5],[5, 5, 1, 3, 5, 5],[5, 5, 1, 5, 5, 5],[5, 2, 1, 5, 5, 5],[5, 5, 1, 3, 5, 4],[5, 5, 1, 5, 5, 4],[5, 2, 1, 5, 5, 4],
];

  初始化每个cube时多传入一个参数,这样就能实现你要的颜色了。

 

问题的关键

  如何交互,如何实现玩家想要的3*3模块的旋转才是问题的关键。

  我最终想到的是像demo一样选择两个相邻的正方体,然后一个监听mousedown事件,另一个监听mouseup事件,表面看上去,两个有顺序的正方体似乎能确定了那个想要旋转的3*3模块了(其实不然)。而在寻找3*3模块之前,我们首先要解决的是如何确定这两个正方体。

 

  • 两个正方体的确定

  因为我们在画布上展现出来的图案其实都是h5的原生api绘上去的,并不像dom一样能写个事件监听。如何得到这两个正方体,思来想去我觉得唯一方法就是点的判断。

  遍历27个正方体在二维空间的6*27个面,判断鼠标点击是否在面内。这里可以把场景内的cubes倒排,因为cubes在每帧中都要根据体心重新排序,越后面的越先绘制,而鼠标点击的cubes按多数情况下应该是离视点近的,所以可以从后到前遍历,这样可以加快寻找速度;而遍历一个正方体6个面时,不可见面也不用判断。这个问题的最后就是二维系上一个点在一个凸四边形内的判断。具体可以参考 -> 判断一个点是否在给定的凸四边形内

  我用了博文的第一种方法。

  由于数学能力的欠缺,一开始我把叉积当做点积了,debug了良久才发现。

  鼠标监听:

document.addEventListener('mousedown', function(event){window.rotateArray = [];var obj = canvas.getBoundingClientRect();// 鼠标点击的地方在canvas上的(x,y)坐标var x = event.clientX - obj.left;var y = event.clientY - obj.top;var v = new Vector2(x, y)var ans = getCubeIndex(v);if(ans)window.rotateArray.push(ans);
});

  getCubeIndex函数就是遍历27个cube和每个cube中6个面的一个两层循环。

  点在凸四边形的判断:

// 判断点m是否在顺时针方向的a,b,c,d四个点组成的凸四边形内
function isPointIn(a, b, c, d, m) {var f = b.minus(a).dot(m.minus(a));if(f <= 0) return false;var f = c.minus(b).dot(m.minus(b));if(f <= 0) return false;var f = d.minus(c).dot(m.minus(c));if(f <= 0) return false;var f = a.minus(d).dot(m.minus(d));if(f <= 0) return false;return true;
}

  至此,2个被点击的正方体在27个cube中的位置已经找出。

 

  • 3*3模块的确定

  接着需要寻找由两个正方体确定的3*3模块。

  我们知道,玩魔方每次旋转的肯定是个3*3的模块,而这样的模块在一个魔方中有3*3=9个。而2个相邻的正方体能不能确定唯一的3*3模块?答案是不能的,如下图:

  上图1和2两个正方体确定了图示的两个3*3模块。其实如果两个正方体的位置是在魔方的棱上,那么就能确定两个。我们暂时不管它,一个也好,两个也罢,先把它找出来。

  怎么找?最开始我想到的是维护一个三维数组,初始化给每个cube一个index值,值和三维数组值相对应,每次魔方旋转时同时改变三维数组的值,这样找到这个3*3的模块就是遍历三维数组的三个维度,找到任一维度的3*3=9个正方体中如果有包含点击得到的两个正方体,则为一组解。后来被我放弃了,三维数组的维护实在是太麻烦了。

  最后我用深度搜索来解,寻找一条长度为8的闭合回路。已经确定了前两个值,因为这条闭合回路不会经过魔方最中心的那个正方体,所以每个点的下一个点的取值最多只有4种情况,最大复杂度也就O(4^6),完全在可控范围之内。而且搜过的点标记掉不用继续搜索,答案几乎秒出。

  深度搜索如下:

function dfs(index) {var cubes = garden.cubes;if(index === 8) {var dis = cubes[window.rotateArray[0]].pos3.getDistance(cubes[window.rotateArray[7]].pos3);if(Math.abs(dis - 60) > 10) return;// 判断8个点在一个平面var cubes = garden.cubes;var a = cubes[window.rotateArray[1]].pos3.minus(cubes[window.rotateArray[0]].pos3);var b = cubes[window.rotateArray[7]].pos3.minus(cubes[window.rotateArray[6]].pos3);// 找一个面的法向量var v = undefined;for(var i = 0; i < 27; i++) {var c = cubes[i].pos3;if(a.isPerpTo(c) && b.isPerpTo(c)) {v = c;break;}if(i === 26 && v === undefined) return;}// 判断任意相邻向量是否垂直法向量for(var i = 0; i < 7; i++) {var a = cubes[window.rotateArray[i]].pos3.minus(cubes[window.rotateArray[i + 1]].pos3);if(!a.isPerpTo(v)) return;}// 如果是最前面的面,returnvar zz = 0;for(var i = 0; i < 8; i++) zz += cubes[window.rotateArray[i]].pos3.z;zz /= 8;if(zz < -40) return;// 如果是俄罗斯方块那种类型var vv = new Vector3();for(var i = 0; i < 8; i+=2) {vv.x += cubes[window.rotateArray[i]].pos3.x;vv.y += cubes[window.rotateArray[i]].pos3.y;vv.z += cubes[window.rotateArray[i]].pos3.z;}vv.x /= 4; vv.y /= 4;vv.z /= 4;var flag = false;for(var i = 0; i < 27; i++) {var vvv = cubes[i].pos3if(vv.getDistance(vvv) > 5) continue;flag = true;break;}if(!flag) return;for(var i = 0; i < 8; i++) {window.isFindRoute = true;window.rotateFinalArray[i] = window.rotateArray[i];}return;}if(window.isFindRoute) return;for(var i = 0; i < 27; i++) {if(window.hash[i]) continue;// 魔方中点不找,待会应该判断魔方中点,不应该直接赋值if(cubes[i].pos3.isEqual(new Vector3())) continue;var front = window.rotateArray[index - 1];var dis = cubes[front].pos3.getDistance(cubes[i].pos3);if(Math.abs(dis - 60) > 10) continue;window.rotateArray[index] = i;window.hash[i] = true;dfs(index + 1);window.hash[i] = false;}
}

  我是先找一条长度为8的闭合回路,找到后再进行判断:(其实边找边判断效率会更高)

  1、判断8个点是否在同一个面上。 可以任选两条不平行的向量做分别垂直于这两条向量的法向量,如果这8个点成面,则该法向量垂直于平面内两点组成的任意向量。

  2、如果是最前面的面,则return。 这个判断有点坑爹,先看下图:

  如果操作的是1和2两个正方体,得到两条回路如图。我们想要的应该是上面那个3*3模块的操作,剔除的是前面一块,这里我根据平均的z值进行判断,如果z太小(距离视点太近,认为是前面一块),则剔除。其实这是不准确的,所以demo有时会出错,而这点也是操作正方体体心无法解决的,如果要解决,程序复杂度可能要上升一个级别,要精确到对面的判断。所以这里采用了模糊判断。这也是最前面说的有两条回路如何选择的方法。

  3、找到了同一平面的闭合回路,但是不符合要求,如下:

  因为闭合回路所组成的3*3模块的中心肯定是魔方上某正方体的体心,这里就根据此近似判断。

  至此,我们得到了需要翻转的3*3=9个正方体。

 

  • 旋转轴的确定

  得到了需要翻转的正方体,最后只需要得到翻转轴即可。

  我们已经得到绕x轴和y轴旋转后的坐标变化,那么是否有绕任意轴的坐标变化公式呢?luckily,答案是有的 -> 三维空间里一个点绕矢量旋转后的新的点的坐标

  

  这样就好办了,我们可以获取需要翻转面的法向量,然后单位化即可。而这条法向量其实肯定经过27个正方体中某个的体心,遍历即可。但是一个面的法向量有两条,还好我们获取的闭合回路是有方向的,因为翻转的角度肯定是90度,我们可以知道3*3模块中某个正方体翻转90度后的实际位置,其实就是闭合回路往前两个的正方体的位置;我们获取的任一法向量,将值代入函数中进行计算,选择某个正方体,如果该正方体绕该法向量旋转90度后得到的值就是正确的位置,即这条法向量为正解。(实际上另一条需要旋转270度)

  于是我们写成一个rotateP函数:

rotateP: function() {if(this.cube.isRotate) {this.cube.index++;// 一个点达到60改变isRotate值?应该8个点全部达到吧if(this.cube.index === 480) {this.cube.isRotate = false;this.cube.index = 0;}var c = Math.cos(this.cube.garden.angleP);var s = Math.sin(this.cube.garden.angleP);// (x,y,z)为经过原点的单位向量var x = this.cube.rotateVector.x;var y = this.cube.rotateVector.y;var z = this.cube.rotateVector.z;var new_x = (x * x * (1 - c) + c) * this.pos3.x + (x * y * (1 - c) - z * s) * this.pos3.y + (x * z * (1 - c) + y * s) * this.pos3.z;var new_y = (y * x * (1 - c) + z * s) * this.pos3.x + (y * y * (1 - c) + c) * this.pos3.y + (y * z * (1 - c) - x * s) * this.pos3.z;var new_z = (x * z * (1 - c) - y * s) * this.pos3.x + (y * z * (1 - c) + x * s) * this.pos3.y + (z * z * (1 - c) + c) * this.pos3.z;this.pos3.reset(new_x, new_y, new_z);} 

  这样在每帧的渲染中,需要旋转的cube的点的坐标的位置也会随着rotateP函数改变,于是出现旋转效果。

 

总结

  完整代码:

  1 <!DOCTYPE html>
  2 <html>
  3   <head>
  4     <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  5     <title> 3d魔方 </title>
  6     <script>
  7       window.onload = function() {
  8         var canvas = document.getElementById('canvas');
  9         var ctx = canvas.getContext('2d'); 
 10         var garden = new Garden(canvas);
 11         window.garden = garden;
 12 
 13         // 0红 1橙 3蓝 4绿 5黄 6白 // face面绘制顺序 前 右 后 左 上 下
 14         window.colors = ['#ff0000', '#ff6600', '#0000ff', '#00ff00', '#ffff00', '#ffffff'];
 15        
 16         // 记录鼠标操作的两个cube的index值
 17         window.rotateArray = [];
 18         window.isStill = false;
 19         
 20         // 设置二维视角原点(一般为画布中心)
 21         garden.setBasePoint(500, 250);
 22           
 23         var color = [
 24           // 第一排
 25           [2, 5, 5, 5, 5, 5],
 26           [0, 5, 5, 5, 0, 5],
 27           [2, 0, 5, 5, 4, 5],
 28           [0, 5, 5, 4, 5, 5],
 29           [4, 5, 5, 5, 5, 5],
 30           [5, 3, 5, 5, 5, 5],
 31           [3, 5, 5, 5, 5, 0],
 32           [0, 5, 5, 5, 5, 3],
 33           [1, 4, 5, 5, 5, 2],
 34 
 35           // 第二排
 36           [5, 5, 3, 5, 1, 5],
 37           [5, 5, 3, 5, 2, 5],
 38           [5, 5, 3, 5, 0, 5],
 39           [5, 5, 5, 0, 5, 5],
 40           [5, 5, 5, 5, 5, 5],
 41           [5, 1, 1, 5, 5, 5],
 42           [5, 5, 0, 3, 5, 4],
 43           [5, 5, 5, 5, 5, 3],
 44           [5, 1, 3, 5, 5, 3],
 45 
 46           // 第三排
 47           [5, 5, 3, 2, 4, 5],
 48           [5, 5, 1, 5, 4, 5],
 49           [5, 2, 0, 5, 4, 5],
 50           [5, 5, 1, 3, 5, 5],
 51           [5, 5, 1, 5, 5, 5],
 52           [5, 2, 3, 5, 5, 5],
 53           [5, 5, 1, 4, 5, 5],
 54           [5, 5, 1, 5, 5, 2],
 55           [5, 2, 5, 5, 5, 1],
 56         ];
 57 
 58         var r = 60;
 59         var num = 0;
 60         var a = [-r, 0, r];
 61 
 62         // 初始化
 63         for(var l = 0; l < 3; l++)  // z轴
 64           for(var j = 0; j < 3; j++) // y轴
 65             for(var i = 0; i < 3; i++)  { // x轴
 66               var v = new Vector3(a[i], a[j], a[l]);
 67               garden.createCube(v, r / 2 - 2, color[num++]); // 初始化cube的index值
 68             }
 69 
 70         garden.setListener();
 71         addListener();
 72 
 73         // 渲染
 74         setInterval(function() {garden.render();}, 1000 / 60);  
 75       };
 76 
 77       function addListener() {
 78         document.addEventListener('mousedown', function(event){
 79           window.rotateArray = [];
 80           var obj = canvas.getBoundingClientRect();
 81           // 鼠标点击的地方在canvas上的(x,y)坐标
 82           var x = event.clientX - obj.left;
 83           var y = event.clientY - obj.top;
 84           var v = new Vector2(x, y)
 85           var ans = getCubeIndex(v);
 86           if(ans)
 87             window.rotateArray.push(ans);
 88         });
 89 
 90         document.addEventListener('mouseup', function(event){
 91           var obj = canvas.getBoundingClientRect();
 92           // 鼠标点击的地方在canvas上的(x,y)坐标
 93           var x = event.clientX - obj.left;
 94           var y = event.clientY - obj.top;
 95           var v = new Vector2(x, y)
 96           var ans = getCubeIndex(v);
 97           if(ans)
 98             window.rotateArray.push(ans);
 99           
100           window.isFindRoute = false;
101           window.hash = [];
102           window.hash[window.rotateArray[0]] = window.hash[window.rotateArray[1]] = true;
103 
104           // 保存回路答案
105           window.rotateFinalArray = [];
106           dfs(2);
107 
108           // 计算中间点在cube数组中的位置
109           var index = getMiddleCube();
110           rotateFinalArray.push(index);
111 
112           // 必定是体心指向某个cube中心的一条向量,返回该cube的index
113           var index2 = getRotateVector();
114 
115           var cubes = garden.cubes;
116           for(var i = 0; i < rotateFinalArray.length; i++) {
117             cubes[rotateFinalArray[i]].isRotate = true;
118             cubes[rotateFinalArray[i]].rotateVector = cubes[index2].pos3.normalize();
119           }
120         });
121 
122         document.onkeydown = function(e) {
123           if(e.keyCode === 32) {
124             window.isStill = !window.isStill;
125           }     
126         }
127       }
128 
129       function dfs(index) {
130         var cubes = garden.cubes;
131         if(index === 8) {
132           var dis = cubes[window.rotateArray[0]].pos3.getDistance(cubes[window.rotateArray[7]].pos3);
133           if(Math.abs(dis - 60) > 10) 
134             return;
135 
136           // 判断同一平面
137           var cubes = garden.cubes;
138           var a = cubes[window.rotateArray[1]].pos3.minus(cubes[window.rotateArray[0]].pos3);
139           var b = cubes[window.rotateArray[7]].pos3.minus(cubes[window.rotateArray[6]].pos3);
140 
141           // 找一个面的法向量,如果8点成面,那么肯定有两条符合的向量
142           var v = undefined;
143           for(var i = 0; i < 27; i++) {
144             var c = cubes[i].pos3;
145             if(a.isPerpTo(c) && b.isPerpTo(c)) {
146               v = c;
147               break;
148             }
149             if(i === 26 && v === undefined) return;
150           }
151 
152           // 判断任意相邻向量是否垂直法向量
153           for(var i = 0; i < 7; i++) {
154             var a = cubes[window.rotateArray[i]].pos3.minus(cubes[window.rotateArray[i + 1]].pos3);
155             if(!a.isPerpTo(v)) return;
156           }
157 
158           // 如果是最前面的面,return
159           var zz = 0;
160           for(var i = 0; i < 8; i++) 
161             zz += cubes[window.rotateArray[i]].pos3.z;
162           zz /= 8;
163           if(zz < -40) return;
164 
165           // 如果是俄罗斯方块那种类型
166           var vv = new Vector3();
167           for(var i = 0; i < 8; i+=2) {
168             vv.x += cubes[window.rotateArray[i]].pos3.x;
169             vv.y += cubes[window.rotateArray[i]].pos3.y;
170             vv.z += cubes[window.rotateArray[i]].pos3.z;
171           }
172           vv.x /= 4; 
173           vv.y /= 4;
174           vv.z /= 4;
175           var flag = false;
176           for(var i = 0; i < 27; i++) {
177             var vvv = cubes[i].pos3
178             if(vv.getDistance(vvv) > 5) continue;
179             flag = true;
180             break;
181           }
182           if(!flag) return;
183 
184           for(var i = 0; i < 8; i++) {
185             window.isFindRoute = true;
186             window.rotateFinalArray[i] = window.rotateArray[i];
187           }
188           return;
189         }
190 
191         if(window.isFindRoute) return;
192 
193         for(var i = 0; i < 27; i++) {
194           if(window.hash[i]) continue;
195           // 魔方中点不找
196           if(cubes[i].pos3.isEqual(new Vector3())) continue;
197           var front = window.rotateArray[index - 1];
198           var dis = cubes[front].pos3.getDistance(cubes[i].pos3);
199           if(Math.abs(dis - 60) > 10) continue;
200           window.rotateArray[index] = i;
201           window.hash[i] = true;
202           dfs(index + 1);
203           window.hash[i] = false;
204         }
205       }
206 
207       // 不在同一条直线的两个向量才能确定一个平面
208       function getRotateVector() {
209         // 垂直于rotate面的任意两条向量
210         var cubes = garden.cubes;
211         var a = cubes[window.rotateFinalArray[1]].pos3.minus(cubes[window.rotateFinalArray[0]].pos3);
212         var b = cubes[window.rotateFinalArray[7]].pos3.minus(cubes[window.rotateFinalArray[6]].pos3);
213 
214         // 这里应该有两个
215         for(var i = 0; i < 27; i++) {
216           var c = cubes[i].pos3;
217           // 因为有两个向量,所以通过istrue函数判断是否是答案所要的向量
218           if(a.isPerpTo(c) && b.isPerpTo(c) && isTrue(i)) 
219             return i;
220         }
221       }
222 
223       // 判断window.rotateFinalArray里的第0个cube经过90度旋转是否能到达第2个cube的位置,判断体心即可
224       function isTrue(index) {
225         var cubes = garden.cubes;
226         // 旋转向量
227         var v = cubes[index].pos3;
228         // 单位化
229         v = v.normalize();
230 
231         var a = cubes[window.rotateFinalArray[0]];
232         var c = Math.cos(Math.PI / 2);
233         var s = Math.sin(Math.PI / 2);
234         // (x,y,z)为经过原点的单位向量
235         var x = v.x;
236         var y = v.y;
237         var z = v.z;
238         var new_x = (x * x * (1 - c)+c) * a.pos3.x + (x*y*(1-c)-z*s) * a.pos3.y + (x*z*(1-c)+y*s) * a.pos3.z;
239         var new_y = (y*x*(1-c)+z*s) * a.pos3.x + (y*y*(1-c)+c) * a.pos3.y + (y*z*(1-c)-x*s) * a.pos3.z;
240         var new_z = (x*z*(1-c)-y*s) * a.pos3.x + (y*z*(1-c)+x*s) * a.pos3.y + (z*z*(1-c)+c) * a.pos3.z;
241         var b = new Vector3(new_x, new_y, new_z);
242 
243         // 判断旋转后所得的b向量是否和rotateArray[2]相同
244         var f = b.isEqual(cubes[window.rotateFinalArray[2]].pos3);
245         return f;
246       }
247 
248       function getMiddleCube() {
249         var v = new Vector3();
250         var cubes = garden.cubes;
251         for(var i = 0; i < 8; i += 2) {
252           v.x += cubes[window.rotateFinalArray[i]].pos3.x;
253           v.y += cubes[window.rotateFinalArray[i]].pos3.y;
254           v.z += cubes[window.rotateFinalArray[i]].pos3.z;
255         }
256 
257         v.x /= 4;
258         v.y /= 4;
259         v.z /= 4;
260         for(var i = 0; i < 27; i++) {
261           if(v.isEqual(cubes[i].pos3))
262             return i;
263         }
264       }
265 
266       function getCubeIndex(v) {
267         var length = garden.cubes.length;
268         var cubes = garden.cubes;
269         // 遍历cube,因为经过排序前面的cube先绘,所以倒着判断
270         var num = 0;
271         for(var i = length -1 ; i >= 0; i--) {
272           // 遍历六个面
273           for(var j = 5; j>=0; j--) {
274             num ++;
275             var f = cubes[i].f[j];
276             if(f.angle < 0) continue; // 夹角大于90不可见
277             // 可见则判断
278             var isFound = isPointIn(f.a.pos2, f.d.pos2, f.c.pos2, f.b.pos2, v);
279             if(isFound) { // 找到了
280               // 越大越晚绘,所以越前面
281               return i;
282             }
283           }
284         }
285       }
286 
287       // 判断点m是否在顺时针方向的a,b,c,d四个点组成的凸四边形内
288       function isPointIn(a, b, c, d, m) {
289         var f = b.minus(a).dot(m.minus(a));
290         if(f <= 0) return false;
291 
292         var f = c.minus(b).dot(m.minus(b));
293         if(f <= 0) return false;
294 
295         var f = d.minus(c).dot(m.minus(c));
296         if(f <= 0) return false;
297 
298         var f = a.minus(d).dot(m.minus(d));
299         if(f <= 0) return false;
300         return true;
301       }
302 
303       // Garden类
304       function Garden(canvas) {
305         this.canvas = canvas;
306         this.ctx = this.canvas.getContext('2d');
307 
308         // 三维系在二维上的原点
309         this.vpx = undefined;
310         this.vpy = undefined;
311         this.cubes = [];
312         this.angleY = Math.PI / 180 * 0;
313         this.angleX = Math.PI / 180 * 0;
314         this.angleP = Math.PI / 180 * 1.5;
315       }
316 
317       Garden.prototype = {
318         setBasePoint: function(x, y) {
319           this.vpx = x;
320           this.vpy = y;
321         },
322 
323         createCube: function(v, r, color, index) {
324           this.cubes.push(new Cube(this, v, r, color));
325         },
326 
327         render: function() {
328           this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
329           this.cubes.sort(function (a, b) {
330             if(b.pos3.z !== a.pos3.z)
331               return b.pos3.z - a.pos3.z;
332             else if(b.pos3.x !== a.pos3.x) {
333               if(b.pos3.x >= 0 && a.pos3.x >= 0 || b.pos3.x <= 0 && a.pos3.x <= 0)
334                 return Math.abs(b.pos3.x) - Math.abs(a.pos3.x);
335               else return b.pos3.x - a.pos3.x;
336             } else {
337               if(b.pos3.y >= 0 && a.pos3.y >= 0 || b.pos3.y <= 0 && a.pos3.y <= 0)
338                 return Math.abs(b.pos3.y) - Math.abs(a.pos3.y);
339               else return b.pos3.y - a.pos3.y;
340             }
341           });
342 
343           for(var i = 0; i < this.cubes.length; i++) {
344             this.cubes[i].render();
345           }        
346         },
347 
348         setListener: function() {
349           var that = this;
350           document.addEventListener('mousemove', function(event){
351             var obj = canvas.getBoundingClientRect();
352             var x = event.clientX - obj.top - that.vpx;
353             var y = event.clientY - obj.left - that.vpy;
354             that.angleY = -x * 0.0001;
355             that.angleX = y * 0.0001;
356           });
357         }
358       };
359 
360       // Cube类
361       function Cube(garden, v, r, color) {
362         this.garden = garden;
363 
364         // 正方体中心和半径
365         this.pos3 = v;
366         this.r = r;
367 
368         // this.angleX = Math.PI / 180 * 1;
369         // this.angleY = Math.PI / 180 * 1;
370 
371         // cube的8个点
372         this.p = [];
373 
374         // cube的6个面
375         this.f = [];
376 
377         // 6个面的颜色集
378         this.colors = color;  // color数组
379 
380         // 是否在玩家需要翻转的3*3矩形中
381         this.isRotate = false;
382 
383         // rotateP函数中围绕的轴的单位向量
384         this.rotateVector = new Vector3(1, 0, 0);
385 
386         // 已翻转的次数,每次翻转1.5度,需要翻转60次
387         this.index = 0;
388 
389         this.init();
390       }
391 
392       Cube.prototype = {
393         init: function() {
394           // 正方体的每个顶点都是一个ball类实现
395           this.p[0] = new Ball(this, this.pos3.x - this.r, this.pos3.y - this.r, this.pos3.z - this.r);
396           this.p[1] = new Ball(this, this.pos3.x - this.r, this.pos3.y + this.r, this.pos3.z - this.r);
397           this.p[2] = new Ball(this, this.pos3.x + this.r, this.pos3.y + this.r, this.pos3.z - this.r);
398           this.p[3] = new Ball(this, this.pos3.x + this.r, this.pos3.y - this.r, this.pos3.z - this.r);
399           this.p[4] = new Ball(this, this.pos3.x - this.r, this.pos3.y - this.r, this.pos3.z + this.r);
400           this.p[5] = new Ball(this, this.pos3.x - this.r, this.pos3.y + this.r, this.pos3.z + this.r);
401           this.p[6] = new Ball(this, this.pos3.x + this.r, this.pos3.y + this.r, this.pos3.z + this.r);
402           this.p[7] = new Ball(this, this.pos3.x + this.r, this.pos3.y - this.r, this.pos3.z + this.r);
403 
404           // 正方体6个面
405           this.f[0] = new Face(this, this.p[0], this.p[1], this.p[2], this.p[3], this.colors[0]);
406           this.f[1] = new Face(this, this.p[3], this.p[2], this.p[6], this.p[7], this.colors[1]);
407           this.f[2] = new Face(this, this.p[6], this.p[5], this.p[4], this.p[7], this.colors[2]);
408           this.f[3] = new Face(this, this.p[4], this.p[5], this.p[1], this.p[0], this.colors[3]);
409           this.f[4] = new Face(this, this.p[0], this.p[3], this.p[7], this.p[4], this.colors[4]);
410           this.f[5] = new Face(this, this.p[5], this.p[6], this.p[2], this.p[1], this.colors[5]);
411         },
412 
413         render: function() {
414           for(var i = 0; i < 8; i++) 
415             this.p[i].render();
416 
417           // 八个点的坐标改变完后,改变cube体心坐标,为下一帧cube的排序作准备
418           this.changeCoordinate();
419 
420           for(var i = 0; i < 6; i++)
421             this.f[i].angle = this.f[i].getAngle();
422 
423           // 从小到大排
424           // 不排序会闪
425           this.f.sort(function (a, b) {
426             return a.angle > b.angle;
427           });
428 
429           for(var i = 0; i < 6; i++) {
430             // 夹角 < 90,绘制
431             if(this.f[i].angle > 0)
432               this.f[i].draw();
433           }
434         },
435 
436         // cube体心坐标改变
437         changeCoordinate: function() {
438           this.pos3.x = this.pos3.y = this.pos3.z = 0;
439           for(var i = 0; i < 8; i++) {
440             this.pos3.x += this.p[i].pos3.x;
441             this.pos3.y += this.p[i].pos3.y;
442             this.pos3.z += this.p[i].pos3.z;
443           }
444           this.pos3.x /= 8;
445           this.pos3.y /= 8;
446           this.pos3.z /= 8;
447         }
448       };
449 
450       // Face类
451       // a, b, c, d为四个ball类
452       // color为数字
453       function Face(cube, a, b, c, d, color) {
454         this.cube = cube;
455         this.a = a;
456         this.b = b;
457         this.c = c;
458         this.d = d;
459         // this.color = '#' + ('00000' + parseInt(Math.random() * 0xffffff).toString(16)).slice(-6);
460         this.color = window.colors[color];
461         // 面的法向量和面心到视点向量的夹角的cos值
462         this.angle = undefined;
463       }
464 
465       Face.prototype = {
466         draw: function() {
467           var ctx = this.cube.garden.ctx;
468           ctx.beginPath();
469           ctx.fillStyle = this.color;
470           ctx.moveTo(this.a.pos2.x, this.a.pos2.y);
471           ctx.lineTo(this.b.pos2.x, this.b.pos2.y);
472           ctx.lineTo(this.c.pos2.x, this.c.pos2.y);
473           ctx.lineTo(this.d.pos2.x, this.d.pos2.y);
474           ctx.closePath();
475           ctx.fill();
476         },
477 
478         // 获取面的法向量和z轴夹角
479         getAngle: function() {
480           var x = (this.a.pos3.x + this.b.pos3.x + this.c.pos3.x + this.d.pos3.x) / 4 - this.cube.pos3.x;
481           var y = (this.a.pos3.y + this.b.pos3.y + this.c.pos3.y + this.d.pos3.y) / 4 - this.cube.pos3.y;
482           var z = (this.a.pos3.z + this.b.pos3.z + this.c.pos3.z + this.d.pos3.z) / 4 - this.cube.pos3.z;
483           // 面的法向量
484           var v = new Vector3(x, y, z);
485 
486           // 视点设为(0,0,-500)
487           var x = 0 - (this.a.pos3.x + this.b.pos3.x + this.c.pos3.x + this.d.pos3.x) / 4;
488           var y = 0  - (this.a.pos3.y + this.b.pos3.y + this.c.pos3.y + this.d.pos3.y) / 4;
489           var z = - 500 - (this.a.pos3.z + this.b.pos3.z + this.c.pos3.z + this.d.pos3.z) / 4;
490           // 面心指向视点的向量
491           var v2 = new Vector3(x, y, z);
492           return v.dot(v2);
493         }
494       };
495 
496       // Ball类
497       function Ball(cube, x, y, z) {
498         this.cube = cube;
499 
500         // 三维上坐标
501         this.pos3 = new Vector3(x, y, z)
502 
503         // 二维上坐标
504         this.pos2 = new Vector2();
505       }
506       
507       Ball.prototype = {
508         // 绕y轴变化,得出新的x,z坐标
509         rotateY: function() {
510           if(window.isStill) return;
511           var cosy = Math.cos(this.cube.garden.angleY);
512           var siny = Math.sin(this.cube.garden.angleY);
513           var x1 = this.pos3.z * siny + this.pos3.x * cosy;
514           var z1 = this.pos3.z * cosy - this.pos3.x * siny;
515           this.pos3.reset(x1, this.pos3.y, z1);
516         },
517 
518         // 绕x轴变化,得出新的y,z坐标
519         rotateX: function() {
520           if(window.isStill) return;
521           var cosx = Math.cos(this.cube.garden.angleX);
522           var sinx = Math.sin(this.cube.garden.angleX);
523           var y1 = this.pos3.y * cosx - this.pos3.z * sinx;
524           var z1 = this.pos3.y * sinx + this.pos3.z * cosx;
525           this.pos3.reset(this.pos3.x, y1, z1);
526         },
527 
528         // 绕任意穿过原点的轴旋转
529         rotateP: function() {
530           if(this.cube.isRotate) {
531              this.cube.index++;
532             // 8 * 60
533             if(this.cube.index === 480) {
534               this.cube.isRotate = false;
535               this.cube.index = 0;
536             }
537 
538             var c = Math.cos(this.cube.garden.angleP);
539             var s = Math.sin(this.cube.garden.angleP);
540             // (x,y,z)为经过原点的单位向量
541             var x = this.cube.rotateVector.x;
542             var y = this.cube.rotateVector.y;
543             var z = this.cube.rotateVector.z;
544             var new_x = (x * x * (1 - c)+c) * this.pos3.x + (x*y*(1-c)-z*s) * this.pos3.y + (x*z*(1-c)+y*s) * this.pos3.z;
545             var new_y = (y*x*(1-c)+z*s) * this.pos3.x + (y*y*(1-c)+c) * this.pos3.y + (y*z*(1-c)-x*s) * this.pos3.z;
546             var new_z = (x*z*(1-c)-y*s) * this.pos3.x + (y*z*(1-c)+x*s) * this.pos3.y + (z*z*(1-c)+c) * this.pos3.z;
547             this.pos3.reset(new_x, new_y, new_z);
548           }
549         },
550 
551         getPositionInTwoDimensionalSystem: function(a) {
552           // focalLength 表示当前焦距,一般可设为一个常量
553           var focalLength = 300; 
554           // 把z方向扁平化
555           var scale = focalLength / (focalLength + this.pos3.z);
556           this.pos2.x = this.cube.garden.vpx + this.pos3.x * scale;
557           this.pos2.y = this.cube.garden.vpy + this.pos3.y * scale;
558         },
559 
560         render: function() {
561           this.rotateX();
562           this.rotateY();
563           this.rotateP();
564           this.getPositionInTwoDimensionalSystem();
565         }
566       };
567 
568       // 向量
569       function Vector3(x, y, z) {
570         this.x = x || 0;
571         this.y = y || 0;
572         this.z = z || 0;
573       } 
574 
575       Vector3.prototype.reset = function(x, y, z) {
576         this.x = x;
577         this.y = y;
578         this.z = z;
579       }   
580 
581       // 向量点积,大于0为0~90度
582       Vector3.prototype.dot = function(v) {
583         return this.x * v.x + this.y * v.y + this.z * v.z;
584       };
585 
586       Vector3.prototype.length = function() {
587         return Math.sqrt(this.sqrLength());
588       };
589 
590       Vector3.prototype.sqrLength = function() {
591         return this.x * this.x + this.y * this.y + this.z * this.z;
592       };
593 
594       Vector3.prototype.getDistance = function(v) {
595         var dis = (this.x - v.x) * (this.x - v.x) + (this.y - v.y) * (this.y - v.y) + (this.z - v.z) * (this.z - v.z);
596         return Math.sqrt(dis);
597       };
598 
599       // 近似判断两个向量是否是同一个
600       // 因为程序中基本上是判断3*3*3的27个点是否是同一个,不同的点距离实在太远
601       Vector3.prototype.isEqual = function(v) {
602         if(this.getDistance(v) < 30) return true;
603         else return false;
604       };
605 
606       // 标准化,单位长度为1
607       Vector3.prototype.normalize = function() {
608         var inv = 1 / this.length();
609           return new Vector3(this.x * inv, this.y * inv, this.z * inv);
610       }
611 
612       // 是否垂直,点积为0
613       Vector3.prototype.isPerpTo = function(v) {
614         var ans = this.dot(v);
615         if(Math.abs(ans) < 5) return true;
616         return false;
617       }
618       
619       // 向量ab,即为b向量减去a向量返回的新向量
620       Vector3.prototype.minus = function(v) {
621         return new Vector3(this.x - v.x, this.y - v.y, this.z - v.z);
622       }
623 
624       ////
625       // 二维向量
626       function Vector2(x, y) {
627         this.x = x || 0;
628         this.y = y || 0;
629       }
630 
631       Vector2.prototype.reset = function(x, y) {
632         this.x = x;
633         this.y = y;
634       } 
635 
636       // 向量叉乘
637       Vector2.prototype.dot = function(v) {
638         return this.x * v.y - v.x * this.y;
639       };
640 
641       Vector2.prototype.minus = function(v) {
642         return new Vector2(this.x - v.x, this.y - v.y);
643       }
644     </script>
645   </head>
646   <body bgcolor='#000'> 
647     <canvas id='canvas' width=1000 height=600 style='background-color:#000'>
648       This browser does not support html5.
649     </canvas>
650   </body>
651 </html>
View Code

  其实这是蛮坑爹的体验,h5原生api不适合做这种3d效果。但重要的是思考过程,不是结果。

  这只是一个demo,如果要做一个真正的魔方游戏,还需要以下几点:

  • 魔方颜色初始化

  现在魔方的颜色我是随意设置的,如果是个可玩的游戏,先得初始化复原后的魔方颜色,然后在游戏loading过程中随机打乱。

  • 3*3模块的精确判断

  之前我也说了,3*3模块的判断是不精确的,更极端的例子见下图:

   此时我鼠标操作的是1和2区域,我想旋转的是黑色箭头围成的模块,但是实际程序中旋转了黄色箭头围成的3*3,这就是因为我的模糊判断。我无法确定到底是哪一个,因为我一直是根据体心来判断的,如果要得到正确的结果,就要上升到正方体面的判断,我不知道代码量要增加几倍。(所以demo操作时尽量操作离视点近的面)

  • 游戏结果判断

  如果在确定3*3步骤使用维护三维数组的方法,这里判断相对简单;但是如果不,又得回到面的判断上,同上,很复杂。

  • 其他

  增加loading、计时等等。

 

  如果有更好的方法或建议欢迎与我交流~

  

转载于:https://www.cnblogs.com/lessfish/p/4267180.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/460525.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

彻底搞懂硬盘相关的概念

以下内容源于网络资源的学习与整理&#xff0c;如有侵权请告知删除。 参考博客 硬盘结构&#xff08;机械硬盘和固态硬盘&#xff09;详解 简单理解磁盘结构_Guanngxu的博客-CSDN博客_磁盘的结构 硬盘基础知识_Forskamse的博客-CSDN博客 硬盘知识笔记整理_落子摘星的博客-CSDN…

ubuntu下数据库的导入导出

2019独角兽企业重金招聘Python工程师标准>>> 一.导出远程数据库 例如 sudo mysqldump -h 172.16.1.211 -u haha -p123 -P3307 app>app.sql 二.导入.sql文件的 到数据库 1.create database xxx(创建一个名称是xxx的数据库) 2. use xxx(切换到该数据库下) 3. sou…

MBR分区表的简介

以下内容源于网络资源的学习与整理&#xff0c;如有侵权请告知删除。 参考内容 &#xff08;1&#xff09;S5PV210 Uboot开发与移植01&#xff1a;Uboot概述_麦兜的学习笔记的博客-CSDN博客 &#xff08;2&#xff09;Linux系统下的硬盘分区、格式化与挂载_天糊土的博客-CSD…

Redis配置文件参数说明

配置文件参数说明: 1. Redis默认不是以守护进程的方式运行&#xff0c;可以通过该配置项修改&#xff0c;使用yes启用守护进程 daemonize no 2. 当Redis以守护进程方式运行时&#xff0c;Redis默认会把pid写入/var/run/redis.pid文件&#xff0c;可以通过pidfile指定 pidfile …

movi命令(do_movi函数的源码分析)

以下内容源于网络资源的学习与整理&#xff0c;如有侵权请告知删除。 一、do_movi函数分析 当执行movi相关命令时&#xff0c;实际执行的是do_movi函数。 x210 # help movi movi init - Initialize moviNAND and show card info movi read {u-boot | kernel} {addr} - Read …

[LeetCode]Search Insert Position

原题链接&#xff1a;http://oj.leetcode.com/problems/search-insert-position/ 题意描述&#xff1a; Given a sorted array and a target value, return the index if the target is found. If not, return the index where it would be if it were inserted in order. You …

如何将镜像烧写至iNand(fastboot命令的源码分析)

以下内容源于网络资源的学习与整理&#xff0c;如有侵权请告知删除。 参考博客 u-boot sdfuse命令烧录分析----从SD卡加载内核_white_bugs的博客-CSDN博客 一、将镜像文件烧写至iNand的步骤 步骤1&#xff1a;完成准备工作。 &#xff08;1&#xff09;准备fastboot相关软件包…

你知道自己执行的是哪个jre吗?

多个JRE 我在做《Java日志工具之java.util.logging.Logger》的DEMO时&#xff0c;修改java.util.logging.Logger的配置文件&#xff0c;怎么修改都不起作用&#xff0c;因此打印了系统属性"java.home"&#xff0c;才知道自己使用的是 C:\Program Files\Java\jre7 &am…

无需写try/catch,也能正常处理异常

对于企业应用的开发者来说&#xff0c;异常处理是一件既简单又复杂的事情。说其简单&#xff0c;是因为相关的编程无外乎try/catch/finallythrow而已&#xff1b;说其复杂&#xff0c;是因为我们往往很难按照我们真正需要的策略来处理异常。我一直有这样的想法&#xff0c;理想…

vs2010插件

转载于:https://www.cnblogs.com/tinytiny/p/3608030.html

leetcode1——两数之和

一、提出问题 给定一个整数数组 nums 和一个整数目标值 target&#xff0c;请你在该数组中找出 和为目标值 target 的那 两个 整数&#xff0c;并返回它们的数组下标。 你可以假设每种输入只会对应一个答案。但是&#xff0c;数组中同一个元素在答案里不能重复出现。 你可以按…

如何脱离SDK,使用DW5.5和phonegap以及JQMobile搭建开发环境

也许有些人是学C出身&#xff0c;对于Java几乎不了解。一时心血来潮想学学android开发&#xff0c;于是下载了Eclipse&#xff0c;安装了SDK&#xff0c;有模有样的学习起来。也许是懒惰了&#xff0c;对于java一直总是提不起精神。于是确定使用DreamweaverJquery mobilePhoneg…

leetcode2——两数相加

一、提出问题 给你两个非空的链表&#xff0c;表示两个非负的整数。它们每位数字都是按照逆序的方式存储的&#xff0c;并且每个节点只能存储一位数字。请你将两个数相加&#xff0c;并以相同形式返回一个表示和的链表。 你可以假设除了数字 0 之外&#xff0c;这两个数都不会以…

32为Linux安卓AVD启动报错

2019独角兽企业重金招聘Python工程师标准>>> 当使用android的AVD时提示以下错误&#xff1a;Starting emulator for AVD NexusOne ERROR: 32-bit Linux Android emulator binaries are DEPRECATED, to use them you will have to do at least one of the following:…

填问卷,得《2015中国呼叫中心知识库现状与问题报告》

为了解中国呼叫中心知识库运营现状和存在的主要问题&#xff0c;掌握呼叫中心知识库的总体发展水平&#xff0c;中国知识管理中心&#xff08;KMCenter&#xff09;面向全国呼叫中心发起“2015中国呼叫中心知识库现状与问题调研”活动&#xff0c;主要通过问卷调研和典型用户访…

ARM汇编的特点

以下内容源于网络资源的学习与整理&#xff0c;如有侵权请告知删除。 一、LDR/STR架构 ARM采用RISC架构&#xff0c;CPU本身不能直接读取内存&#xff0c;需要先将内存中的内容加载到CPU的通用寄存器中才能被CPU处理。换言之&#xff0c;寄存器是CPU和内存进行数据交换的中介。…

持久代是方法区还是堆中的?

2019独角兽企业重金招聘Python工程师标准>>> 昨天跟一哥们讨论&#xff0c;持久代在方法区&#xff0c;属不属于堆中的? 它的意思是持久代不属于堆,属于方法区&#xff0c;而我则认为持久代属于方法区也属于堆。 结果今天上网一查,还真的是。下面是解释: 持久代”仅…

Web 前端攻防(2014版)-baidu ux前端研发部

http://fex.baidu.com/articles/page2/ Web 前端攻防&#xff08;2014版&#xff09; zjcqoo | 20 Jun 2014禁止一切外链资源 外链会产生站外请求&#xff0c;因此可以被利用实施 CSRF 攻击。 目前国内有大量路由器存在 CSRF 漏洞&#xff0c;其中相当部分用户使用默认的管理账…

Silverlight动态设置WCF服务Endpoint

2013-02-02 05:57 by jv9, 1763 阅读, 3 评论, 收藏, 编辑 去年12月收到一位朋友的邮件&#xff0c;咨询Silverlight使用WCF服务&#xff0c;应用部署后一直无法访问的问题&#xff0c;通过几次交流&#xff0c;才发现在他的项目中&#xff0c;全部使用静态URL作为WCF服务的End…

第4季3:Hi3518e的sensor接口引脚复用设置(load3518e文件)

以下内容源于朱有鹏嵌入式课程的学习与整理&#xff0c;如有侵权请告知删除。 在第2、3季的内容中&#xff0c;在板载系统的配置脚本即/etc/profile文件中&#xff0c;都有如下这句代码&#xff1a; ./load3518e -i -sensor ar0130 -osmem 32 -total 64 在第4季1&#xff1a…