CSS3 制作魔方 - 玩轉(zhuǎn)魔方

在上一篇《CSS3 制作魔方 - 形成魔方》中介紹了一個(gè)完整魔方的繪制實(shí)現(xiàn),本文將介紹魔方的玩轉(zhuǎn),支持上下左右每一層獨(dú)立地旋轉(zhuǎn)。先來一睹玩轉(zhuǎn)的風(fēng)采。

1.一個(gè)問題

由于魔方格的位置與轉(zhuǎn)動(dòng)的路徑相關(guān),僅依靠 rotateX,rotateY,rotateZ 單個(gè)的值無法直接表明其定位。如下圖,第一個(gè)魔方格進(jìn)行了特殊化處理。

當(dāng)使用路徑 rotateY(90)->rotateY(90)->rotateX(90)->rotateY(-90) 來旋轉(zhuǎn)這個(gè)特殊魔方格時(shí),Y 最終是 90度,X 是90 度,按路徑旋轉(zhuǎn)的結(jié)果如下圖。

它并不等于

Y 90度,X 90 度的旋轉(zhuǎn)結(jié)果:rotateY(90)->rotateX(90)

2.解決辦法

不能直接表示,就換一種方式??梢愿鶕?jù)旋轉(zhuǎn)的方向 重算魔方格在魔方中的坐標(biāo) 并重繪魔方格,而旋轉(zhuǎn)動(dòng)畫效果可以采用 逆向90度重繪再 transition 回來 的方式,詳見后述。

3.自上而下實(shí)現(xiàn)旋轉(zhuǎn)

3.1頂層視角

縱觀魔方,實(shí)現(xiàn)各層的旋轉(zhuǎn)它應(yīng)該開放什么接口呢?進(jìn)行旋轉(zhuǎn)需要指定的數(shù)據(jù)是什么呢?這便形成了功能的頂層描述與接口需求:指定x,y,z軸向、給定第幾層、規(guī)定轉(zhuǎn)向完成90度的旋轉(zhuǎn)。

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

其中:

  • 軸向可取值 x、y、z
  • 層根據(jù)坐標(biāo)體系從0開始 至 魔方階數(shù)-1
  • 轉(zhuǎn)向因?yàn)橐粚又粫?huì)有兩個(gè)方向,為了統(tǒng)一描述,總以朝軸正方向視角來分左向(left)與右向(right)。

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

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

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

3.2魔方格接口實(shí)現(xiàn)

3.2.1旋轉(zhuǎn)坐標(biāo)變換

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

很容易得出規(guī)律,旋轉(zhuǎn)前后的坐標(biāo)中,總有一個(gè)是相同的,另兩個(gè)的和是固定的,而且剛好為魔方階數(shù)減1。

假設(shè)旋轉(zhuǎn)前后坐標(biāo)分別為(x1,y1)、(x2,y2),則:

往左旋轉(zhuǎn),坐標(biāo)變換規(guī)律為:x2 = y1, y2 = 3 - x1,這其中的 3 為魔方階數(shù)減1。

往右旋轉(zhuǎn),坐標(biāo)變換規(guī)律為:x2 = 3 - y1, y2 = x1。

這恰是:我成為了你,而你是我的補(bǔ)!

于是有了以下轉(zhuǎn)換過程:

/** 坐標(biāo)轉(zhuǎn)換
 * axis 軸向
 * turn 轉(zhuǎn)向
 * dimension 階數(shù)
 **/
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旋轉(zhuǎn)重繪

在魔方格里,通過版面(block)在旋轉(zhuǎn)方向上的變換達(dá)到旋轉(zhuǎn)的效果,方式為根據(jù)旋轉(zhuǎn)方向同向移動(dòng)方向即可。

/** 將各 block 調(diào)整位置,重繪魔方格
* axis 軸向
* turn 轉(zhuǎn)向
**/
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動(dòng)畫體現(xiàn)

調(diào)整好的魔方格,逆向旋轉(zhuǎn)90度,則外觀保持跟旋轉(zhuǎn)前一樣,這就有了進(jìn)行動(dòng)畫的基礎(chǔ),動(dòng)畫的實(shí)質(zhì)就是欺騙眼睛。

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

// 先停止動(dòng)畫效果,逆向 90 度,此時(shí)外觀跟旋轉(zhuǎn)前一致
this.Element.style["transition"] = "";
var rotateDegs = new Object();
rotateDegs[axis] = (turn == 'left' ? -90 : 90);  
this.Element.style["transform"] = this.FormatTransform(rotateDegs); 
// 旋轉(zhuǎn)原點(diǎn)旋轉(zhuǎn)的層都需要以魔方的中心點(diǎn)旋轉(zhuǎn)
// 旋轉(zhuǎn)原點(diǎn)是以元素自身來計(jì)算的,因所有魔方格都是從(0,0,0)平衡的,因此計(jì)算結(jié)果都一樣
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";

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

// 以下為transfrom 屬性格式化的一個(gè)方法,這個(gè)屬性值太長了又是旋轉(zhuǎn)平移多組合
// 格式化 transform 屬性
// css3 把旋轉(zhuǎn)與平移混一起(真不好用)
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.旋轉(zhuǎn)控制實(shí)例效果

有了這個(gè)旋轉(zhuǎn)的方法,通過給定一組旋轉(zhuǎn)參數(shù)序列,可以讓魔方自動(dòng)運(yùn)轉(zhuǎn),并且自動(dòng)復(fù)原。

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);
    }

    /* 反向旋轉(zhuǎn),就能復(fù)原魔方 */
    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);
    }
}

/** 產(chǎn)生一個(gè)指定數(shù)量的旋轉(zhuǎn)序列數(shù)組
 * dimension 魔方階數(shù)
 * count 序列數(shù)量
 **/
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.小結(jié)與附件

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

本文實(shí)例,支持動(dòng)態(tài)建立多階的魔方,但對(duì)參數(shù)缺少邊界檢查,同時(shí)對(duì)旋轉(zhuǎn)的是否結(jié)束未作標(biāo)記或判斷,感興趣的朋友可以進(jìn)一步完善它。

本實(shí)例代碼發(fā)布在 https://github.com/triplestudio/magicbox

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容