在Web網頁中主要是以矩形分布的。而平面媒體則傾向于更多不同的形狀。造成這種差異的原因是因為缺少合適的工具去實現我們平面媒體中的內容。這也就造成了很多設計師的創意發揮,就算是有創意,前端實現也將付出巨大的開發成本。
雖然CSS Shapes Module Level 1(CSS形狀模塊標準1)的規范出現,可以打破矩形設計的限制。但仍需要一些不規則的圖形。而早前實現一些不規則的圖形,都需借助其它的元素功能,比如CSS繪制圖形,很多時候就依賴于偽元素,或多個元素。如此一來,CSS Shapes依舊無法發揮其強大的功能,讓我們的Web打破常規的矩形布局。不過值得慶幸的是,CSS的clip-path
出現,它可以幫助我們繪制很多特殊的圖形(不規則的圖形),比如:
那么這篇文章,我們就一起來了解這個屬性。
基本概念
clip-path
從單詞"clip path"的直譯上來說,表示的就是裁剪路徑。既然有裁剪,咱們就來了解這里面的幾個簡單的概念。
裁剪就是從某樣東西剪切一塊。比如說,我們在圖片元素上,根據需要,剪切一部分需要留下的區域。而在整個裁剪中,將會碰到兩個相關的概念:裁剪路徑(Clipping Path)和裁剪區域(Clipping Region)。
裁剪路徑是我們用來裁剪元素的路徑,它標記了我們需要裁剪的區域。它可以是個簡單的形狀(比如Web中常見的矩形),也可以是一個復雜的多邊形(不規則的多邊形)。
裁剪區域是裁剪路徑閉合后所包含的全部區域。
這樣一來,元素分為兩部分,裁剪區域和裁剪區域外。瀏覽器會裁剪掉裁剪區域以外的區域,不僅是背景及其它類似的內容,也包括
border
、text-shadow
等。更贊的是,瀏覽器不會捕獲元素裁剪區域以外的 hover
、click
等事件。
即使如今一些特定元素不受長方形限制,但實際上元素周圍的內容還是會認為元素是原始形狀(長方形)的,并按此進行文檔流的布局。要想使周圍元素根據元素裁剪后的形狀進行布局,可以使用 shape-outside
屬性。有關于shape-outside
相關詳細的介紹,可以閱讀有關于CSS Shapes相關的教程,這里不做過多闡述。
clip-path語法
W3C官方規范提供的clip-path
語法:
clip-path: | [ || ] | none
其默認值是none
。另外簡單介紹clip-path
幾個屬性值:
-
clip-source
: 可以是內、外部的SVG的<clippath></clippath>
元素的URL引用 -
basic-shape
: 使用一些基本的形狀函數創建的一個形狀。主要包括circle()
、ellipse()
、inset()
和polygon()
。具體的說明可以看CSS Shapes中有關于說明。另外在CSS Shapes 101一文中也有詳細介紹。 -
geometry-box
: 是可選參數。此參數和basic-shape
函數一起使用時,可以為basic-shape
的裁剪工作提供參考盒子。如果geometry-box
由自身指定,那么它會使用指定盒子形狀作為裁剪的路徑,包括任何(由border-radius
提供的)的角的形狀。
開始使用clip-path
在開始使用clip-path繪制圖形,或者說裁剪圖形之前,有兩點需要大家注意:
使用clip-path要從同一個方向繪制,如果順時針繪制就一律順時針,逆時針就一律逆時針,因為polygon是一個連續線段,若線段彼此有交集,裁剪區域就會有相減的情況發生,當然如果你特意需要這樣的效果除外。
如果繪制時采用比例的方式繪制,長寬就必須要先行設定,不然有可能繪制出來的長寬和我們想像的就會有差距,使用像素繪制就不會有這樣的現象。
先來看一個使用polygon()函數繪制的示例:
img {
clip-path: polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%);
}
這段代碼會將所有的圖片裁剪為菱形。但是為什么圖片會被裁剪為菱形而不是梯形或平行四邊形之類的呢?這主要取決于函數頂點的值。下圖將說明一切:
每個點的第一個坐標值決定了它在
x
軸上的位置,第二個坐標值指定了它在 y
軸的位置,所有點是順時針繪制的。比如菱形最右邊的點,它位于 y
軸下方一半處,所以它的 y
坐標是 50%
。同時這個點位于 x
軸的最右側,所以它的 x
坐標是 100%
。其它點的坐標同理可得。
最后效果如下所示:
記得以前CSS繪制圖形總得束手束腳,而且還得想法設法,使用clip-path
繪制什么六邊形、八邊形、五角形、心形等,都不再是難事了。OXXO.STUDIO有一篇文章《運用 clip-path 的純 CSS 形狀變換》詳細介紹了這些圖形是如何繪制的。當然除此之外,在線的CSS clip-path maker提供了很多不規則的圖形案例:
利用 geometry-box 裁剪元素
在具體使用geometry-box來裁剪元素之前,對geometry-box做一下相關的了解。
geometry-box可以是shape-box、fill、stroke或者view-box。其中shape-box應用于HTML元素,它具有四種值:margin-box、border-box、padding-box和content-box。
來看個簡單的示例:
.clip-me {
clip-path: polygon(10% 20%, 20% 30%, 50% 80%) margin-box;
margin: 10%;
}
在上例中,元素的 margin-box
會作為參考,來決定裁剪點的實際位置。點(10%,10%
)是 margin-box
的左上角,所以 clip-path
的定位會根據此點進行計算。
其實shape-box
和CSS Shapes中的引用框概念非常類似,有關于這方面的介紹,可以花點時間閱讀《理解CSS Shapes的引用框》一文。
如果geometry-box
和basic-shape
一起使用,可以引用basic-shape
提供的引用框。其作用和shape-outside
屬性類似,更多的細節可以看看shape-outside
的屬性介紹。
如果geometry-box
由自身指定,那么它會使用指定盒子形狀作為裁剪的路徑,包括任何(由border-radius
提供的)的角的形狀。
除了shape-box
值,還可以運用SVG元素上,它具有另外三個值:fill
、stroke
和view-box
。
瀏覽器兼容性
IE 和 Edge 不支持這個屬性。Firefox 僅部分支持 clip-path (它只支持 url() 語法)。但是 47 以上的版本,激活 Firefox 的 layout.css.clip-path-shapes.enabled選項就可以支持這個屬性了。
Chrome、Safari 和 Opera 需要使用 -webkit- 前綴支持此屬性。不幸的是,它們還不支持外部的 SVG 形狀。更多瀏覽器支持性信息如下:
實例操練:
<!--晴-->
<div class="weather sunny"></div>
<!--陰-->
<div class="weather cloudy"></div>
<!--雨-->
<div class="weather rainy"></div>
<!--雪-->
<div class="weather snowy"></div>
涉及到的關鍵CSS3屬性:
transform:用于移位、旋轉、縮放效果
box-shadow:利用投影實現圖像的復制(關鍵!)
clip-path:基于繪制的形狀對元素進行遮罩處理
animation:設置元素的動畫
1.基礎背景
設置了區域的寬高、背景色和圓角效果。
.weather {
position: relative;
display: inline-block;
width: 180px;
height: 240px;
background: #23b7e5;
border-radius: 8px;
}
2.晴天
晴天圖標由兩個元素組成:太陽和內六角形陽光。
:before、:after 兩個偽元素可以在元素內部分別“添加”一個元素,正好都利用上了。
2.1繪制太陽
首先,用 :before實現太陽。
.sunny:before {
content: "";
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 60px;
height: 60px;
background: #F6D963;
border-radius: 50%;
box-shadow: 0 0 20px #ff0;
z-index: 2;
}
content用來生成一個元素。
position、top、left、transform用來實現中心居中。
box-shadow實現外發光效果,這只是box-shadow最基本最常用的使用方式。
3.2繪制內六角形
用 :after實現內六角形。
實現的關鍵就是使用遮罩。通過clip-path繪制一個內六角形。這就變成了一個簡單的初中幾何問題。
內六角形由兩個等邊三角形拼合而成。
合并之后,我們可以把整體劃分為若干個完全相同的小等邊三角形。
在垂直方向做個輔助線,連接中間頂部和底部兩點。不難發現,“垂直方向的最大長度”要大于“水平方向的最大長度”。
設小等邊三角形的邊長為1,以內六角形中心為坐標原點,可以計算出每個點的坐標,如下:
為了使用clip-path的百分比定位來繪制圖像,下一步需要把長度坐標轉換為百分比坐標。
設垂直方向最大長度為100%,仍以內六角形中心為坐標原點,每個點的坐標值轉換如下:
由于clip-path繪制原點是在左上角,x軸右側為正值,y軸下方為正值。需要做下坐標系轉換。即:
新x軸坐標值 = 舊x軸坐標值 + 50%
新y軸坐標值 = (舊y軸坐標值 - 50%) * -1
使用clip-path的polygon方法繪制內六角形,坐標已通過上面的步驟計算出來了。
樣式代碼如下:
.sunny:after {
content: "";
position: absolute;
top: 50%;
left: 50%;
margin: -45px 0 0 -45px;
width: 90px;
height: 90px;
background: #FFEB3B;
clip-path: polygon(
50% 0%,
64.43% 25%,
93.3% 25%,
78.87% 50%,
93.3% 75%,
64.43% 75%,
50% 100%,
35.57% 75%,
6.7% 75%,
21.13% 50%,
6.7% 25%,
35.57% 25%);
z-index: 1;
animation: sunScale 2s linear infinite;
}
@keyframes sunScale {
0% {
transform: scale(1);
}
50% {
transform: scale(1.1);
}
100% {
transform: scale(1);
}
}
※注:safari需要將clip-path改為-webkit-clip-path。由于代碼太占篇幅,這里就不重復寫兩遍了。
實現原理就是通過clip-path繪制了一個內六角形遮罩,把黃顏色背景通過遮罩變成了最終的內六角形。
animation通過關鍵幀動畫實現了“放大縮小”交替動效。
最終效果:
3.陰天
觀察圖形發現,有兩個云朵:前面的白云和后面的烏云。貌似需要分別用 :before和 :after實現。如果這樣做的話,后續章節的雨天和雪天的雨雪元素就沒有多余的偽元素可用了。所以只能用一個偽元素實現兩朵云。 這里就用到了box-shadow的“影分身”了!
由于后續章節的雨天和雪天都復用了云的樣式,所以寫在一起了,代碼如下:
.cloudy:before,
.rainy:before,
.snowy:before {
content: "";
position: absolute;
top: 50%;
left: 25%;
transform: translate(-50%, -50%);
width: 36px;
height: 36px;
background: #fff;
border-radius: 50%;
z-index: 2;
}
真實的元素(真身)就是一個圓。通過box-shodow來把投影作為“分身”。
先來看看box-shadow的屬性:
box-shadow: h-shadow v-shadow blur spread color inset;
參數詳解:
h-shadow: 陰影的水平偏移量。
v-shadow: 陰影的垂直偏移量。
blur: 模糊距離(就是漸變的距離,設為0就沒有漸變)。
spread: 投影的尺寸,通過這個控制“影分身”的大小。
color: 投影顏色,通過這個實現后方的烏云。
inset: 改為內陰影。這里用不到。
先復制一個影分身試試:
box-shadow: #fff 22px -15px 0 6px;
繼續復制多個影分身,帶全部影分身的完整代碼如下:
.cloudy:before,
.rainy:before,
.snowy:before {
content: "";
position: absolute;
top: 50%;
left: 25%;
transform: translate(-50%, -50%);
width: 36px;
height: 36px;
background: #fff;
border-radius: 50%;
box-shadow:
#fff 22px -15px 0 6px,
#fff 57px -6px 0 2px,
#fff 87px 4px 0 -4px,
#fff 33px 6px 0 6px,
#fff 61px 6px 0 2px,
#ccc 29px -23px 0 6px,
#ccc 64px -14px 0 2px,
#ccc 94px -4px 0 -4px;
z-index: 2;
}
五個分身的白圓(#fff),三個分身的灰圓(#ccc)拼成了兩朵云。
再給云朵加上“上下浮動”的動效:
.cloudy:before {
animation: cloudMove 2s linear infinite;
}
@keyframes cloudMove {
0% {
transform: translate(-50%, -50%);
}
50% {
transform: translate(-50%, -60%);
}
100% {
transform: translate(-50%, -50%);
}
}
4.雨天
云朵的代碼直接復用第4章的陰天。這里使用 :after 偽元素實現雨滴。
先實現一個雨滴(為方便觀看,暫時隱藏云朵):
.rainy:after {
content: "";
position: absolute;
top:50%;
left: 25%;
width: 4px;
height: 14px;
background: #fff;
border-radius: 2px;
}
然后通過box-shadow“影分身”:
.rainy:after {
content: "";
position: absolute;
top:50%;
left: 25%;
width: 4px;
height: 14px;
background: #fff;
border-radius: 2px;
+ box-shadow:
+ #fff 25px -10px 0,
+ #fff 50px 0 0,
+ #fff 75px -10px 0,
+ #fff 0 25px 0,
+ #fff 25px 15px 0,
+ #fff 50px 25px 0,
+ #fff 75px 15px 0,
+ #fff 0 50px 0,
+ #fff 25px 40px 0,
+ #fff 50px 50px 0,
+ #fff 75px 40px 0;
}
再加入下雨的移動動效,修改如下:
.rainy:after {
...(略)
+ animation: rainDrop 2s linear infinite;
}
+ @keyframes rainDrop {
+ 0% {
+ transform: translate(0, 0) rotate(10deg);
+ }
+ 100% {
+ transform: translate(-4px, 24px) rotate(10deg);
+ box-shadow:
+ #fff 25px -10px 0,
+ #fff 50px 0 0,
+ #fff 75px -10px 0,
+ #fff 0 25px 0,
+ #fff 25px 15px 0,
+ #fff 50px 25px 0,
+ #fff 75px 15px 0,
+ rgba(255, 255, 255, 0) 0 50px 0,
+ rgba(255, 255, 255, 0) 25px 40px 0,
+ rgba(255, 255, 255, 0) 50px 50px 0,
+ rgba(255, 255, 255, 0) 75px 40px 0;
+ }
+ }
動畫添加了10度的旋轉,讓雨滴傾斜,以及垂直方向的移動。
這里的關鍵就是:雖然本質是垂直移動,但為了看上去是“循環”效果,需要將最下面的雨滴進行透明漸變,同時調節X和Y軸的值,讓最終位置正好跟初始位置重合,就不會顯得“斷開”。
我們生成的是三行雨滴,第一行被云朵擋住了,實際能看到的是下面兩行。在第一行移動到第二行位置的時候,原第三行已經透明看不見了,正好與初始狀態一樣,實現了無縫循環拼接。
5.雪天
雪天與雨天的區別就是把雨滴換成圓形,取消旋轉角度。 代碼如下:
.snowy:after {
content: "";
position: absolute;
top:50%;
left: 25%;
width: 8px;
height: 8px;
background: #fff;
border-radius: 50%;
box-shadow:
#fff 25px -10px 0,
#fff 50px 0 0,
#fff 75px -10px 0,
#fff 0 25px 0,
#fff 25px 15px 0,
#fff 50px 25px 0,
#fff 75px 15px 0,
#fff 0 50px 0,
#fff 25px 40px 0,
#fff 50px 50px 0,
#fff 75px 40px 0;
animation: snowDrop 2s linear infinite;
}
@keyframes snowDrop {
0% {
transform: translateY(0);
}
100% {
transform: translateY(25px);
box-shadow:
#fff 25px -10px 0,
#fff 50px 0 0,
#fff 75px -10px 0,
#fff 0 25px 0,
#fff 25px 15px 0,
#fff 50px 25px 0,
#fff 75px 15px 0,
rgba(255, 255, 255, 0) 0 50px 0,
rgba(255, 255, 255, 0) 25px 40px 0,
rgba(255, 255, 255, 0) 50px 50px 0,
rgba(255, 255, 255, 0) 75px 40px 0;
}
}
6.全部源碼
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>單標簽!純CSS實現動態晴陰雨雪</title>
</head>
<body>
<div class="weather sunny"></div>
<div class="weather cloudy"></div>
<div class="weather rainy"></div>
<div class="weather snowy"></div>
</body>
<style>
.weather {
position: relative;
display: inline-block;
width: 180px;
height: 240px;
background: #23b7e5;
border-radius: 8px;
}
.sunny:before {
content: "";
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 60px;
height: 60px;
background: #F6D963;
border-radius: 50%;
box-shadow: 0 0 20px #ff0;
z-index: 2;
}
.sunny:after {
content: "";
position: absolute;
top: 50%;
left: 50%;
margin: -45px 0 0 -45px;
width: 90px;
height: 90px;
background: #FFEB3B;
clip-path: polygon(
50% 0%,
64.43% 25%,
93.3% 25%,
78.87% 50%,
93.3% 75%,
64.43% 75%,
50% 100%,
35.57% 75%,
6.7% 75%,
21.13% 50%,
6.7% 25%,
35.57% 25%);
z-index: 1;
animation: sunScale 2s linear infinite;
}
@keyframes sunScale {
0% {
transform: scale(1);
}
50% {
transform: scale(1.1);
}
100% {
transform: scale(1);
}
}
.cloudy:before,
.rainy:before,
.snowy:before {
content: "";
position: absolute;
top: 50%;
left: 25%;
transform: translate(-50%, -50%);
width: 36px;
height: 36px;
background: #fff;
border-radius: 50%;
box-shadow:
#fff 22px -15px 0 6px,
#fff 57px -6px 0 2px,
#fff 87px 4px 0 -4px,
#fff 33px 6px 0 6px,
#fff 61px 6px 0 2px,
#ccc 29px -23px 0 6px,
#ccc 64px -14px 0 2px,
#ccc 94px -4px 0 -4px;
z-index: 2;
}
.cloudy:before {
animation: cloudMove 2s linear infinite;
}
@keyframes cloudMove {
0% {
transform: translate(-50%, -50%);
}
50% {
transform: translate(-50%, -60%);
}
100% {
transform: translate(-50%, -50%);
}
}
.rainy:after {
content: "";
position: absolute;
top:50%;
left: 25%;
width: 4px;
height: 14px;
background: #fff;
border-radius: 2px;
box-shadow:
#fff 25px -10px 0,
#fff 50px 0 0,
#fff 75px -10px 0,
#fff 0 25px 0,
#fff 25px 15px 0,
#fff 50px 25px 0,
#fff 75px 15px 0,
#fff 0 50px 0,
#fff 25px 40px 0,
#fff 50px 50px 0,
#fff 75px 40px 0;
animation: rainDrop 2s linear infinite;
}
@keyframes rainDrop {
0% {
transform: translate(0, 0) rotate(10deg);
}
100% {
transform: translate(-4px, 24px) rotate(10deg);
box-shadow:
#fff 25px -10px 0,
#fff 50px 0 0,
#fff 75px -10px 0,
#fff 0 25px 0,
#fff 25px 15px 0,
#fff 50px 25px 0,
#fff 75px 15px 0,
rgba(255, 255, 255, 0) 0 50px 0,
rgba(255, 255, 255, 0) 25px 40px 0,
rgba(255, 255, 255, 0) 50px 50px 0,
rgba(255, 255, 255, 0) 75px 40px 0;
}
}
.snowy:after {
content: "";
position: absolute;
top:50%;
left: 25%;
width: 8px;
height: 8px;
background: #fff;
border-radius: 50%;
box-shadow:
#fff 25px -10px 0,
#fff 50px 0 0,
#fff 75px -10px 0,
#fff 0 25px 0,
#fff 25px 15px 0,
#fff 50px 25px 0,
#fff 75px 15px 0,
#fff 0 50px 0,
#fff 25px 40px 0,
#fff 50px 50px 0,
#fff 75px 40px 0;
animation: snowDrop 2s linear infinite;
}
@keyframes snowDrop {
0% {
transform: translateY(0);
}
100% {
transform: translateY(25px);
box-shadow:
#fff 25px -10px 0,
#fff 50px 0 0,
#fff 75px -10px 0,
#fff 0 25px 0,
#fff 25px 15px 0,
#fff 50px 25px 0,
#fff 75px 15px 0,
rgba(255, 255, 255, 0) 0 50px 0,
rgba(255, 255, 255, 0) 25px 40px 0,
rgba(255, 255, 255, 0) 50px 50px 0,
rgba(255, 255, 255, 0) 75px 40px 0;
}
}
</style>
</html>