我一直喜歡3D。我也開始使用CSS 3D Transform,而且瀏覽器對它的支持度越來越好。但給我的感覺,使用Transform就是用來創建2D圖形,並且使用旋轉和位移可以創建一些3D圖形。但在實際使用的時候,還是越到了不少的麻煩,而且這些麻煩出乎我的意料。我想或許大家也同樣遇到過這樣的問題,為了大家在使用CSS 3D Transform能避免這些麻煩,我把我碰到的與在大家分享一下。
3D渲染上下文
我清楚的記得一個晚上遇到一個麻煩,而且這個麻煩引起我的好奇心,於是我想親自寫一個測試用例來試試看,看看瀏覽器如何處理平面的交叉。該測試用例只包含了兩個平面元素:
<div class='plane'></div>
<div class='plane'></div>
它們尺寸大小相同,並且使用絕對定位,讓元素在屏幕中居中( 水平垂直居中 ),並且給他們設置了個背景色,讓它們在屏幕中可見:
$dim: 40vmin;
.plane {
position: absolute;
top: 50%;
left: 50%;
margin: -.5*$dim;
width: $dim;
height: $dim;
background: #ee8c25;
}
把
body
元素當作一個場景,並且設置了
perspective
(
視角
),讓視角覆蓋整個視窗。視角的值越大,元素似乎看起來離自已越遠,看起來越小,反之越大。
body {
margin: 0;
height: 100vh;
perspective: 40em;
}
測試示例實際上就是測試兩個平面相交,所以使用Transform中的
rotateY()
做了一個
Y
軸旋轉,並且設置了一個不同的背景顏色:
.plane:last-child {
transform: rotateY(60deg);
background: #d14730;
}
結果是令人失望的。瀏覽器似乎並沒有正確的處理好兩個平面交叉:
事實上是我錯了,造成這個現象是我的代碼導致的。我應該做的是讓兩個平面在
3D上下文渲染
。3D上下文渲染和
層疊上下文渲染
還是不同的。就像如果它們不在同一個層疊上下文中,我們不能使用
z-index
來改變元素在
z
軸的順序。同樣的,如果不在同一個3D渲染範圍內,3D Transform同樣不能改變元素的順序,讓元素交叉。
為了確保這兩個平面是在相同的3D渲染環境,最簡單的方法就是把它們放在同一個容器中:
<div class='assembly'>
<div class='plane'></div>
<div class='plane'></div>
</div>
同樣讓容器元素通過絕對定位,讓它放置在容器中間,同時給它設置一個
transform-style:preserve-3d
:
div {
position: absolute;
}
.assembly {
top: 50%;
left: 50%;
transform-style: preserve-3d;
}
這樣就解決了這個問題:
如果你使用Firefox查看上面的示例,你看到的效果是兩個平面並沒有交叉,這或許是Firefox獨有的特性吧,但你使用Webkit內核瀏覽器或者Edge瀏覽器看到的效果是我們想要的效果。
上圖演示是前面示例的效果,兩個平面沒有交叉。
現在你可能會感到非常奇怪,為什麽不給這兩個平面添加一個容器,不在場景中設置
transform-style:preserve-3d
就不能正常工作(在我們的示例中是
body
元素)?那麽,在這種特殊情況下,如果在最初的示例中增加這個規則(在
boyd
元素是直接添加
transform-style:preserve-3d
),它就能正常工作(除非你是在Firefox瀏覽器查看這效果,正如前面所說,Firefox對於3D的順序和交叉還是有問題的)。
實際上,如果我們想要在網頁上使用3D,我們的場景可能不會直接是
body
元素,可能會在場景中添加其他屬性,這些可能會影響頁面的性能。
打斷3D(造成壓扁)
比方說,我們的場景是頁面中的一個
div
元素,而且有其他的內容圍繞著這個場景
div
:
在第二個元素上多添加了幾個
transform
的屬性,使之更加明顯,看上去有些部分在場景外。上面示例看到的效果是我們不想要的。我們希望添加一些屬性讓文本更好的閱讀。
overflow
最初讓我想到的解決方案是在場景中添加一個
overflow:hidden
。然而,這樣做是能讓文本更好的閱讀,但3D交叉效果又不正常了。
即使在場景中設置了
preserve-3d
,只要設置了
overflow
的值,就算是
visible
也會使效果變得像
transform-style
設置了
flat
(壓平)一樣。因此,多添加一個容器可能會讓我寫更多一點代碼,但能讓我們繞開這些麻煩。
這就是為什麽我們要把一切元素都放置在一個獨立的元素中,把這個容器當作場景,即使該元素不使用3D Transform。例如下面這個示例:
所有旋轉的六邊形列都放在
.helix
元素內:
<div class='helix'>
<div class='col'>
<!-- all the hexagons inside a column -->
</div>
<!-- the other columns -->
</div>
除了保證整個組件絕對定位在視窗中間的樣式之外,
.helix
元素沒有任何其他樣式(除非是繼承下來的樣式),並且所有的列都在同一個3D渲染範圍內:
div {
position: absolute;
transform-style: preserve-3d;
}
.helix {
top: 50%;
left: 50%;
}
在場景中設置了
overflow:hidden
(這個示例
body
元素就是場景),這樣做是因為六邊形不依賴於視窗的大小,我不知道它們是否會延伸到容器外而導致滾動條的出現,這種現象是我不想看到的。
我承認我不止一次碰到這樣的現象,從中吸引了相關教訓。我保守起見,為了不讓溢出出現在這裏,直接使用
overflow:hidden
,這樣能讓溢出看起來不明顯。
transform-style:preserve-3d
告訴瀏覽器
設置了3D Transform的子元素不應該在他們的父元素內拍平(元素上設置了
transform-style:preserve-3d
)。因此,就算是直覺,場景中設置了
overflow:hidden
,防止其子元素打破他們的父容器,也不會讓3D元素在場景內拍平。
但有時一個3D Transform子元素仍然可以在其父容器是平面的。比如說下面這種情況,我們一張具有兩面的卡片:
<div class='card'>
<div class='face'>front</div>
<div class='face'>back</div>
</div>
通過絕對定位讓所有元素在場景中(這個示例場景指的是
body
元素)水平垂直居中,並且給這兩個卡片具有相同的大小尺寸。為了讓這們在相同的空間,給卡片設置
transform-style:preserve-3d
。為了讓背面可見,設置
backface-visibility:hidden
,並且在第二張卡片上設置
rotate
,讓其沿著垂直軸(
Y
軸)旋轉半圈(
.5turn
):
$dim: 40vmin;
div {
position: absolute;
width: $dim;
height: $dim;
}
.card {
top: 50%;
left: 50%;
margin: -.5*$dim;
transform-style: preserve-3d;
}
.face {
backface-visibility: hidden;
background: #ee8c25;
&:last-child {
transform: rotateY(.5turn);
background: #d14730;
}
}
示例的效果如下所示:
兩張卡片在他的父容器內仍然是平的,只不過第二張卡片繞著它的垂直軸(
Y
軸)旋轉了半圈。它的朝向是相反的方式,但仍然是在同一平面上。到目前為止,這一切看上去都是正常的。
好,現在我想這兩個卡片不是矩形的。給它們一個
border-radius: 50%
。但看起來沒有任何變化:
接下來在
.card
上設置
overflow:hidden
,效果就正常了:
哎呀,這打破了我們的3D卡片。既然我們無法做到這一點,我們就在
.face
上設置:
.face {
border-radius: 50%;
}
在這種情況之下,解決這個問題的方法比打破3D卡這個問題更簡單。但是,如果我們想要的是另一種形狀,比如說一個正八邊形,正八邊形還是很容易實現的,比如采用兩個元素(或者元素和偽元素的配合):
<div class='octagon'>
<div class='inner'></div>
</div>
給這兩個元素設置相同的尺寸,並且
.inner
元素設置
rotate
的值為
45deg
,給他們設置一個背景色,然後在
.octagon
元素上設置
overflow:hidden
,就可以看到一個正八邊形:
$dim: 65vmin;
div {
width: $dim;
height: $dim;
}
.octagon {
overflow: hidden;
}
.inner {
transform: rotate(45deg);
background: #ee8c25;
}
你看到的八邊形效果如下所示:
如果你對如何制作正多邊形感興趣,建議你閱讀《 Sass繪制多邊形 》和《 單一 div 的正多邊形變換 ( 純 CSS ) 》。
如果希望在正多邊形中添加文本呢?
<div class='octagon'>
<div class='inner'>octagon</div>
</div>
你將看到的效果是這樣,不盡人意:
造成這個現象是因為裁角的時候把文本也裁剪掉了。為了讓文本能正常顯示,給它設置一個
text-align:center
,並且設置一個
line-height
的值等於
.octagon
的高度(或者
.inner
),讓文本垂直居中:
.inner {
font: 10vmin/ #{$dim} sans-serif;
text-align: center;
}
現在看起來好多了,但文本仍然是旋轉的,因為
.inner
元素設置了一個
rotate(45deg)
:
為了解決這個問題,只需要在
.octagon
元素上增加一個
rotate
,其旋轉的角度值和
.inner
的旋轉值一樣,只是旋轉方向剛好與
.inner
元素相反,所以是一個負值:
.octagon {
transform: rotate(-45deg);
}
這下,文本在正八邊形中的文本顯示正常了:
現在讓我們看看,如果我們想一正八邊形的卡片,又將如何應用它。我們不能直接在
.card
上直接運用
overflow:hidden
(讓它在
.octagon
元素上作用,同時兩個面
.inner
又會是什麽樣)。因為在卡片上設置了
overflow:hidden
樣式,根據前面介紹的內容,這樣就會打破3D空間,讓兩個頁不在同一個3D渲染環境。
替代方案是,需要把這些規則用在
.octagon
元素,並且使用它們的偽元素來做卡片的兩個面:
.face {
overflow: hidden;
transform: rotate(45deg);
backface-visibility: hidden;
&:before {
left: 0;
transform: rotate(-45deg);
background: #ee8c25;
content: 'front';
}
&:last-child {
transform: rotateY(.5turn) rotate(45deg);
&:before {
background: #d14730;
content: 'back'
}
}
}
最後看到的效果就是我們想要的效果:
clip-path
能引起類似的問題還有另一個屬性
clip-path
。回到上面卡片的示例,我們不能在
.card
元素上直接使用
clip-path
來繪制三角形,因為我們需要一個3D Ttransform的子元素,也就是第二個面。我們應該用在卡片的面上:
.face {
clip-path: polygon(100% 50%, 0 0, 0 100%);
}
註意
:
clip-path
屬性在Webkit內核瀏覽器下還是需要添加
-webkit-
前綴。對於Firefox(47+)瀏覽器需要通過
about:config
將
layout.css.clip-path-shapes.enabled
設置為
true
,另外Edge是不支持這個屬性的(但你可以在這裏
進行投票
,讓Edge早日能支持這個屬性)。
如果您從未接觸過
clip-path
這個屬性,建議你先閱讀《 CSS的clip-path
》一文進行了解。
上面的代碼看到的效果應該是這樣的:
雖然不存在3D的問題,但效果看起來真的很別扭。如果從正面觀察卡片,三角形的頂角是朝右的,按理說,後面應該是朝左的。但效果並不是我們所期望的,反面也朝右了。要解決這個問題,那麽需要為不同的面設置不同的路徑。
clip-path
繪制正面的三角形超右,繪制反正的三角形超左。
.face:last-child {
clip-path: polygon(0 50%, 100% 0, 100% 100%);
}
下面看到的效果才是我們想要的:
註意:
還需要修改
text-align
的值:正面的默認值為
left
,反正的就需要設置為
right
。
另外,我們還可以在反面是使用
scaleX(-1)
來實現。如查你想進一步的了解
scale
的工作機制,可以看看下面的示例:
將上面介紹的原理運用到我們前面介紹的DEMO中:
.face:last-child {
transform: rotateY(.5turn) scaleX(-1);
}
效果如下:
這樣看起來三角的方向是對了,但文字又出問題了。這意味著我們實際上要把文本放在背景元素的偽元素上,並且在
.face
元素上做一個反轉。
scale
的反轉就是設置另一個
scale
是
1/f
。在我們這個示例中,
f
就是
-1
。也就是說在偽元素上設置
scale
的值為
1/-1=-1
,就可以讓文本看起來正常:
.face:last-child:before {
transform: scaleX(-1);
background: #d14730;
text-align: right;
content: 'back';
}
最後的效果如下:
如果
mask
設置非
none
值時,也會致使
transform-style
變為
flat
,就像
overflow
和
clip-path
設置了
visible
和
none
以外的值一樣。
opacity
這是一個意想不到的問題。相對而言,這也是
規範中相對較新的一個變化
,如果在3D渲染環境下設置的
opacity
值小於
1
,效果就像是在層疊上下文一樣(失去3D渲染上下文,就像前面所說的3D拍平)。這效果並不是在所有瀏覽器中都會發生的,比如在Edge、Safari和Brave下正常,而chrome、Firefox和Opera看到的效果就是拍平後的效果。
請看下面的示例,一組立方體在3D空間內同時旋轉:
結構很簡單,在
.assembly
容器內有很多個(這個示例是有
20
個)
.cube
元素,而且每個
.cube
有
6
個面。
<div class='assembly'>
<div class='cube'>
<div class='cube__face'></div>
<!-- five more cube faces -->
</div>
<!-- more cubes, each with 6 faces -->
</div>
現在我們說,想要的立方體是半透明的。那麽我們這樣做,能不能做到呢?
.cube {
opacity: .5;
}
這樣一來,就算在
.cube
上設置了
transform-style:preserve-3d
,也會變成
flat
的效果,就像
.cube
在它的父容器裏被拍平了。現在只是Chrome、Opera和Firefox,但是在將來,所有瀏覽器都會是這樣:
在Brave、Edge和Safari中,設置
opacity
值小於
1
時,立方體沒有拍平。
在Chrome、Firefox、Opera瀏覽器中,結果是
.cube
被拍平了。
我們不能把
opacity:0.5
設置在已經設置了
transform-style:preserve-3d
的
.assembly
上。其效果和前面所說的將一樣:
我們把
opacity:0.5
設置到
.cube
的每個面上,會不會引起同樣的問題:
也可以把
opacity
設置在場景元素上(下面的示例是設置了
body
元素上),但需要註意,它也會影響場景的
background
或者偽元素。它也不會使個別立方體或面單獨具有半透明度,只能整體一樣,而且也沒辦法讓不同的立方體有不同的透明值。
對比一下,在各個面上設置
opacity
和在場景中設置
opacity
效果差異:
上圖的效果是在每個面設置
opacity:0.5
的效果。
上圖的效果是在場景中設置
opacity:0.5
的效果。
filter
這個也讓我感到驚奇,雖然不像
opacity
,但它在所有瀏覽器都是一樣的。接著再拿3D立方體舉例。通過
hue-rotate()
函數,讓立方體在旋轉的時候每個面都有不同的
hue
值。在
.cube
或者
.assembly
上設置
filter
的值不是
none
時,3D立方體就將會被拍平。
$n: 20; // number of cubes
@for $i from 0 to $n {
$angle: random(360)*1deg;
.cube:nth-child(#{$i + 1}) {
filter: hue-rotate($angle);
}
}
filter
在Webkit內核瀏覽器中仍需要添加-webkit-
前綴。
給每個面隨機的色相有正常工作,但每個3D立方體還是被拍平了:
這個問題的解決方案是把
filter
設置在立方體的每個面上:
$n: 20; // number of cubes
@for $i from 0 to $n {
$angle: random(360)*1deg;
.cube:nth-child(#{$i + 1}) .cube__face {
filter: hue-rotate($angle);
}
}
這樣一來,3D立方體的每個面的色相是隨機的,而且也在3D渲染上下文中,沒有被拍平:
我們也不能把
filter
設置在
.assembly
上面。比方說,希望所有的立方體都具有模糊效果,你就為了方便在
.assembly
上設置
filter
:
.assembly {
filter: blur(4px);
}
其結果就是整個都被拍平了,平面變得模糊。Edge是個例外,直接一切都消失了。
我們可以做的就是盡量在立方體的每個面上使用
blur()
,雖然結果不會完全一樣。而且就算是這樣做,在不同的瀏覽器內核是渲染也將不一致,
看上去就像是有Bug一樣
。
我們可以嘗試在場景中設置
blur()
,盡管在各瀏覽器看上去好像還是個Bug(在Chrome和Firefox有時會閃爍,各個面消失;Edge完全不顯示任何東西):
我比較好奇的是下面這個簡單示例,在場景中也有設置了
filter
的
blur()
效果,但是在Blink內核的瀏覽器和Edge中效果很好,只是在Firefox下有問題。
總體而言,在3D渲染上下文中使用
filter
似乎問題很多,所以在使用的時候需要謹慎。
說了這麽久的
filter
,如果你從未接觸過的話,建議你先 點擊這裏 了解filter
相關的東西,然後在回過頭去閱讀前面的內容。
mix-blend-mode
比如說我們一個
.container
元素,這個元素有一個多彩的
background
。在這個元素內有一個設置了
background-image
的
.mover
元素,並且
.mover
元素有一個改變位置的動畫效果和設置
mix-blend-mode:overlay
的樣式。看到效果將是,
.mover
(這裏稱之為草莓,因為背景圖片是草莓)移動位置,其位置在
.container
元素不同元素塊上時,呈現給用戶的效果將會不一樣:
混合模式目前在Edge中不支持,所以在Edge中將看不到任何效果,但你可以
在這裏投票
,讓其早日也能支持混合模式。但是有一點需要註意,不能直接使用
body
或者
html
來替代
.container
元素,因為在Blink內核的瀏覽器中還
存在bug
。這個bug在
body
或
html
替代
container
時,
.mover
運用混合模式時會出問題。但在Firefox和Safari中不存在這個現象。
好吧,上面看到的是2D平面的,但我們要聊的是3D的,那我們需要
.mover
是一個帶有圖片的3D立方體,在3D空間中旋轉。
到目前為止,在沒有設置混合模式之一,這一切都很好。接下來在立方體上設置
mix-blend-mode:overlay
。問題來了,3D渲染打破了,立方體又被拍平了:
由於我們需要在立方體上使用3D Transform來制作動畫,而且他們的子元素都需要使用3D Transform,所以需要在立方體(
.cube
)上設置
transform-style
的值為
preserve-3d
。但我們還需要在
.cube
上設置
mix-blend-mode:overlay
,這樣問題就來了,在
.cube
上設置
mix-blend-mode:overlay
致使
transform-style
的值變為
flat
,那麽所有立方體就被拍平了。
嘗試把
mix-blend-mode:overlay
設置在立方體的各個面上,但這問題依舊還是存在:
要解決這個問題,需要在
.container
和
.mover
容器之間再添加一個
.scene
容器,並且在這個元素上設置
perspective
和
mix-blend-mode
。
這似乎是解決了一切問題!
本文根據 @ANA TUDOR 的《 Things to Watch Out for When Working with CSS 3D 》所譯,整個譯文帶有我們自己的理解與思想,如果譯得不好或有不對之處還請同行朋友指點。如需轉載此譯文,需註明英文出處: https://css-tricks.com/things-watch-working-css-3d/ 。
大漠
常用昵稱“大漠”,W3CPlus創始人,目前就職於手淘。對HTML5、CSS3和Sass等前端腳本語言有非常深入的認識和豐富的實踐經驗,尤其專註對CSS3的研究,是國內最早研究和使用CSS3技術的一批人。CSS3、Sass和Drupal中國布道者。2014年出版《 圖解CSS3:核心技術與案例實戰 》。如需轉載,煩請註明出處: http://www.w3cplus.com/css3/things-watch-working-css-3d.html
譯文 CSS3 Transform CSS 3D Web 3DTags: background absolute position 水平垂直 瀏覽器
文章來源: