在上一篇 "《CSS3 製作魔方 形成魔方》" 中介紹了一個完整魔方的繪製實現,本文將介紹魔方的玩轉,支持上下左右每一層獨立地旋轉。先來一睹玩轉的風采。 1.一個問題 由於魔方格的位置與轉動的路徑相關,僅依靠 rotateX,rotateY,rotateZ 單個的值無法直接表明其定位。如下圖,第一個 ...
在上一篇《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