接受前端挑戰:用CSS實現3D立方體
你喜歡挑戰麼?你願意承擔一項以前從沒遇到過的任務並且按時完成麼?如果在進行任務中,你碰到來一個似乎無法解決的問題呢?我想分享我使用CSS 3D效果的經歷,那是第一次用於實際專案中,以此來激勵你接受挑戰。
那是平常的一天,當Eugene( CreativePeople的經理)寫信給我的時候。他寄給我一個視訊,說他正在為一個新專案開發一個概念,而且想知道我是否可能開發一個像視訊裡那樣的東西。
這是一個繞著一個軸旋轉的3D物體(準確地說是個立方體)。對於用CSS 3D工作我已經有一些經驗了,於是我的腦海裡開始形成一個解決方案。我Google搜尋了像“CSS 3D cube”這樣的關鍵詞來確認我的想法,隨後我回復Eugene說我可以。
Eugene下一個問題是問我是否願意承擔這個專案?我喜歡複雜的任務,所以我不能拒絕。在這一刻,我還沒有意識到我正陷入其中,但我無法確定是否可以完成。
理解軸(字面翻譯是磨斧)
提醒下這個axes不是戰斧,而是 數軸的意思,正如我們在學校學到的三維直角座標系一樣的軸線。維基百科定義:
直角座標系是一個兩兩垂直有序的三元線行成的三維空間,三條軸都有一個單獨的單位長度並且每一條軸線有一個方向。
下面的圖片展示了在Web瀏覽器中怎樣確定軸線方向。
一個以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-3d
的transform-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)上編寫。
我有什麼資料呢?首先,我有標註文字位置的起點和終點,或者簡單說來就是從側面中心位置到上邊和下邊的偏移量
。其次,我有它旋轉的角度
。
我花了幾個小時試圖定義一個公式。隨後,我恍然大悟。這就是我的靈感:
正弦餘弦函式圖 (圖片: 維基共享資源)。
在正弦餘弦函式的幫助下,通過角度我輕鬆地計算出了每個標註的偏移。這是我提出的公式:
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實踐中我收穫了寶貴的經驗,並且發現了許多有意思的屬性。更重要的是,我懂得了一個人不應該輕言放棄,很可能你會找到一個方法來完成。
我希望你喜歡我的故事,也希望你現在做好準備迎接新的挑戰!