CSS 3D應該註意的事項

分類:IT技術 時間:2016-10-19

我一直喜歡3D。我也開始使用CSS 3D Transform,而且瀏覽器對它的支持度越來越好。但給我的感覺,使用Transform就是用來創建2D圖形,並且使用旋轉和位移可以創建一些3D圖形。但在實際使用的時候,還是越到了不少的麻煩,而且這些麻煩出乎我的意料。我想或許大家也同樣遇到過這樣的問題,為了大家在使用CSS 3D Transform能避免這些麻煩,我把我碰到的與在大家分享一下。

3D渲染上下文

我清楚的記得一個晚上遇到一個麻煩,而且這個麻煩引起我的好奇心,於是我想親自寫一個測試用例來試試看,看看瀏覽器如何處理平面的交叉。該測試用例只包含了兩個平面元素:

<div class='plane'></div>
<div class='plane'></div>
ADVERTISEMENT

它們尺寸大小相同,並且使用絕對定位,讓元素在屏幕中居中( 水平垂直居中 ),並且給他們設置了個背景色,讓它們在屏幕中可見:

$dim: 40vmin;
.plane {
  position: absolute;
  top: 50%;
  left: 50%;
  margin: -.5*$dim;
  width: $dim;
  height: $dim;
  background: #ee8c25;
}

body 元素當作一個場景,並且設置了 perspective ( 視角 ),讓視角覆蓋整個視窗。視角的值越大,元素似乎看起來離自已越遠,看起來越小,反之越大。

ADVERTISEMENT
body {
  margin: 0;
  height: 100vh;
  perspective: 40em;
}

測試示例實際上就是測試兩個平面相交,所以使用Transform中的 rotateY() 做了一個 Y 軸旋轉,並且設置了一個不同的背景顏色:

.plane:last-child {
  transform: rotateY(60deg);
  background: #d14730;
}

結果是令人失望的。瀏覽器似乎並沒有正確的處理好兩個平面交叉:

事實上是我錯了,造成這個現象是我的代碼導致的。我應該做的是讓兩個平面在 3D上下文渲染 。3D上下文渲染和 層疊上下文渲染 還是不同的。就像如果它們不在同一個層疊上下文中,我們不能使用 z-index

ADVERTISEMENT
來改變元素在 z 軸的順序。同樣的,如果不在同一個3D渲染範圍內,3D Transform同樣不能改變元素的順序,讓元素交叉。

為了確保這兩個平面是在相同的3D渲染環境,最簡單的方法就是把它們放在同一個容器中:

<div class='assembly'>
    <div class='plane'></div>
    <div class='plane'></div>   
</div>

同樣讓容器元素通過絕對定位,讓它放置在容器中間,同時給它設置一個 transform-style:preserve-3d

ADVERTISEMENT

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 3D
Tags: background absolute position 水平垂直 瀏覽器

文章來源: