最近我在Codepen看到一個這樣子的加載示例。使用CSS完成的具有漸變褪色倒影的、旋轉的3D條形塊。每一個條形塊使用了一個元素,之後進行複製,這些元素形成倒影,最後添加漸變進行覆蓋形成漸變褪色效果。這聽起來有點像用你的左腳趾從背後抓右耳朵般不切實際。更不用說漸變覆蓋方法形成褪色效果在非平面背景色不起作用了。是不是有更好的方法使用CSS可以實現這種效果呢?
回答無非是:「肯定」和「否定」。「肯定」是可以實現,以及「否定」是不可以實現。不幸的是,儘管該代碼可以使用預處理器進行壓縮(外界獲取的一個循環中生成的代碼會大大減少),如果想要跨瀏覽器獲取最佳的效果,為了倒影複製所有的條形塊以及使用漸變覆蓋生成褪色效果的方法仍然是最好的,而不是使用canvas。
這篇文章主要探索創建倒影的方法,說明「大部分」的解決方案以及跨瀏覽器產生的令人頭疼的問題,最後討論我自己的想法。
案例的基本設置在創建倒影之前,讓我們看看如何在大部分的瀏覽器中創建位置以及條形塊的陰影。
創建條形塊首先,創建一個包裹.loader元素,在內部設置10個.bar元素。
書寫多行同樣的代碼是一件令人頭痛的事情,這種情況下我們會使用一種預處理器。這裡使用了Haml,當然你可以使用其它的。
將所有的元素從視口的中間進行絕對定位。在大多數的情況下,設置top:50%,但是這裡,為了之後的方便起見設置bottom:50%。
決定條形塊的width以及height,這裡方便看見它們,設置一個background:
我們想要這些條形塊的底部邊緣和視口的中線(將視口分為相等的兩部分)相吻合,並且我們已經設置了bottom:50。
現在,所有的條形塊已經一個接一個堆疊在一起,其左邊緣正對將視口均分兩份(左側以及右側)的垂直線,其底部邊緣在將視口均分為兩份(上部以及下部)的水平線上。
定位條形塊現在需要對他們進行定位,使得第一個條形塊的左邊緣以及最後一個條形塊的右邊緣位於視口的水平中間位置。這個距離總是條形塊數量的一半($n)乘以條形塊的width($bar-w)。最初的演示使用的是vanilla CSS,但是這裡為了減少代碼的數量使用了Sass。
意味著所有的條形塊從現在的位置開始,我們需要左移.5 * $n * $bar-w。左側是x軸的負方向,意味著需要在前面添加一個負號(-)。margin-left的值為-.5 * $n * $bar-w。
第二個條形塊(若索引基於0,這裡為1)向右(x軸的正方向)是一個寬度。margin-left的值為-.5 * $n * $bar-w + $bar-w。
第三個條形塊(若索引基於0,這裡為2)向右(x軸的正方向)是一個寬度。margin-left的值為-.5 * $n * $bar-w + 2*$bar-w。
最後一個條形塊(若索引基於0,這裡為$n - 1)向右(x軸的正方向)是一個寬度。margin-left的值為-.5 * $n * $bar-w + ($n - 1) * $bar-w。
一般情況下,如果我們考慮到當前的條形塊$i是基於0的,margin-left的值為-.5 * $n * $bar-w + $i * $bar-w,簡寫為($i - .5 * $n) * $bar-w。
這裡可以使用一個Sass循環進行條形塊的定位:
可以設置一個box-shadow,方便我們看見每一個條形塊的起始位置:
條形塊的顏色設置條形塊的顏色最左邊的顏色為深藍(#1e3f57),最右邊為淺藍色(#63a6c1)。這裡可以使用一個Sass mix()函數。第一個參數為淺藍,第二個為深藍以及第三個(稱為相對重量)為結果mix中所包含的淺藍的權重(為%)。
對於第一個條形塊,數額為0% - 結果中淺藍的0%,也就是深藍。
對於最後一個條形塊,數額為100% - 最後結果中(也就是深藍的0%)淺藍的100%,也就是淺藍。
對於其餘的條形塊,我們需要獲取均勻分布的中間值。如果一種有$n個條形塊,第一個條形塊為0%,最後一個為100%,其餘的$n - 1需要均分獲取值。
一般來說,索引為$i的條形塊的相對權重為$i * 100% / ($n -1),這時也就要添加如下的代碼:
現在條形塊看起來如下所示:
探索倒影的各種實現方法Webkit 瀏覽器: -webkit-box-reflectOh,不是吧!這不是一個標準的屬性!我不知道這不是一個標準屬性。當第一次接觸Safari,我甚至沒有聽說過CSS。但是,對於Webkit瀏覽器,它起作用並且運行的很好。它簡單易用並且在不支持的瀏覽器中也不會產生什麼副作用,只是不顯示倒影而已。
接下來看看它是如何工作的,這裡分為了三個步驟:
下面的互動演示說明了這一點(點擊方向,偏移量,漸變角度以及結束時的alpha和偏移進行對比觀察):
注意linear-gradient()可以由多個終止值或者可以被radial-gradient()所替換。
在我們的示例中,我的第一反應是在.loader元素上進行添加:
但是,在Webkit瀏覽器中沒有任何反應!
發生了什麼?我們對所有的元素進行了絕對定位,並且在包含條形塊的.loader元素中沒有設置任何明確的尺寸。這導致了產生了一個0x0的元素 - 即width和height為0。
現在設置一些明確的尺寸,height為條形塊的高($bar-h),width比所有條形塊的寬度($n * $bar-w)大一丟丟。這裡也暫時設置一個box-shadow便於觀察:
當需要高亮一個元素的邊緣時,相對於outline而言我更偏愛box-shadow,因為當子元素溢出父元素時,outline在不同的瀏覽器中表現不一致。
box-shadow以及outline在Webkit瀏覽器以及Edge(top) vs. Firefox(bottom) - 當子元素溢出父元素時,outline表現不一致
添加上上述代碼,我們在Webkit瀏覽器中得到的效果如下:
如果不是Webkit瀏覽器,得到的效果如下:
指定.loader元素明確的大小後,在Chrome中的呈現結果截圖
現在我們可以看見加載的邊緣以及一些倒影,但是指定的位置有點不準確。我們想要加載位於水平中間線的底部,所以我們將width向左移動了一半。同時,我們想要條形塊的底部和它們的父元素相吻合,這裡設置bottom:0:
現在定位問題解決了嗎,得到的結果如下:
Firefox: element() + mask使用element()創建倒影element()函數(還處於實驗中,目前僅Firefox實現還需添加-moz-前綴)給我們提供了一個圖像值,如真正的圖像所具有的值(如background,border-image,但是在對於偽內容的值不起作用 - 見bug 1285811)。如果想要看到顯示的background或者border-image這裡需要獲取一個參數也就是元素的id選擇器。這允許我們做一些邪惡的事情,如使用控制的圖像作為背景。但是如果想要在Firefox中獲取一個元素的倒影就會變得十分簡單。
關於element()函數你需要了解的是它不是遞歸函數 - 我們不可以使用函數本身作為它們自身的背景。這使得在一個加載創建一個偽倒影變得十分安全,不需要額外的元素。
好吧,讓我們看看如何實現。首先,在.loader元素上設置一個id(假設為明顯的loader)。改變樣式,從Webkit示例中的最後一個demo中開始。之後在.loader元素上添加::after偽元素,進行絕對定位並進行完全覆蓋。
我們還設置了一些額外的臨時樣式,僅僅是為了看出偽元素的邊界以及最終形式的方向,我們希望其倒置:
現在需要思考如何使用::after偽元素對抗它們的底部邊緣。為了做到這一點,我們使用了scaleY()轉換以及一個transform-origin。下面的互動演示說明了對於不同的scale因素以及transform起點如何進行定向縮放:
注意,scale因素以及transform-origin可能超出demo演示的限制。
在我們的示例中,需要一個scaleY(-1)以及偽元素::after底部邊緣線的transform-origin:
使用scale(-1)轉換以及一個適當的transform-origin形成倒影
添加如下代碼並且對使用element()函數的偽元素::after設置#loader作為背景。
注意,因為一些特殊原因選擇器使用了.loader,並且element()函數參數使用了#loader,因為其需要一個id選擇器。
添加如上代碼後的效果(僅僅為Firefox瀏覽器)為:
可能不是所有的人都使用Firefox瀏覽器,這裡有一個顯示的效果截圖:
Firefox下使用element()函數實現的倒影效果
使用遮罩實現倒影的漸變褪色這裡和在Webkit示例中使用的方法一樣:使用一個遮罩。在那種情況下,遮罩是-webkit-box-reflect值的一個組件。在這個示例中,我們討論一下CSS mask屬性 - 其值為一個SVG引用:
#fader元素是一個SVG遮罩元素包含著一個矩形元件。
使用Haml可以壓縮為:
然而,當我們添加了如下代碼後,我們的倒影消失了 - 在Firefox可以得以驗證:
這是因為默認情況下,SVG圖形有一個實心黑色的fill,完全不透明,並且默認情況下遮罩為亮度遮罩。所以我們需要做的給矩形一個fill - 一個SVG linearGradient的引用。
SVG linearGradient被定義為兩點之間使用x1,y1,x2以及y2屬性進行指定。x1和y1是漸變線的起點坐標(0%),x2和y2是重點坐標(100%)。如果這些被丟失,默認取值分別為0%,0%,100%以及0%。代表為應用元素上(默認,gradientUnits為objectBoundingBox),線的左上(0% 0%)以及右上(100% 0%),也就是默認情況下,漸變是從左向右。
但是在我們的示例中,想要實現漸變始於top終於bottom,所以我們將x2的值由100%改為0%,y2的值由0%改為100%。這就會導致應用元素的漸變矢量變為由左上(0% 0%)到左下(0% 100%)。
在linearGradient元素中,我們至少兩個top元素。需要三個參數:offset,stop-color以及stop-opacity。
offset需要一個a%值,一般在0%到100%之間,類似於CSS 漸變示例。也可以設置一個數字值,一般在0和1之間。
stop-color理論上可以為一個關鍵字,hex,rgb(),hsl()或者hsla()值。實際中,Safari瀏覽器不支持半透明值,所以如果想要在漸變中設置半透明,我們還應該依賴第三個屬性...
stop-opacity。取值一般在0(完全透明)和1(完全可視)之間。
我們需要牢記,在應用了漸變遮罩的的偽加載已經通過scale(-1)轉換產生了倒影。也就是說,漸變遮罩的底部已經可視。所以我們需要將漸變從頭部(目視向下)的完全透明漸變為底部(目視向上)的alpha值.7。
由於我們的漸變為從上向下,第一個top為完全透明。
添加線性漸變後,在Firefox中實現的效果如下:
Codepen顯示如下:
SVG中漸變問題在我們的示例中,事情十分簡單因為我們的遮罩漸變是垂直的。但是如果漸變不是垂直,或者為水平或者說不是從一個角落到另外一個角落,又會怎麼樣呢?如果我們想要在某一個角度實現漸變呢?
SVG 漸變還具有一個稱為gradientTransform的屬性,可以將被x1,x2,y1以及y2屬性定義的漸變線旋轉。大家可能會想在某一個角度重現CSS漸變是容易實現的。但是...並不是如此!
讓我們思考如何實現從金黃到深紅的漸變。為了使其清晰,在50%設置一個急劇轉變。最初,我們獲取一個0deg的CSS漸變版本。也就是漸變從底部(金黃)的0%到頂部(深紅)的100%。CSS代碼如下:
如果你不知道CSS線性漸變的工作原理,你可以參考Patrick Brosset書寫的文章。
這時呈現的效果如下:
為了使用SVG重現這個效果,我們創建了一個漸變,y1為100%,y2為0%,以及具有相同值的x1和x2(為了方便起見這裡設置為0)。這就意味著漸變是垂直的,從底部到頂部。同時,這裡將兩個top偏移量設置為50%。
編者說: 我向Ana詢問,為什麼這裡轉向使用了Jade,回答:開始使用Haml是為了避免變量循環。這路轉向使用Jade是因為允許變量以及相關計算。
現在漸變還沒有旋轉,gradientTransform屬性在這一點的值為(0 .5 .5)。後面的兩個值指定漸變旋轉點的坐標,和應用漸變的元素相關。0 0表示左上角,1 1表示右下角,.5 .5表示中間。可以在下面的示例中清晰的分辯:
如果想要漸變從左向右,在CSS漸變示例中將角度由0deg改為90deg。
為了使SVG漸變獲取相同的效果,我們將gradientTransform的值改為rotate(90 .5 .5) :
到現在為止,一切都很順利。看起來使用SVG複製CSS漸變也不是一件很令人頭痛的事情。但是,讓我們換一個角度。如下所示的互動示例中,左邊為CSS漸變,右邊為SVG實現的版本。紫色線為漸變線,並且應該為金黃到深紅之間的突變線。在CSS以及SVG示例中,拖動滑塊改變漸變角度。我們會發現在90deg倍數,會出現錯誤。
如上所示,當值不是90deg的倍數時,我們沒有得到相同的效果。如果將漸變設置為方形,值一致。意味著我們可以將漸變設置在一個較大的方形元素上 - 之後可以應用到實際元素中。但是,後果會導致我們使用使用element()以及mask創建漸變倒影變得些許困難。
Edge: 也全為SVG?很不幸,以上所提及的方法在Edge均不起作用。不用手動進行複製並且可以在Edge中運行,目前可以想到的方法就是使用SVG創建加載。其優點為跨瀏覽器兼容。
基本上,我們使用viewBox創建一個SVG元素,其0 0點在中間是固定的。定義一個條形塊,其底部邊緣在x軸上,左側邊緣在y軸上。之後在#loader中,多次複製(通過SVGuse元素)這個條形塊直到滿足需求。關於這些條形塊的定位處理方式和之前一樣。
現在已經創建了這些條形塊,想要對SVG元素進行定位,這裡使用flexbox。同樣和之前一樣,使這些條形塊產生漸變色,SCSS代碼如下:
複製#loader群組(再次使用use元素)。使用scale(1 -1)函數並且應用一個mask產生倒影,和之前偽元素方式一致。默認情況下,SVG元素相對於SVG畫布的0, 0點,這裡定位於加載的底部邊緣比較有利於形成倒影,並且不需要設置transform-origin。
因為Edge中不支持CSS transforms,這裡使用了transform屬性 - 如果你想要使用它,需要等到瀏覽器的支持!
最後一步就是使用mask進行褪色處理。和之前的方法一樣,所以就不再次重複。
動畫最初案例中的CSS動畫是相對簡單的一個,在3D中旋轉條形塊:
這裡所有的條形塊的動畫一樣:
animation: bar 3s cubic-bezier(.81, .04, .4, .7) infinite;
我們只是在每一個條形塊循環中添加了一個不同的延遲:
animation-delay: $i*50ms;
既然在3D中旋轉條形塊,需要在loader元素中添加一個perspective。
但是需要使用-webkit-box-reflect方法,以便在Webkit瀏覽器中起作用。
使用-webkit-box-reflect方法在Chrome中的最終效果
同樣的,方便觀察我們添加了一個圖像背景。
我們也想要其在Firefox中正常工作。然而,添加動畫代碼後,效果好像有些不一致:
Firefox中使用element()以及mask的最初動畫版本效果記錄
第一個問題是倒影越出偽元素的邊界。可以通過增加loader元素的尺寸來修正此問題:
$loader-w: ($n + 1) * $bar-w + $bar-h;
但是,對於其餘的兩個問題我們就會變得有些無能為力 - 當條形塊3D旋轉時,倒影更新不足夠平滑轉換;以及perspective特性會造成條形塊的消失(查看bug 1282312)。
那麼SVG解決方案呢?不幸的是,我們上述所設置的關鍵幀用於CSS 3D轉換動畫。在Edge中還不支持用於CSS 轉換的SVG元素 - 這也就是之前為什麼我們依賴transform元素創建倒影效果。但是transform特性的值在2D中是十分嚴格的,不使用JavaScript難以對其添加動畫效果(這時,你可能會想到SMIT,這是標記所厭惡的,並且在Edge / IE中也不支持,在Chrome中也已經被棄用)。
所以目前現在還沒有絕對的解決方案可以使其在所有的瀏覽器中工作。我們所能做的就是擁有兩個loader元素,每一個的條形塊的數量一樣:
條形塊的樣式和之前的一樣,在第二個loader元素使用scale(-1)轉換:
現在需要對倒影進行褪色處理。不幸的是,為了得到跨瀏覽器的效果,我們不可以在第二個loader元素上應用mask。Edge目前也不支持HTML元素的遮罩處理,但是你可以加快它的實現進程。
我們可以做的就是使用漸變覆蓋第二個loader(此處為產生倒影的那一個)。注意這意味著我們不可以使用圖片背景。一個固定的背景在這個示例中是十分有限的,你可以選擇使用漸變背景。使用::after偽元素實現並使其足夠大以便旋轉時可以進行覆蓋。
我們需要一個更好地跨瀏覽器解決方案。我相信,可以通過不進行複製產生倒影。我們也可以不選擇SVG解決方案(也會產生負面的影響)進行倒影的褪色處理並且可以擁有一個背景圖片。
哪一個解決方案相對好一點呢?-webkit-box-reflect還是element() + mask?我不知道。我更喜歡可以跨瀏覽器兼容的解決方案。
曾經我一度認為我不需要額外的元素產生倒影效果,儘管:reflection偽元素聽起來不錯。可以自由的在不同的方向創建多次倒影,並且以不同的方式進行倒影轉換,如在3D中進行旋轉或者傾斜。使用element()方法可以實現,這也是為什麼我喜歡這種方法的原因。這裡不提及使用SVG實現遮罩,那意味著會產生更多複雜的影響。
另一方面,面對這巨大的外界壓力,你可能不能花費大量的時間熟悉文章方法背後的複雜性。有時你僅僅需要一個簡單的方法得到簡單的效果。
擴展閱讀本文根據@ANA TUDOR的《The State of CSS Reflections》所譯,整個譯文帶有我們自己的理解與思想,如果譯得不好或有不對之處還請同行朋友指點。如需轉載此譯文,需註明英文出處:https://css-tricks.com/state-css-reflections/。
文章涉及到圖片和代碼,如果展示不全給您帶來不好的閱讀體驗,歡迎點擊文章底部的 閱讀全文。如果您覺得小站的內容對您的工作或學習有所幫助,歡迎關注此公眾號。
W3cplus.com
————————————
記述前端那些事,引領web前沿
長按二維碼,關注W3cplus
▼