1. 程式人生 > >接受前端挑戰:用CSS實現3D立方體

接受前端挑戰:用CSS實現3D立方體

你喜歡挑戰麼?你願意承擔一項以前從沒遇到過的任務並且按時完成麼?如果在進行任務中,你碰到來一個似乎無法解決的問題呢?我想分享我使用CSS 3D效果的經歷,那是第一次用於實際專案中,以此來激勵你接受挑戰。

那是平常的一天,當Eugene( CreativePeople的經理)寫信給我的時候。他寄給我一個視訊,說他正在為一個新專案開發一個概念,而且想知道我是否可能開發一個像視訊裡那樣的東西。

這是一個繞著一個軸旋轉的3D物體(準確地說是個立方體)。對於用CSS 3D工作我已經有一些經驗了,於是我的腦海裡開始形成一個解決方案。我Google搜尋了像“CSS 3D cube”這樣的關鍵詞來確認我的想法,隨後我回復Eugene說我可以。

Eugene下一個問題是問我是否願意承擔這個專案?我喜歡複雜的任務,所以我不能拒絕。在這一刻,我還沒有意識到我正陷入其中,但我無法確定是否可以完成。

理解軸(字面翻譯是磨斧)

提醒下這個axes不是戰斧,而是 數軸的意思,正如我們在學校學到的三維直角座標系一樣的軸線。維基百科定義:

直角座標系是一個兩兩垂直有序的三元線行成的三維空間,三條軸都有一個單獨的單位長度並且每一條軸線有一個方向。

下面的圖片展示了在Web瀏覽器中怎樣確定軸線方向。A right-handed three-dimensional Cartesian coordinate system with the Z-axis pointing towards the viewer.

一個以z軸朝向觀察者的右手三維直角座標系。 (圖片來自: 維基共享資源)。

x軸平行,y軸垂直,z軸指向正對你的螢幕。z軸的零點就是螢幕所在的平面。記住這一點。

理解透視值

要建立一個3D物體,我需要一個具有透視效果的元素(我們稱之為“scene”)。透視大小就是這個場景的深度,並且它取決於它包含的物體大小。

.scene {
    perspective: 800px;
}

如果透視距離太小,物體可能會被扭曲。如果太大,3D效果將減少到沒有。

由Anna Selezniova (@askd在 CodePen)上編寫.

此外,在這個場景中對於所有物體而言只有一個視野角度。3D效果取決於觀察點的位置。

由Anna Selezniova (@askd 在 CodePen)上編寫。

那麼,怎麼計算透視值呢?我發現它取決於軸的旋轉。對於x軸,高度值乘以4

應該合適。對於y軸,應該是寬度值乘以4。這是我的魔法公式:

const perspective = dimension * 4;

考慮所有側面

決定透視值後,我開始建立3D物件。我選擇了一個立方體,因為它簡單可預測。立方體元素由普通的div建立,相對定位,寬度和高度都定義(200px)。通過具有preserve-3d值的transform-style 屬性使它轉變成一個3D物件。它告訴瀏覽器通過3D世界的規則來渲染所有內嵌元素。

在我的例子中,這個立方體有6個絕對定位的div(或者說是側面)。類名相當於幾個側面(後面,左邊,右邊,上面,下面,前面)的初始位置。標記如下:

<div class="scene">
  <div class="cube">
    <div class="side back"></div>
    <div class="side left"></div>
    <div class="side right"></div>
    <div class="side top"></div>
    <div class="side bottom"></div>
    <div class="side front"></div>
  </div>
</div> 

預設情況下,所有側面都在一個平面上。所以,我需要將它們重新排列。演示如下:

由Anna Selezniova (@askd 在 CodePen)上編寫。

由此產生CSS如下:

.cube {
    position:relative;
    width: 200px;
    height: 200px;
    transform-style: preserve-3d;
}
.side {
    position: absolute;
    width: 200px;
    height: 200px;
}
.back {
    transform: translateZ(-100px);
}
.left {
    transform: translateX(-100px) rotateY(90deg);
}
.right {
    transform: translateX(100px) rotateY(90deg);
}
.top {
    transform: translateY(-100px) rotateX(90deg);
}
.bottom {
    transform: translateY(100px) rotateX(90deg);
}
.front {
    transform: translateZ(100px);
}

要旋轉這個立方體,我在這個元素上設定 transform屬性值是X軸旋轉任意角度:

.cube {
    transform: rotateX(42deg);
}

克服缺點

根據任務要求,我打算只沿著x軸旋轉這個立方體,所以我不需要左側或者右側。我添加了標註來將剩下側面的初始位置對齊。

我開始旋轉立方體時發現底部和背面的標註說明都顯示顛倒了:

由Anna Selezniova (@askd 在 CodePen)上編寫。

為了解決這個問題,我把每個側面都圍繞x軸旋轉了180度:

.back {
    transform: translateZ(-100px) rotateX(180deg);
}
.bottom {
    transform: translateY(100px) rotateX(270deg);
}

超越螢幕

我開始用真實內容填充側面了,隨即就遇到了另一個問題。我需要展示1個畫素的虛線,但看起來很糟糕模糊。

由Anna Selezniova (@askd 在 CodePen)上編寫。

我立馬認識到問題出在哪了。你記得圖片延伸到螢幕之外的3D TV廣告麼?這跟我這個立方體是同一回事。

如果你可以從左側或者右側看下這個立方體,就會看到它的中心在螢幕所在的平面上(z軸的零點)並且正面超出了螢幕。因此,在視覺上增大了也模糊了。

由Anna Selezniova (@askd 在 CodePen)上編寫。

為了解決這個問題,我沿著z軸移動這個立方體使得正面對齊到螢幕所在的平面:

.cube {
    transform:translateZ(-100px);
}

現在,這個立方體準備的差不多了:

由Anna Selezniova (@askd 在 CodePen)上編寫。

使用神奇數字

我猜你已經注意到我使用了這個神奇的數字100來沿著軸移動這些側面。而100這個值正好是我測試的立方體高度的一半。為什麼是一半?因為那個值是立方體側面(顯然是一個正方形)一個內切圓的半徑。

const offset = dimension / 2;

如果我需要旋轉一個三稜柱,這個圓就是三角形的內切圓。這種情況下,偏移公式就會如下:

const offset = dimension / (2 * Math.sqrt(3));

消除立方體

要想把任務完成,我必須在不同的瀏覽器中進行測試。 在IE中看到的畫面讓我陷入沮喪。為了讓你知道我在說什麼,在你最愛的瀏覽器中開啟這個樣例。我改變了一個屬性導致在IE中這個立方體顯示完全不正確。無論如何,不要偷看原始碼直到你讀了在這個樣例下面的那段文字。

由Anna Selezniova (@askd 在 CodePen)上編寫。

現實就是IE不支援值是preserve-3dtransform-style屬性。通過檢視可靠資源Can I Use(notes中第一點)我瞭解到這一點。在上面的樣例中,我將preserve-3d換成了flat。你是不是已經知道了?哼!讓你不要偷看了!

我很煩躁,但我並不打算放棄。遇到一個問題就是獲得一次學習新東西的機會。再說,我已經接收了這次挑戰。

尋找支點

我在找尋一種可以不通過使用transform-style: preserve-3d來建立一個3D物件的方法,最終我發現一個有用的屬性:transform-origin。它決定了一個元素變換的中心點。我建了一個可以互動的樣例,可以幫助你理解這個屬性是如何工作的:

由Anna Selezniova (@askd 在 CodePen)上編寫。

在這個例子中,元素的3D旋轉是不是和立方體正面很像?這正是我要用的。

(順便問一下,你嘗試過在三維旋轉過程中選擇多選框backface-visibility:hidden麼?這個屬性用來在3D變換中隱藏元素的背面)。

重新出發

我開始重做這個立方體。我不必讓整個場景進行互動,所以我去掉了scene元素的 perspective屬性然後將該屬性新增到每個3D變換,這樣每個元素的變換就是獨立的了。同時,我給每個側面設定了新屬性:transform-origin,其值是立方體中心的位置,以及backface-visibility: hidden。樣式改變如下:

.scene {

}
.cube {
    position: relative;
    width: 200px;
    height: 200px;
    transform: perspective(800px) translateZ(-100px);
}
.side {
    position: absolute;
    transform-origin: 50% 50% -100px;
    backface-visibility: hidden;
}

我必須將這些側面放在正確的位置。由於transform-origin屬性,我不用再改變它們的位置,只需要圍繞軸旋轉它們。這就像魔術一樣!我們來目睹一下它的神奇:

由Anna Selezniova (@askd 在 CodePen)上編寫。

這些側面位置的CSS如下:

.back {
    transform: perspective(800px) rotateY(180deg);
}
.top {
    transform: perspective(800px) rotateX(90deg);
}
.bottom {
    transform: perspective(800px) rotateX(-90deg);
}
.front {
    transform: perspective(800px);
}

這裡你能看到在執行中的全新立方體:

由Anna Selezniova (@askd 在 CodePen)上編寫。

橋是橋路是路,做好自己的事

第二個立方體看起來旋轉和第一個一樣。但在這個例子中,你需要單獨變換每一個側面。這可能不太容易,尤其是你想控制旋轉的中間角度。

此外,如果你在Chrome瀏覽器開啟這個例子,會看到這些側面在旋轉的時候會閃爍,這讓我感覺很沮喪。

最後,我將transform-style: preserve-3d屬性的簡單測試應用在這兩個實現立方體的方式中。第一個立方體是預設的,第二個是針對IE瀏覽器以及不支援preserve-3d的瀏覽器。

運用數學的力量

最終,我必須實現一個視差效果。通常,這種效果根據使用者行為響應,無論是滑鼠游標還是滾動條的位置。在這個例子中,這個效果取決於旋轉的角度。

由Anna Selezniova (@askd 在 CodePen)上編寫。

我有什麼資料呢?首先,我有標註文字位置的起點和終點,或者簡單說來就是從側面中心位置到上邊和下邊的偏移量。其次,我有它旋轉的角度

我花了幾個小時試圖定義一個公式。隨後,我恍然大悟。這就是我的靈感:

Graphs of the sine and cosine functions

正弦餘弦函式圖 (圖片: 維基共享資源)。

在正弦餘弦函式的幫助下,通過角度我輕鬆地計算出了每個標註的偏移。這是我提出的公式:

const front_offset = offset * sin(angle) * -1;
const bottom_offset = offset * cos(angle);
const back_offset = offset * sin(angle);
const top_offset = offset * cos(angle) * -1;

總結

現在任務完成了,我很享受這個結果並且將它分享給你。看一下它展示的如何。使用滑鼠滾動或者箭頭鍵旋轉廣告塊。同樣,你也可以嘗試拉出左邊的黑三角上下拖動來手動控制旋轉的角度(遺憾的是,這個特徵在IE瀏覽器中無法工作)。看起來確實不錯吧?而且效能也相當高(大概每秒60幀)。

我很高興參與了這個網站的開發。在CSS 3D實踐中我收穫了寶貴的經驗,並且發現了許多有意思的屬性。更重要的是,我懂得了一個人不應該輕言放棄,很可能你會找到一個方法來完成。

我希望你喜歡我的故事,也希望你現在做好準備迎接新的挑戰!