【自己给自己题目做】:如何用裸的 Canvas 实现魔方效果

2015-02-02 10:52:00  浏览:1606  作者:管理员

  最终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   5window.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;    }    ////////////////////////////////////////////////    // 如果是最前面的面,return    var 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].pos3      if(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的index113           var index2 = getRotateVector();114 115           var cubes = garden.cubes;116           for(var i = 0; i < rotateFinalArray.length; i++) {117 
            
            
            

评论区

共 0 条评论
  • 这篇文章还没有收到评论,赶紧来抢沙发吧~

【随机新闻】