CSS如何实现内凹角效果 By 大漠
記得@Lea Verou的《CSS Secrets》一書和前幾天@Chris Coyier剛發的帖子都介紹了CSS怎么實現元素斜切口的效果。我也嘗試著借助Vue的能力,把這種效果構建成一個Vue組件。我把這種效果定義為外切口。而今天將要聊的是與其剛好相反的一個效果:CSS如何實現內凹角的效果。
上圖展示的效果就是接下來所要聊的內凹角的效果。也就是說,通過下文的介紹,我們可以知道這種效果是如何做的,而且如何在多個元素上實現這樣的內凹角效果。在實現這樣的效果當中,將會遇到些什么棘手的問題,又是怎么繞過這些問題的。
最初的想法:box-shadow
對于box-shadow的屬性,想必大家已經非常了解了,如果你從未接觸過box-shadow屬性,那么強烈建議您花一點時間去了解一下box-shadow相關的知識。這樣能幫助你更好的理解后續的內容。
我先假設你對box-shadow有了一定的了解。就算你不了解,也沒有關系。你也可以繼續后面的內容。假設我們有一個div的元素。給這個元素添加了一個.box的類名:
我們可以顯式的給這個.box元素設置大小或者通過其自己的內容來決定大小,不管是哪種方式,都并不很重要。這里為了簡單起見,給其設置了max-width和min-height(也是用來設置其大小的)。另外為了能在瀏覽器中看到效果,其添加了一個outline的效果,讓其看起來有邊框的樣子。或許你會問,為什么不直接使用border呢?這個問題留給大家去思考吧,因為不是這篇文章要探討的內容。
接下來,通過偽元素::before來創建一個正方形,其邊長等于圓角的直徑(或者半徑--r的兩倍),而且對這個偽元素使用絕對定位。另外為了能在瀏覽器中看到效果,給這個偽元素添加了一個box-shadow和background屬性。這只是用來輔助大家理解的,后續會刪除的。
特別聲明:本文的實例代碼都來自于@ANA TUDOR的《Scooped Corners in 2018》一文。不同的是我把文章中的Sass變量換成了CSS自定義變量。后續內容如無特別說明,都將類似的做了修改。
這個時候看到的效果如下:
效果如你所期望的一樣。接下來對偽元素::before的border-radius值設置為50%,讓它成為一個圓形,并且給它設置一個margin的負值,值等于它的半徑--r。偽元素的中心點和它的父容器.box的左上角(0,0)重合。為了讓溢出的.box的偽元素能隱藏起來,需要在.box中添加一個overflow:hidden。
現在的結果是這樣的:
但這樣的效果仍然不是我們想要的。為了達到我們想要的效果,我們需要使用box-shadow的第四個參數值:陰影擴展半徑。如果你想了解box-shadow添加第四個參數值的效果,可以看下面這個Demo:
你可能已經猜到我們下一步要做什么了。把background和box-shadow前三個值(x和y軸的偏移值以及模糊半徑)設置為0,并給box-shadow的擴展半徑設置為一個較大的值。
下面的這個示例演示了box-shadow的擴展半徑如何讓陰影效果覆蓋容器更多的面積。
這里用到的一個技巧是讓box-shadow有足夠大的擴展半徑,這樣讓偽元素的陰影能覆蓋其容器更多的面積。這是非常有意思的一點,給.box設置box-shadow以及給其偽元素添加一個半透明的陰影效果。
其實這是很關鍵的一步,如果你不仔細看,你或許會認為,那個凹角的效果是box-shadow實現的。或許你和我一樣會納悶,box-shadow是如何實現透明凹角的效果。事實并非如此,透明凹角部分是偽元素::before的background-color為transparent,而整個紫色部分是由::before的box-shadow實現的(就是陰影擴散半徑有足夠大的值,能鋪滿.box的容器大小)。我錄一個視頻給大家看看,或許能比文字更好的說明一切原理:
是不是一圖勝過千言萬語呀。
上面看到的效果,不難發現,凹角的大小是固定的。好在我們這里使用了CSS的自定義屬性。因為使用CSS自定義屬性之后,可以很容易的通過JavaScript來修改這個屬性。這樣一來,就可以很好的控制凹角的大小。比如:
這是實現凹角效果的關鍵樣式。具體的不多說了,能只要仔細閱讀上面的內容,你就能明白為什么。
值得一提的是,我們前面看到的效果都是.box中沒有任何內容。也就是說.box里有內容的時候,我們是需要在樣式上做一定的調整的。為什么這么說呢?先來看一個效果:
要解決這個問題,很簡單,咱們只需要在.box的偽元素::before上添加z-index屬性,并且給其設置值為-1。
另外通過.setProperty()來修改--r的值。這需要一些JavaScript代碼來支持:
現在離我們想要的效果越來越近了。我們已經知道如何通過box-shadow給單個.box設置單個凹角的效果。那么如果我們想要給一個元素添加四個凹角效果,怎么實現呢?想想,如果你想得出來,可以立馬動手試試,就算你想不出來,也并不要緊,后面我們會介紹怎么給.box盒子的每個角添加凹角的效果。
那么到這一步,咱們先暫停一下。上面我們看到的是CSS的自定義屬性和JavaScript來實現想要的凹角效果。那么咱們先暫停一步,來看看怎么通過Vue來實現上面示例的效果。
有關于Vue的代碼這里就不展示了。詳細的可以查看上面Demo的代碼,其實你還可以添加其他的參數,比如除了給scooped-corners組件傳凹角半徑值之外,還可以傳border-radius和background-color之類。感興趣的可以嘗試一下,并且歡迎在下面的評論中分享您的成果。
就用這種技術
接下來,咱們再深入一點,看看怎么運用這種技術來實現文章開頭展示的效果。這里有一點不一樣,偽元素的中心點與盒子不致,但他們都有一個共同點,偽元素的中心點,在每個盒子的頂點處。
使用的HTML結構非常簡單,這里使用了4個<article>元素(相當于前面所講的.box元素),在<article>元素中包含了一些文本內容:
<body>包含了四個<article>元素,還有一個<header>元素,整個布局效果采用的是Flexbox。從文章開頭的效果上來看,<header>寬度非常寬,然后每行有一個個或兩個<article>元素。具體每行展示一個還是兩個,這取決于瀏覽器視窗的寬度。
如果我們每一行只有一個<article>時,那么元素上就不會有凹角的效果,這個時候需要把凹角的半徑設置為0。否則我們就要設置一個非零的半徑,也就是說--r的值不為0。
特別注意,CSS自定義屬性不能用于媒體查詢的條件中,但可以用于媒體查詢的區塊內。
現在我們考慮一下,每行有兩個<article>元素(當然,每個元素都有一個內凹角,因為這個才是我們感興趣的東東)。
第一個元素中,凹角的圓形在它的父元素的最右邊邊,也就是left:100%。為了將凹角圓中心點x坐標移到其父元素的右邊緣,我們就需要減去圓的半徑--r,那么left的值就變成了calc(100% - var(--r))。但我們不想讓它出現在右邊,而是希望它在<article>元素向右移--m。這樣我們就可以算出我們最終想要的一個值:
對于最后一個,其水平方向的偏移量和第二個具有相同的值,垂直方向的偏移量和第三個具有相同的值。
所以四個元素對應的left和top的偏移量如下:
這意味著凹角圓的中心位置取決于<article>元素之間的間距(這個間距是我們設置的margin: var(--m)的兩倍),凹角的半徑是--r。在一對水平和垂直的乘數因子分別是--i和--j,而且他們的最初的值都是-1。
對于第一行的兩個<article>元素(第一行是一個2 x 2的網格),我們需要改變垂直方向的乘數因子--j為1,這樣就可以讓凹角的圓心在y軸上低于父容器底部邊緣;而對于奇數的<article>(第一列),需要改變水平方向的乘數因子--i為1,這樣就可以讓凹角的圓心在x軸上位于父容器的右側邊緣。
@media (min-width: 2*($min-w + 2*$m)) {article {&:nth-of-type(-n + 2) h3, &:nth-of-type(n + 3) section { &:before { --r: 0 ; } }} }
以類似的方式,我們為<article>元素的子元素添加不同的樣式:
h3, section { --p: .5rem;padding: $p; }@media (min-width: 2*($min-w + 2*$m)) {article {&:nth-of-type(-n + 2) section, &:nth-of-type(n + 3) h3 {padding-right: calc(.5*(1 + var(--i))*(var(--r) - var(--m)) + var(--p));padding-left: calc(.5*(1 - var(--i))*(var(--r) - var(--m)) + var(--p));}} }
最終效果如下:
特別聲明:今天使用CSS自定義屬性,在媒體查詢的條件中使用自定義屬性踩了一個坑。那是因為我想在代碼中統一使用CSS自定義屬性來替代Sass這樣處理器的變量。一直以為在CSS的媒體查詢的條件中使用CSS自定義屬性是OK的,結果實測代碼的時候才發現不支持。最后查找了一下原因:
The?var()?function can be used in place of any part of a value in any property on an element. The?var()?function can not be used as property names, selectors, or anything else besides property values. (Doing so usually produces invalid syntax, or else a value whose meaning has no connection to the variable.) —— From the?spec
值得慶達的是,你可以使用PostCSS插件postcss-media-variables來做處理。感興趣的可以自己試試。說實話,再一次感嘆PostCSS的神奇之處和無所不能。
潛在的問題
上面的示例看上去完美,方法簡單而又能跪瀏覽器兼容。或許你已經發現了,上例是在一個特定情況下想的結果,但很多時候我們總不是這么的幸運。哪一天需求一變,是不是還能如此輕易而又完美的實現呢?
首先,我們需要用一個偽元素來做這個凹角,當你只需要一個(比如上面看到的示例)或者兩個的時候,都不是問題,但有的時候元素的四個角都需要這樣的凹角時,那么我們就需要引入一個額外的元素。另外當你的偽元素被其他功能(比如Icon)占用時,你也不得不為此效果添加一個額外的標簽元素。蛋疼了吧!
其次上面示例中的background是一個純色,但我們不可能總是在使用純色背景的場景中。如果我們想要一個半透明的或者漸變的背景,或者在一張背景圖片之下,那么凹角將會成為我們的一個痛點,甚至會說,這個沒法實現。
因此,我們需要探索其他更可靠的方案,并且也能讓它得到眾多瀏覽器的支持。
靈活性和良好的瀏覽器支持?是SVG?
想到SVG并不奇怪,但是如果我們想要靈活一點,瀏覽器兼容性全面一點,SVG可以說是一個最好的解決方案。在.box容器中包含了一個<svg>元素,而且放置在內容的前面。SVG中包含了一個<circle>元素,在這個元素上設置了r屬性。
讓svg相對于.box元素做相對定位,將將其大小設置為能完全覆蓋父容器:
.box { position: relative; }svg {position: absolute;width: 100%;height: 100%; }
到目前為止,沒有什么有趣的東西,所以給<circle>添加一個id屬性,并且使用SVG的<use>元素來復制多個id相同的<circle>:
看到這里,是不是會覺得比使用::before偽元素要來得簡便,而且也非常方便,就算你要移去一個或多個凹角(示例效果的紫色部分),你只需要少使用幾個<use>去克隆就行了。
從上例效果中可以看到,.box的四個角落都圓圈在那了,但這并不是我們想要的凹角,對吧!不要納悶了,我們的做法是對的。請接著往下看。接下來要做的就是把這些圓圈放到一個<mask>中,給這個<mask>設置一個white的填充色(fill屬性來搞定)。同時在其里面使用<rect>元素設置一個和SVG元素一樣大小的矩形,主要用來覆蓋整個SVG。然后我們在另一個再次使用<use>來調用這個已創建好的<mask>:
最終效果如下:
同樣的,我錄制了一個動圖來演示效果中的每一個元素:
特別注意:如果.box中有內容,建議放置在svg元素之后。當然也可以放置在其前面,如果放置在前面,svg在做定位時,需要顯式的設置top、right、bottom和left之類的值。至于為什么,這里不做過多的闡述,感興趣的同學可以自己去深究其中的為什么。
如果.box有文本,需要把.box的padding的值和圓角的半徑設置相同,同樣的,如果使用了CSS自定義屬性,可以使用JavaScript來控制它。最好把圓的半徑和.box的padding使用同一個CSS自定義屬性--r。這樣會更好的控制一點:
我喜歡的就是CSS
或許你會說,我不懂SVG,我就是想使用CSS來實現。其實很高興你能這樣的深究與思考。事實上我們的確可以使用CSS來實現這樣的效果。
遺憾的是,使用CSS的方案目前為止并不是所有瀏覽器都能支持,但使用CSS讓我們把事情變得更簡化,而且在不遠的將來,它們肯定是能得到眾多瀏覽器支持的。
在HTML元素上使用CSS的mask
這里我們移除SVG所有的東西,然后使用CSS,可以在.box元素上設置一個background(可以是一個純色、半透明、漸變、圖像或者多背景,甚至是你任何你想要的CSS)和mask屬性。
使用CSS設置圓半徑
這意味著,需要把<circle>中的r屬性刪除,然后在CSS中給其設置半徑的大小,這里設置的半徑大小與.box容器的padding值一樣:
這樣一來,如果我們改變半么--r的值,凹角的大小和.box的padding也會隨著更新。
注意,在CSS中給SVG元素設置幾何屬性只在Blink瀏覽器中有效!
結合前兩種方法
雖然這很酷,但遺憾的是目前在任何瀏覽器中都看不到效果。但值得慶幸的是我們可以做得更好!
使用漸變來做朦層
Note that CSS masking on HTML elements doesn't work at all in Edge at this point, though it's?listed as "In Development"?and a flag for it (that doesn't do anything for now)?has already shown up?in?about:flags.
由于我們需要完全拋棄SVG,所以我們需要使用CSS的漸變為mask做些事情。這里將使用CSS徑向漸變來畫圓,下面就是CSS繪制的一個半徑為--r的圓,并且這個圓位于.box的左上角。
如果您對CSS的漸變不太了解,建議您花點時間閱讀這幾篇文章:《再說CSS3漸變:線性漸變》、《再說CSS3漸變:徑向漸變》、《為什么要使用repeating-linear-gradient》、《?你真的理解CSS的linear-gradient?》。
這個時候你可以看到像上面這樣的一個效果:
接下來在mask使用相同的漸變:
注意,Webkit瀏覽器仍然需要給mask屬性添加-webkit-前綴。如果你不知道mask怎么使用,建議你花點時間閱讀《如何在CSS中使用遮罩》一文。因為后面很多內容都會涉及到這個屬性,這樣能幫助更好的理解后續的內容。
給.box每個角落都添加漸變繪制的圓:
上面這樣做還不是很好,可以借助CSS處理器的循環特性來做,會更好一些:
就代碼而言,這樣寫已經很完美了。因為我們不需要多次編寫,并且以后在任何地方使用都不需要做任何更改。但到目前為止的結果并不是我們想要的:
從上面的示例中可以看出,我們除了凹角部分之外的東西都剪切掉了,這正好和我們想要的東西相反。要得到我們想要的效果,咱們只需要做一件事情,把漸變反過來。讓凹角的圓變成透明,剩余的部分全部是黑色。
--stop-list: transparent var(--r, 50px), #000 0;需要注意的是,如果我們只使用一個漸變的時候,那么上面的代碼就幫我們解決了問題:
但是,當我們把所有的四個圓圈(甚至兩個)都堆起來的時候,就會得到一個黑色的矩形,這個矩形的大小相當于我們的mask的大小,這意味著沒有任何東西會被掩蓋掉。
因此,我們需要把每個漸變的大小限制在盒子的四分之處(width的50%和height的50%),從而得到25%的面積:
這個意思就是,我們需要設置mask-size的值為50% 50%,同時mask-repeat的值為no-repeat以及每個mask-image自身的位置。
$grad-list: ();@for $i from 0 to 4 {$x: ($i%2)*100%;$y: floor($i/2)*100%;$grad-list: $grad-list radial-gradient(circle at $x $y, var(--stop-list)) /* mask image */$x $y; /* mask position */ }.box {/* same as before *//* any CSS background we wish */--stop-list: transparent var(--r, 50px), #000 0;mask: $grad-list;mask-size: 50% 50%;mask-repeat: no-repeat; }但這里有一個大問題,一般情況下,我們的四分之一計算的每個部分會經過四舍五入,那么這四個部分重新組合在一起的時候,width和height都有可能產生間距。如下圖所示:
好吧,我們不能用linear-gradient()來做這個線條或者說把mask-size的尺寸增加到51%。比如下面的這個示例,增加了mask-size的尺寸來處理四個漸變區載之間的間距。
但是,難道沒有更優雅的方式來處理這個間距?不是的,可以使用mask-composite屬性來幫我們處理。當我們返回全部漸變的全尺寸時,可以把mask-composite的值設置為intersect。
$grad-list: ();@for $i from 0 to 4 {$grad-list: $grad-list, radial-gradient(circle at ($i%2)*100% floor($i/2)*100%, var(--stop-list)); }.box {/* same as before *//* any CSS background we wish */--stop-list: transparent var(--r, 50px), #000 0;mask: $grad-list;mask-composite: exclude; }這非常酷,因為它是純CSS的解決方案,沒有使用任何SVG代碼,但不幸的是,目前得能看到效果的也僅限于Firefox53+。
corner-shape
大約在五年前,@Lea Verou提出了一個想法,甚至還為它創建了一個預覽頁面。遺憾的是,它不僅沒有被任何瀏覽器實現,而且在此期規范還沒有得到很大的提高。對于未來,它仍然是值得期待的,因為它提供了很多靈活性,而且代碼非常少。比如說,實現我們前面所說的效果,只需要以下幾行代碼:
padding: var(--r); corner-shape: scoop; border-radius: var(--r);就是一個非常簡單的CSS。是不是值得期待,但最終還是要看瀏覽器什么時候會對其支持。
CSS Houdini
CSS Houdini慢慢的開始進入大家的世界當中,試問一下,我們使用CSS Houdini是不是可以更方便的實現這個內凹角的效果呢?比如像下面這樣的一個效果,它就是使用CSS Houdini實現的:
咱們不仿嘗試一下使用Paint Worklet或者CSS Paint API來實現呢?請開動你的大腦,動手擼一擼。希望您能把你的成果在下面的評論中與大家一起分享?如果你感興趣,也可以在下面的評論中留言,我們后續可以專門花一點時間來看看CSS Houdini可以實現內凹角的效果,甚至是前面所講的斜外切口的效果。
構建一個內凹角的Vue組件
記得在《使用Vue制作切口盒子組件》一文中,咱們就嘗試使用Vue構建了一個斜外切口的Vue組件c-noth:
那么我們來看看怎么使用Vue來構建一個內凹角的組件,具體代碼如下:
特別聲明,如果您的瀏覽器沒有看到任何效果,請使用Firefox 53+瀏覽器查閱。具體原因,前面文章已經介紹過來了。
為了照顧其他同學查看最終的效果,我錄了一個屏:
這就是最終的效果。由于我自己是Vue的初學者,現在有一個病,看到什么東西都想用Vue來寫,而且想封裝成一個組件。如果寫得不好,或者有更好的方案,歡迎大家指點,并且希望能看到您的分享的成果。如果你和我一樣,也是Vue的一個初學者,可以和我一起來學習Vue。整理了一些有關于Vue的學習筆記,希望大家能喜歡,更希望能幫助到初學者,同時也希望不會誤人子弟。
總結
文章開頭拋出了怎么實現內凹角的一個效果。首先從CSS的box-shadow著手,使用CSS的box-shadow可以輕易的實現內凹角的效果,但這個方案有一定的局限性,比如要多個內凹角時,需要通過增加元素標簽來實現,特別是在面對漸變,或者有背景圖像和半透明的情景之下,這個方案基本上無法來滿足我們的需求。
接著探索了SVG的方案,通過SVG的mask和use之類的一些獨有的特性,可能靈活的幫助我們實現想要的效果,而且能做到box-shadow無法做到的事情。特別是通過CSS自定義屬性來修改SVG的屬性,讓事情變得更具靈活性,只不過部分瀏覽器還不支持CSS來修改SVG的屬性,這算是其中的一個坑吧。不過我們還是可以規避掉的。
雖然SVG能實現想要的效果,但對于一位CSS執著者而言,總是希望不借助其他的外力,通過純CSS來實現這個效果,事實上也是可以的,使用CSS的徑向漸變和mask相關的知識,可以實現我們想要的效果。遺憾的是,目前眾多瀏覽器對mask還是有所保留,未能全面支持。比如文中提到的,很多mask相關的特性,僅能在Firefox 53+上看到。包括咱們寫的示例,有些僅能在Firefox上看到。
隨著CSS Houdini技術越來越成熟,我在試想,是否可以通過CSS Houdini來實現。正如@Lea Verou五年前提出的corner-shape屬性。我想是可以的,后面可以嘗試動手寫寫。當然,CSS Houdini雖然還沒有得到所有瀏覽器支持,但這并不防礙我們去嘗試著寫各種效果。有興趣的一起動手寫寫,看看這個想法是否能成真。
最后為了能練習Vue相關的知識,嘗試使用Vue封裝了一個簡單的凹角組件。寫得比較拙逼,希望能得到大神的指點。
最后的最后,需要特別感謝@ANA TUDOR寫了這么優秀的教程。我在原作者的基礎上做過一些調整,如果你覺得這里整理和不好,可以查閱原文。
大漠
常用昵稱“大漠”,W3CPlus創始人,目前就職于手淘。對HTML5、CSS3和Sass等前端腳本語言有非常深入的認識和豐富的實踐經驗,尤其專注對CSS3的研究,是國內最早研究和使用CSS3技術的一批人。CSS3、Sass和Drupal中國布道者。2014年出版《圖解CSS3:核心技術與案例實戰》。
原文發布時間為:2018年04月23日
原文作者:掘金
本文來源:?掘金?如需轉載請聯系原作者
總結
以上是生活随笔為你收集整理的CSS如何实现内凹角效果 By 大漠的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 基于Object.definePrope
- 下一篇: nginx basic auth配置踩坑