1. 程式人生 > >前端-CSS(x、y、z)座標的含義,以及變換的使用

前端-CSS(x、y、z)座標的含義,以及變換的使用

css裡的3d理念

使用css3的3d transform,就可以在平面的網頁裡新增炫酷的三維視覺效果,這很令人愉悅。

需要注意的是,3d transform只是css的一部分,它並不是一個三維引擎(3d engine)。三維引擎一般是這樣的(遊戲引擎Unity3D):

Unity3D

包括JavaScript 3D庫three.js在內,簡單來說,它們這些可以稱為三維引擎的,都會包括:

  • 獨立的三維座標系統。
  • 幾何圖形和材質貼圖。
  • 光照和攝像機。

3d transform和它們相比起來,是缺少這些內容的。畢竟,css的關注點是網頁樣式,而不是建立虛擬空間。

儘管3d transform的三維空間能力有所不足,但它仍然可以創建出很棒的三維效果。這就需要我們開發者來用心了。

在下面的內容開始之前,如果你還對perspective等3d transform相關的css屬性完全不瞭解,可以閱讀我以前寫的css三維變換的文章

3d transform的座標系統

我們很熟悉的網頁是平面的,一個DOM元素,比如一個<div>,它會有一個初始座標系initial coordinate system):

初始座標系

每一個DOM元素都有一個這樣的初始座標系。其中,原點位於元素的左上角,z軸指向觀察者(也就是螢幕外的我們)。初始座標系的z軸並不算是三維空間,而是像z-index那樣作為參照,決定網頁元素的繪製順序,繪製順序靠後的元素將覆蓋繪製順序靠前的。

在使用transform的時候,情況則有所不同。transform所參照的並不是初始座標系,而是一個新的座標系:

變換座標系

transform所用的這個座標系,相比初始座標系,x、y、z軸的指向都不變,只是原點位置移動到了元素的正中心。如果想要改變這個座標系的原點位置,使用transform-origintransform-origin的預設值是50% 50%,因此,預設情況下,transform座標系的原點位於元素中心。

transform的順序

我們都可能像transform: rotateY(45deg) translateX(100px);這樣使用多個變換函式。這種時候,需要意識到變換函式的順序。這是因為,每一個變換函式不僅改變了元素,同時也會改變和元素關聯的transform座標系,當變換函式依次執行時,後一個變換函式總是基於前一個變換後的新transform座標系。

例如,下面一個包含兩個變換函式的transform的效果(gif):

transform順序1

如果交換這兩個變換函式的順序,是這樣的效果:

transform順序2

可以看到,由於座標系會隨著每一次變換髮生改變,因此不同順序的情況下,元素最終的位置也不同。

對此還有一種解釋,即變換函式是通過數學上的矩陣乘法運算完成的,而矩陣的乘法是不滿足交換律的。任意座標空間內的變換函式或者變換函式的組合,都可以轉換為一個矩陣(還有一個矩陣小工具可以幫你做這個轉換)。

建立三維物體

前面已經提到,3d transform並沒有像三維引擎那樣為你建立三維場景提供全面的資源。因此,就以建立一個三維物體來說,我們只能利用網頁目前已有的內容,自己想辦法。

在網頁裡,你並不能直接定義一系列座標為(x, y, z)的空間中的點,然後基於這些點來生成三維圖形。網頁裡有的,是平面圖形。不管是<div>還是其他html元素,它們都是平的,沒有厚度,像紙片一樣。但紙片就可以搭東西,所以,一個DOM元素用作三維物體的一個“面”,把這些“面”有序地組織起來,得到的就是三維物體了!

事實上,在三維引擎裡,三維物體也不是實體,它們都是由一系列平面(多邊形)所圍成的(並可以在平面上新增紋理和貼圖)。

正方體

現在來做一個正方體,現在先不用考慮perspective。正方體有六個面,然後需要用一個元素來裝這六個面,所以html是:

  1. <divclass="cube">
  2. <divclass="surface surface-1">1</div>
  3. <divclass="surface surface-2">2</div>
  4. <divclass="surface surface-3">3</div>
  5. <divclass="surface surface-4">4</div>
  6. <divclass="surface surface-5">5</div>
  7. <divclass="surface surface-6">6</div>
  8. </div>

對應的css是(邊長120px,省略瀏覽器私有字首,後文同):

  1. .cube{
  2. position: absolute;
  3. transform-style: preserve-3d;
  4. }
  5. .cube .surface{
  6. position: absolute;
  7. width: 120px;
  8. height: 120px;
  9. border: 1px solid #ccc;
  10. background: rgba(255,255,255,0.8);
  11. box-shadow: inset 0 0 20px rgba(0,0,0,0.2);
  12. line-height: 120px;
  13. text-align: center;
  14. color: #333;
  15. font-size: 100px;
  16. }
  17. .cube .surface-1 {
  18. transform: translateZ(60px);
  19. }
  20. .cube .surface-2 {
  21. transform: rotateY(90deg) translateZ(60px);
  22. }
  23. .cube .surface-3 {
  24. transform: rotateX(90deg) translateZ(60px);
  25. }
  26. .cube .surface-4 {
  27. transform: rotateY(180deg) translateZ(60px);
  28. }
  29. .cube .surface-5 {
  30. transform: rotateY(-90deg) translateZ(60px);
  31. }
  32. .cube .surface-6 {
  33. transform: rotateX(-90deg) translateZ(60px);
  34. }

其中,transform-style: preserve-3d;保證所有子元素都處於同一個三維空間(這裡是三維渲染上下文3D rendering context)內,也就是告訴瀏覽器你是想用這些元素做一個三維場景,而不僅僅只是要單個元素的簡單三維效果。

position: absolute;是一個習慣做法,因為三維物體並不符合一般平面網頁內容的排版,所以我們會比較多地希望它不要佔據佈局空間。

6個面位置都不一樣,但卻都有translateZ(60px);,你已經知道這是因為巧妙搭配了在它之前的變換函式。

一旦構成正方體的6個div.surface的位置確定後,就可以操作它們的父元素div.cube來整體移動、旋轉這個正方體。

將三維物體新增到可視區域

只是有了這樣的一個三維物體,我們就可以說有了一個三維場景了。但是,三維場景和平面圖不同,比如這個正方體,我從不同的位置,不同的角度去觀察它,我眼裡看到的都是不一樣的:

從不同角度觀察三維物體

那麼,這樣的三維場景要如何呈現在平面的網頁裡呢?

這就是三維引擎裡的攝像機的概念來源了。就像電影裡表現同一個場景會用不同的鏡頭那樣,我們需要定義場景裡的攝像機來生成最終呈現在螢幕裡的二維畫面。比如three.js裡的攝像機是這樣的感覺:

three.js的攝像機基本引數

這個圖裡標明的是攝像機定義時使用的引數,其中包括視野範圍,影象寬高比等。可以感受到還是有很多內容的。

相比之下,網頁裡的三維場景攝像機就弱多了,你需要用的是perspectiveperspective-origin

perspective定義攝像機(也就是作為觀眾的我們)到螢幕的距離,perspective-origin定義攝像機觀察到的畫面中的滅點(vanishing point)的位置。雖然它們並不能方便地讓你直接定義攝像機的位置和觀察角度等,但只要適當地應用它們,是可以一定程度上控制攝像機的畫面效果的。

控制攝像機畫面

網頁裡的攝像機一般是這樣用的:

  1. <divclass="camera">
  2. <divclass="cube1"></div>
  3. <divclass="cube2"></div>
  4. <!-- more 3d objects... -->
  5. </div>
  1. .camera{
  2. position: relative;
  3. perspective: 1200px;
  4. perspective-origin: 50% 50%;
  5. transform-style: preserve-3d;
  6. }

在網頁裡,無論你搭建了怎樣的三維場景,只要你希望它顯示出來,就應該像這樣把構成場景的三維物體都放在一個容器元素裡,然後為容器元素新增攝像機屬性(perspectiveperspective-origin)。

此外,還需要注意新增transform-style: preserve-3d;以保證多個三維物體都位於同一空間(這樣才有三維引擎的味道,對吧?)。

下面這個場景裡有三個正方體,然後攝影師正在做彈跳練習(限支援3d transform的瀏覽器):

1 1 1 1 1 1 2 2 2 2 2 2 3 3 3 3 3 3

這段動作的動畫程式碼是這樣:

  1. .camera{
  2. animation: cameraMove 2s ease-out infinite alternate both;
  3. }
  4. @keyframes cameraMove{
  5. 0%{
  6. perspective-origin: 50% 180px;
  7. }
  8. 100%{
  9. perspective-origin: 50% -200px;
  10. }
  11. }

可以看出,perspective-origin雖然是指三維透視的滅點的位置,但它的確和我們理解的攝像機的位置是緊密關聯的。如果攝像機在空間裡的位置是(x, y, z)的話,perspective-origin的兩個值有一點像指定xy的感覺。這裡只說“有一點像”,是因為滅點位置和攝像機的位置畢竟是不同的概念,這可能還需要多看一些三維空間來體會。

那麼,在上面的例子中,攝影師不只是這樣跳起來,而是想要向更深處前進,應該怎麼做呢?

答案是,在網頁裡,你不能這樣移動攝像機,你需要換一個思路,參照相對運動的關係,改為讓整個三維場景向你移動。不過,說到這裡,前面提到的攝像機的另一個屬性,perspective,為什麼它不行呢?

perspective代表攝像機距離螢幕的距離,看上去和z軸深度非常近似。但是,它並不等同於攝像機的z座標位置(perspective還只能取正值),而是會影響攝像機本身的其他屬性。下面用這個圖說明perspective的值變化的效果(修改自w3c的配圖):

perspective的值變化的效果

圖中d1d2分別表示兩個不同的perspective的值,其中d2小於d1。然後,你會驚奇地發現,一個原本位於螢幕之後(z座標為負值)的物體,竟然是隨著“走近”而變得更小了!顯然,這不符合我們在三維空間裡運動的基本感受。其原因是,網頁的三維投影平面是固定的,perspective在改變攝像機的位置的同時,也同時改變了攝像機本身的其他屬性(類似前面的three.js的攝像機那張圖裡的各種引數)。

所以,一般來說,perspective應維持一個固定的值。想要用3d transform做出在三維空間裡自由移動的效果(就像各種3d遊戲),應該通過相對運動的方法實現。

其他補充

transform對佈局的影響

transform影響的是視覺渲染,而不是佈局。因此,除以下情況外,transform不會影響到佈局:

生成滾動條而影響佈局的例子

這個因為overflow生成滾動條從而影響佈局的反例,也發生於position: relative;再進行偏移的情況。

left、top等常規屬性對3d transform的影響

相對於transform的translate3d()這類改變空間位置的變換函式,原來css裡就有的定位屬性lefttop似乎會讓情況變得很複雜。

對此,有一個比較推薦的分析方式:就三維空間的位置而言,常規屬性lefttop,甚至margin-left等,是先生效的,它們的效果其實只有一個,就是改變元素的初始位置,從而改變元素的transform-origin的那個原點位置,然後三維空間的transform是後生效的,它會再基於前面的transform-origin繼續改變位置。

perspective-origin和transform-origin的區別

現在你已經瞭解到,perspective-origin是一個攝像機的屬性,定義的是透視畫面的滅點,而transform-origin是任意元素都有的,定義的是的元素的transform座標系的原點。

結語

本文是我對目前網頁裡用3d transform建立三維空間的總結,其中混入了一些三維引擎的知識,並對比著來一一說明。感覺3d transform還是有自己比較明確的定位的,雖然和三維引擎相比很簡陋,但它已經足夠用來為網頁新增吸引人的三維效果。

如果你也想過用3d transform做稍複雜的三維空間,希望本文能有所幫助。

yu

css裡的3d理念

使用css3的3d transform,就可以在平面的網頁裡新增炫酷的三維視覺效果,這很令人愉悅。

需要注意的是,3d transform只是css的一部分,它並不是一個三維引擎(3d engine)。三維引擎一般是這樣的(遊戲引擎Unity3D):

Unity3D

包括JavaScript 3D庫three.js在內,簡單來說,它們這些可以稱為三維引擎的,都會包括:

  • 獨立的三維座標系統。
  • 幾何圖形和材質貼圖。
  • 光照和攝像機。

3d transform和它們相比起來,是缺少這些內容的。畢竟,css的關注點是網頁樣式,而不是建立虛擬空間。

儘管3d transform的三維空間能力有所不足,但它仍然可以創建出很棒的三維效果。這就需要我們開發者來用心了。

在下面的內容開始之前,如果你還對perspective等3d transform相關的css屬性完全不瞭解,可以閱讀我以前寫的css三維變換的文章

3d transform的座標系統

我們很熟悉的網頁是平面的,一個DOM元素,比如一個<div>,它會有一個初始座標系initial coordinate system):

初始座標系

每一個DOM元素都有一個這樣的初始座標系。其中,原點位於元素的左上角,z軸指向觀察者(也就是螢幕外的我們)。初始座標系的z軸並不算是三維空間,而是像z-index那樣作為參照,決定網頁元素的繪製順序,繪製順序靠後的元素將覆蓋繪製順序靠前的。

在使用transform的時候,情況則有所不同。transform所參照的並不是初始座標系,而是一個新的座標系:

變換座標系

transform所用的這個座標系,相比初始座標系,x、y、z軸的指向都不變,只是原點位置移動到了元素的正中心。如果想要改變這個座標系的原點位置,使用transform-origintransform-origin的預設值是50% 50%,因此,預設情況下,transform座標系的原點位於元素中心。

transform的順序

我們都可能像transform: rotateY(45deg) translateX(100px);這樣使用多個變換函式。這種時候,需要意識到變換函式的順序。這是因為,每一個變換函式不僅改變了元素,同時也會改變和元素關聯的transform座標系,當變換函式依次執行時,後一個變換函式總是基於前一個變換後的新transform座標系。

例如,下面一個包含兩個變換函式的transform的效果(gif):

transform順序1

如果交換這兩個變換函式的順序,是這樣的效果:

transform順序2

可以看到,由於座標系會隨著每一次變換髮生改變,因此不同順序的情況下,元素最終的位置也不同。

對此還有一種解釋,即變換函式是通過數學上的矩陣乘法運算完成的,而矩陣的乘法是不滿足交換律的。任意座標空間內的變換函式或者變換函式的組合,都可以轉換為一個矩陣(還有一個矩陣小工具可以幫你做這個轉換)。

建立三維物體

前面已經提到,3d transform並沒有像三維引擎那樣為你建立三維場景提供全面的資源。因此,就以建立一個三維物體來說,我們只能利用網頁目前已有的內容,自己想辦法。

在網頁裡,你並不能直接定義一系列座標為(x, y, z)的空間中的點,然後基於這些點來生成三維圖形。網頁裡有的,是平面圖形。不管是<div>還是其他html元素,它們都是平的,沒有厚度,像紙片一樣。但紙片就可以搭東西,所以,一個DOM元素用作三維物體的一個“面”,把這些“面”有序地組織起來,得到的就是三維物體了!

事實上,在三維引擎裡,三維物體也不是實體,它們都是由一系列平面(多邊形)所圍成的(並可以在平面上新增紋理和貼圖)。

正方體

現在來做一個正方體,現在先不用考慮perspective。正方體有六個面,然後需要用一個元素來裝這六個面,所以html是:

  1. <divclass="cube">
  2. <divclass="surface surface-1">1</div>
  3. <divclass="surface surface-2">2</div>
  4. <divclass="surface surface-3">3</div>
  5. <divclass="surface surface-4">4</div>
  6. <divclass="surface surface-5">5</div>
  7. <divclass="surface surface-6">6</div>
  8. </div>

對應的css是(邊長120px,省略瀏覽器私有字首,後文同):

  1. .cube{
  2. position: absolute;
  3. transform-style: preserve-3d;
  4. }
  5. .cube .surface{
  6. position: absolute;
  7. width: 120px;
  8. height: 120px;
  9. border: 1px solid #ccc;
  10. background: rgba(255,255,255,0.8);
  11. box-shadow: inset 0 0 20px rgba(0,0,0,0.2);
  12. line-height: 120px;
  13. text-align: center;
  14. color: #333;
  15. font-size: 100px;
  16. }
  17. .cube .surface-1 {
  18. transform: translateZ(60px);
  19. }
  20. .cube .surface-2 {
  21. transform: rotateY(90deg) translateZ(60px);
  22. }
  23. .cube .surface-3 {
  24. transform: rotateX(90deg) translateZ(60px);
  25. }
  26. .cube .surface-4 {
  27. transform: rotateY(180deg) translateZ(60px);
  28. }
  29. .cube .surface-5 {
  30. transform: rotateY(-90deg) translateZ(60px);
  31. }
  32. .cube .surface-6 {
  33. transform: rotateX(-90deg) translateZ(60px);
  34. }

其中,transform-style: preserve-3d;保證所有子元素都處於同一個三維空間(這裡是三維渲染上下文3D rendering context)內,也就是告訴瀏覽器你是想用這些元素做一個三維場景,而不僅僅只是要單個元素的簡單三維效果。

position: absolute;是一個習慣做法,因為三維物體並不符合一般平面網頁內容的排版,所以我們會比較多地希望它不要佔據佈局空間。

6個面位置都不一樣,但卻都有translateZ(60px);,你已經知道這是因為巧妙搭配了在它之前的變換函式。

一旦構成正方體的6個div.surface的位置確定後,就可以操作它們的父元素div.cube來整體移動、旋轉這個正方體。

將三維物體新增到可視區域

只是有了這樣的一個三維物體,我們就可以說有了一個三維場景了。但是,三維場景和平面圖不同,比如這個正方體,我從不同的位置,不同的角度去觀察它,我眼裡看到的都是不一樣的:

從不同角度觀察三維物體

那麼,這樣的三維場景要如何呈現在平面的網頁裡呢?

這就是三維引擎裡的攝像機的概念來源了。就像電影裡表現同一個場景會用不同的鏡頭那樣,我們需要定義場景裡的攝像機來生成最終呈現在螢幕裡的二維畫面。比如three.js裡的攝像機是這樣的感覺:

three.js的攝像機基本引數

這個圖裡標明的是攝像機定義時使用的引數,其中包括視野範圍,影象寬高比等。可以感受到還是有很多內容的。

相比之下,網頁裡的三維場景攝像機就弱多了,你需要用的是perspectiveperspective-origin

perspective定義攝像機(也就是作為觀眾的我們)到螢幕的距離,perspective-origin定義攝像機觀察到的畫面中的滅點(vanishing point)的位置。雖然它們並不能方便地讓你直接定義攝像機的位置和觀察角度等,但只要適當地應用它們,是可以一定程度上控制攝像機的畫面效果的。

控制攝像機畫面

網頁裡的攝像機一般是這樣用的:

  1. <divclass="camera">
  2. <divclass="cube1"></div>
  3. <divclass="cube2"></div>
  4. <!-- more 3d objects... -->
  5. </div>
  1. .camera{
  2. position: relative;
  3. perspective: 1200px;
  4. perspective-origin: 50% 50%;
  5. transform-style: preserve-3d;
  6. }

在網頁裡,無論你搭建了怎樣的三維場景,只要你希望它顯示出來,就應該像這樣把構成場景的三維物體都放在一個容器元素裡,然後為容器元素新增攝像機屬性(perspectiveperspective-origin)。

此外,還需要注意新增transform-style: preserve-3d;以保證多個三維物體都位於同一空間(這樣才有三維引擎的味道,對吧?)。

下面這個場景裡有三個正方體,然後攝影師正在做彈跳練習(限支援3d transform的瀏覽器):

1 1 1 1 1 1 2 2 2 2 2 2 3 3 3 3 3 3

這段動作的動畫程式碼是這樣:

  1. .camera{
  2. animation: cameraMove 2s ease-out infinite alternate both;
  3. }
  4. @keyframes cameraMove{
  5. 0%{
  6. perspective-origin: 50% 180px;
  7. }
  8. 100%{
  9. perspective-origin: 50% -200px;
  10. }
  11. }

可以看出,perspective-origin雖然是指三維透視的滅點的位置,但它的確和我們理解的攝像機的位置是緊密關聯的。如果攝像機在空間裡的位置是(x, y, z)的話,perspective-origin的兩個值有一點像指定xy的感覺。這裡只說“有一點像”,是因為滅點位置和攝像機的位置畢竟是不同的概念,這可能還需要多看一些三維空間來體會。

那麼,在上面的例子中,攝影師不只是這樣跳起來,而是想要向更深處前進,應該怎麼做呢?

答案是,在網頁裡,你不能這樣移動攝像機,你需要換一個思路,參照相對運動的關係,改為讓整個三維場景向你移動。不過,說到這裡,前面提到的攝像機的另一個屬性,perspective,為什麼它不行呢?

perspective代表攝像機距離螢幕的距離,看上去和z軸深度非常近似。但是,它並不等同於攝像機的z座標位置(perspective還只能取正值),而是會影響攝像機本身的其他屬性。下面用這個圖說明perspective的值變化的效果(修改自w3c的配圖):

perspective的值變化的效果

圖中d1d2分別表示兩個不同的perspective的值,其中d2小於d1。然後,你會驚奇地發現