在上一篇《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