CSS3 制作魔方 - 玩轉魔方


在上一篇《CSS3 制作魔方 - 形成魔方》中介紹了一個完整魔方的繪制實現,本文將介紹魔方的玩轉,支持上下左右每一層獨立地旋轉。先來一睹玩轉的風采。

1.一個問題

由於魔方格的位置與轉動的路徑相關,僅依靠 rotateX,rotateY,rotateZ 單個的值無法直接表明其定位。如下圖,第一個魔方格進行了特殊化處理。

當使用路徑 rotateY(90)->rotateY(90)->rotateX(90)->rotateY(-90)  來旋轉這個特殊魔方格時,Y 最終是 90度,X 是90 度,按路徑旋轉的結果如下圖。

它並不等於

Y  90度,X 90 度的旋轉結果:rotateY(90)->rotateX(90)

2.解決辦法

不能直接表示,就換一種方式。可以根據旋轉的方向 重算魔方格在魔方中的坐標 並重繪魔方格,而旋轉動畫效果可以采用 逆向90度重繪再 transition 回來 的方式,詳見后述。

3.自上而下實現旋轉

3.1頂層視角

縱觀魔方,實現各層的旋轉它應該開放什么接口呢?進行旋轉需要指定的數據是什么呢?這便形成了功能的頂層描述與接口需求:指定x,y,z軸向、給定第幾層、規定轉向完成90度的旋轉。

於是在 MagicBox 類中可以增加方法:功能接口名稱使用 Rotate,其參數為 軸向(axis)、層(level)、轉向(turn)。

其中:

  • 軸向可取值 x、y、z
  • 層根據坐標體系從0開始 至 魔方階數-1
  • 轉向因為一層只會有兩個方向,為了統一描述,總以朝軸正方向視角來分左向(left)與右向(right)。

旋轉的層所包含的具體魔方格,對於頂層而言,則只需要 對旋轉要求通知到位即可,而通知的內容為軸向(axis)、轉向(turn),以及魔方階數(dimension)。

有此描述,頂層 Rotate 方法非常簡單,功能為找出指定的層進行通知即可:

/** MagicBox.Rotate 旋轉
  * axis 軸向
  * level 層
  * turn 轉向
 **/
this.Rotate = function(axis, level, turn){
    for(var i=0; i < this.cubes.length; i++) { 
        if(this.cubes[i][axis] == level) {    // 該軸該層的才旋轉
            this.cubes[i].Rotate(axis, turn, dimension);   
        }
    }
};

3.2魔方格接口實現

3.2.1旋轉坐標變換

軸向有x,y,z,但每層的旋轉只涉及到兩個軸向,有(x,y)、(y,z)、(x,z),盡管魔方格看上去很多的,但坐標的轉換卻都遵循非常簡單的變換規律,以4階的 (x,y) 坐標轉換為例,如下,黃色為同一個坐標旋轉變換的值:

很容易得出規律,旋轉前后的坐標中,總有一個是相同的,另兩個的和是固定的,而且剛好為魔方階數減1。

假設旋轉前后坐標分別為(x1,y1)、(x2,y2),則:

往左旋轉,坐標變換規律為:x2 = y1, y2 = 3 - x1,這其中的 3 為魔方階數減1。

往右旋轉,坐標變換規律為:x2 = 3 - y1, y2 = x1。

這恰是:我成為了你,而你是我的補

於是有了以下轉換過程:

/** 坐標轉換
 * axis 軸向
 * turn 轉向
 * dimension 階數
 **/
this.TransCoordinate = function(axis, turn, dimension){ 
    if(axis == 'x'){              
        if( turn == 'left' ){
            var oriy = this.y;
            this.y = this.z;
            this.z = dimension - 1 - oriy;
        } else {
            var oriz = this.z;
            this.z = this.y;
            this.y = dimension - 1 - oriz;
        }
    } else if(axis == 'y'){
        if( turn == 'right' ){
            var orix = this.x;
            this.x = this.z;
            this.z = dimension - 1 - orix;
        } else {
            var oriz = this.z;
            this.z = this.x;
            this.x = dimension - 1 - oriz;
        }
    } else if(axis == 'z'){ 
        if( turn == 'right' ){
            var orix = this.x;
            this.x = this.y;
            this.y = dimension - 1 - orix;
        } else {
            var oriy = this.y;
            this.y = this.x;
            this.x = dimension - 1 - oriy;
        }
    } 
}

3.2.2旋轉重繪

在魔方格里,通過版面(block)在旋轉方向上的變換達到旋轉的效果,方式為根據旋轉方向同向移動方向即可。

/** 將各 block 調整位置,重繪魔方格
* axis 軸向
* turn 轉向
**/
this.ReDrawBlocks = function(axis, turn){
    var xyzDirects = [];
    xyzDirects['x'] = ["front", "up", "back", "down"]; 
    xyzDirects['y'] = ["front", "right", "back", "left"]; 
    xyzDirects['z'] = ["up", "right", "down", "left"]; 
    var curDirects = xyzDirects[axis];

    for(var i=0; i < this.blocks.length; i++) { 
        var index = curDirects.indexOf( this.blocks[i].direct );
        if(index > -1){
            var newIndex = turn == 'left' ? (index + 1) % 4 : (index + 4 - 1) % 4;
            this.blocks[i].direct = curDirects[newIndex]; 
            this.blocks[i].DrawIn(this.Element);
        }	 
    }
}

3.2.3動畫體現

調整好的魔方格,逆向旋轉90度,則外觀保持跟旋轉前一樣,這就有了進行動畫的基礎,動畫的實質就是欺騙眼睛。

然后,利用 transition 讓其過濾到不旋轉的(即調整好的)外觀即可達到效果。這樣的好處是,魔方格不論在什么位置,每次相關的旋轉角度僅是逆向的 90 度,問題局部化時,事情就變得簡單

// 先停止動畫效果,逆向 90 度,此時外觀跟旋轉前一致
this.Element.style["transition"] = "";
var rotateDegs = new Object();
rotateDegs[axis] = (turn == 'left' ? -90 : 90);  
this.Element.style["transform"] = this.FormatTransform(rotateDegs); 
// 旋轉原點旋轉的層都需要以魔方的中心點旋轉
// 旋轉原點是以元素自身來計算的,因所有魔方格都是從(0,0,0)平衡的,因此計算結果都一樣
var centerX = this.blockSize * dimension / 2;
var centerY = this.blockSize * dimension / 2;
var centerZ = -this.blockSize * dimension / 2;
this.Element.style["transformOrigin"] = centerX + "px " + centerY + "px " + centerZ + "px";

// 這樣才能觸發動畫
setTimeout(function(obj){
    return function(){
        obj.Element.style["transform"] = obj.FormatTransform(); 
        obj.Element.style["transition"] = "transform 0.3s";  // 0.3 秒
    };
}(this), 1);

// 以下為transfrom 屬性格式化的一個方法,這個屬性值太長了又是旋轉平移多組合
// 格式化 transform 屬性
// css3 把旋轉與平移混一起(真不好用)
this.FormatTransform = function (rotateDegs){ 
    var rotatePart = "rotateX(0deg) rotateY(0deg) rotateZ(0deg)";
    if(rotateDegs){
        rotatePart = "rotateX(" + (rotateDegs.x | 0) + "deg) rotateY(" + (rotateDegs.y | 0) + "deg) rotateZ(" + (rotateDegs.z | 0) + "deg)";
    }	

    return rotatePart + " translate3d(" + (this.x * this.blockSize) + "px," + (this.y * this.blockSize) + "px,-" + (this.z * this.blockSize) + "px) ";		
} 

4.旋轉控制實例效果

有了這個旋轉的方法,通過給定一組旋轉參數序列,可以讓魔方自動運轉,並且自動復原。

function onload(){

    //* 魔方繪制示例
    var magicBox = new MagicBox(5, 50);
    magicBox.DrawIn( document.querySelector(".wrap") ); 

    var rotates = GenRotateActions(5, 10);
    for(var i=0; i<rotates.length; i++){    
        setTimeout(function(magicBox, rotate){
            return function(){
                magicBox.Rotate(rotate.axis, rotate.level, rotate.turn);
            };
        }(magicBox, rotates[i]), 500 * i);
    }

    /* 反向旋轉,就能復原魔方 */
    for(var i=0; i<rotates.length; i++){    
        setTimeout(function(magicBox, rotate){
            return function(){
                magicBox.Rotate(rotate.axis, rotate.level, (rotate.turn == 'left' ? 'right' : 'left'));
            };
        }(magicBox, rotates[rotates.length -1 - i]), 5500 + 500 * i);
    }
}

/** 產生一個指定數量的旋轉序列數組
 * dimension 魔方階數
 * count 序列數量
 **/
function GenRotateActions(dimension, count){
    var result = [];
    for(var i=0; i<count; i++){
        result[i] = {
            axis  : ['x','y','z'][Math.floor(Math.random() * 3)],
            level : Math.floor(Math.random() * dimension), 
            turn  : ['left','right'][Math.floor(Math.random() * 2)]
        };
    }
    return result;
}

效果如下:

5.小結與附件

尋找共性向上抽象,形成統一的處理模式能夠讓處理模型變得簡單。

本文實例,支持動態建立多階的魔方,但對參數缺少邊界檢查,同時對旋轉的是否結束未作標記或判斷,感興趣的朋友可以進一步完善它。

本實例代碼發布在 https://github.com/triplestudio/magicbox


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM