CSS 技巧一則 -- 在 CSS 中使用三角函式繪製曲線圖形及展示動畫
最近一直在使用 css-doodle 實現一些 CSS 效果。
css-doodle 是一個基於 Web-Component 的庫。允許我們快速的建立基於 CSS Grid 佈局的頁面,以實現各種 CSS 效果(或許可以稱之為 CSS 藝術)。後續幾篇文章可能都會與之有關。
當然,本文的主角並不是 css-doodle。
CSS本身一直在快速發展更新,標準也與時俱進,各種新特性層出不窮,為了能夠使用 CSS 來創造各種佈局實現各種形狀,除了合理運用及搭配各個屬性之外,去理解壓榨每個屬性的每個細節點也是非常重要的。
本文將介紹一種在 CSS 中藉助三角函式繪製曲線圖形的小技巧。
理解 box-shadow
首先,回顧一下 box-shadow
這個屬性。基本屬性用法就是給元素創造一層陰影。
關於陰影的許多細節,可以先看看這篇文章:你所不知道的 CSS 陰影技巧與細節
再簡單提一下,本文會用到的關於陰影的第一個技巧:
使用陰影複製影象/投影影象
當 box-shadow 的第三、第四個引數模糊半徑和擴張半徑都為 0 的時候,我們可以得到一個和元素大小一樣的陰影:
div { width: 80px; height: 80px; border: 1px solid #333; box-sizing: border-box; box-shadow: 80px 80px 0 0 #000; }
得到如下結果:
陰影可以是多重的
第二個技巧則是,box-shadow
是允許多重陰影的,並且他們的座標是可以完全掌控的。
是的,我們可以像下面這樣給一個元素定義多重陰影,並且利用陰影的第一、第二個引數控制它相對於元素的座標:
div { width: 80px; height: 80px; border: 1px solid #333; box-sizing: border-box; box-shadow: 80px 80px 0 0 #000, 70px 70px 0 0 #000, ... 60px 60px 0 0 #000; }
在陰影座標中運用三角函式
繼續。接下來,我們嘗試在陰影的座標中引入三角函式。
為啥是三角函式,不是圓的標準方程或者橢圓的標準方程或者其他圖形函式呢?當然也是可以的,只是這裡藉助三角函式的 cos
或 sin
可以實現直接使用 CSS 實現起來很困難的曲線。
帶著疑問,先繼續向下,假設我們要實現這樣一條曲線:
使用 CSS 的話,有什麼辦法呢?
可能的一些辦法是 clip-path
,或者一些奇技淫巧,使用 text-decoration
裡的波浪下劃線 wavy
,或者是使用漸變疊加。
當然,還有一種辦法是本文將提到的使用 box-shadow
及 三角函式。
三角函式
咳咳,簡單回顧下三角函式裡面的 sin、cos 曲線圖像變換,還沒有全部還給老師。
如果我們有一個 1x1 的 div,它的多重陰影,能夠按照像正弦/餘弦函式的影象一樣進行排布,連起來不就是一條曲線嗎?
如何在 CSS 中使用三角函式 sin/cos
想法不錯,但是 CSS 本身並沒有提供三角函式。這裡,我們需要藉助 Sass 來在 CSS 中實現簡單的三角函式。
還好,已經有前人幫忙把這個工作做完了:
- trigonometry in sass
- 在Sass中實現三角函式計算
簡單而言,就是藉助三角函式的泰勒展開式,使用 Sass 函式模擬實現三角函式的 sin()、cos()、tan():
由於展開式是無限長的,使用 Sass 函式模擬時,不可能得到一個非常精確的值,但是在日常作圖下已經完全夠用了,以下是使用 Sass 函式模擬實現三角函式的 sin()、cos()、tan():
@function fact($number) { $value: 1; @if $number>0 { @for $i from 1 through $number { $value: $value * $i; } } @return $value; } @function pow($number, $exp) { $value: 1; @if $exp>0 { @for $i from 1 through $exp { $value: $value * $number; } } @else if $exp < 0 { @for $i from 1 through -$exp { $value: $value / $number; } } @return $value; } @function rad($angle) { $unit: unit($angle); $unitless: $angle / ($angle * 0 + 1); @if $unit==deg { $unitless: $unitless / 180 * pi(); } @return $unitless; } @function pi() { @return 3.14159265359; } @function sin($angle) { $sin: 0; $angle: rad($angle); // Iterate a bunch of times. @for $i from 0 through 20 { $sin: $sin + pow(-1, $i) * pow($angle, (2 * $i + 1)) / fact(2 * $i + 1); } @return $sin; } @function cos($angle) { $cos: 0; $angle: rad($angle); // Iterate a bunch of times. @for $i from 0 through 20 { $cos: $cos + pow(-1, $i) * pow($angle, 2 * $i) / fact(2 * $i); } @return $cos; } @function tan($angle) { @return sin($angle) / cos($angle); }
由於上面最終計算 sin、cos 泰勒展開的時候,只使用了 20 層迴圈,所以當傳入的值太大的時候,則會產生較大誤差。經測試,傳入數值在 [-20, 20] 以內,精度還是非常高的。
而以 sin 函式為例,x 取值在 [-π, π] 之間,已經能覆蓋所有 sin(x) 的取值範圍,所以 [-20, 20] 這個範圍是完全夠用的,我們只需要儘量讓傳入的 x 值落在這個區域範圍內即不會產生太大誤差。
好,鋪墊了那麼多,接下來使用上述的 sin 函式試一下,假設我們有這樣一個結構:
<div></div>
div { width: 1px; height: 1px; background: #000; border-radius: 50%; }
我們再借助 Sass 實現一個 50 層的迴圈,當然其中陰影的 x 座標使用了 sin 函式:
@function shadowSet($vx, $vy) { $shadow : 0 0 0 0 #000; @for $i from 0 through 50 { $x: sin($i / 8) * $vx; $y: $i * $vy; $shadow: $shadow, #{$x} #{$y} 0 0 rgba(0, 0, 0, 1); } @return $shadow; } div { width: 1px; height: 1px; background: #000; border-radius: 50%; box-shadow: shadowSet(4px, 1px); }
上面 sin($i / 8)
,這裡除以 8 是為了讓整個sin(x) 傳入的作用域的取值範圍為 [0, 6.25],當而 sin(x) 的作用域為 [0,2π] 時剛好可以畫一條完整的單次曲線。這個 8 是可以根據迴圈的次數不同而進行調整的。
實際,我們得到的 box-shadow
如下:
{ box-shadow: 0 0 0 0 black, 0.4986989335px 1px 0 0 black, 0.989615837px 2px 0 0 black, 1.4650901163px 3px 0 0 black, 1.9177021544px 4px 0 0 black, 2.3403890918px 5px 0 0 black, 2.7265550401px 6px 0 0 black, 3.0701740089px 7px 0 0 black, 3.3658839392px 8px 0 0 black, 3.6090703764px 9px 0 0 black, 3.7959384774px 10px 0 0 black, 3.9235722281px 11px 0 0 black, 3.9899799464px 12px 0 0 black, 3.9941253622px 13px 0 0 black, 3.9359437875px 14px 0 0 black, 3.8163431264px 15px 0 0 black, 3.6371897073px 16px 0 0 black, 3.4012791593px 17px 0 0 black, 3.1122927876px 18px 0 0 black, 2.7747401278px 19px 0 0 black, 2.3938885764px 20px 0 0 black, 1.9756811944px 21px 0 0 black, 1.5266439682px 22px 0 0 black, 1.0537839735px 23px 0 0 black, 0.5644800322px 24px 0 0 black, 0.0663675689px 25px 0 0 black, -0.4327805381px 26px 0 0 black, -0.9251752496px 27px 0 0 black, -1.4031329108px 28px 0 0 black, -1.8591951521px 29px 0 0 black, -2.286245275px 30px 0 0 black, -2.677619305px 31px 0 0 black, -3.0272099812px 32px 0 0 black, -3.3295620582px 33px 0 0 black, -3.5799574329px 34px 0 0 black, -3.7744887692px 35px 0 0 black, -3.9101204707px 36px 0 0 black, -3.9847360499px 37px 0 0 black, -3.9971711559px 38px 0 0 black, -3.9472317429px 39px 0 0 black, -3.8356970987px 40px 0 0 black, -3.6643076841px 41px 0 0 black, -3.4357379737px 42px 0 0 black, -3.1535547213px 43px 0 0 black, -2.8221613023px 44px 0 0 black, -2.446729px 45px 0 0 black, -2.03311631px 46px 0 0 black, -1.58777752px 47px 0 0 black, -1.1176619928px 48px 0 0 black, -0.630105724px 49px 0 0 black, -0.1327168662px 50px 0 0 black; }
實際得到的影象如下:
CodePen Demo -- sass2sin Line
控制顏色及初始方向
看看上面 Sass 實現的這個方法 @function shadowSet($vx, $vy)
,其中 $vx
,$vy
用於控制影象的振幅及鬆散程度,我們再新增一個控制初始方向的 $direction
,控制陰影層數的 $count, 控制顏色的 $color:
@function shadowSet($vx, $vy, $direction, $count, $color) { $shadow : 0 0 0 0 $color; @for $i from 0 through $count { $x: sin($i / 8) * $vx * $direction; $y: $i * $vy; $shadow: $shadow, #{$x} #{$y} 0 0 $color; } @return $shadow; } .line { width: 1px; height: 1px; margin: 10vh auto; background: #000; border-radius: 50%; box-shadow: shadowSet(4px, 1px, 1, 50, #000); } .reverseline { width: 1px; height: 1px; margin: 10vh auto; background: #000; border-radius: 50%; box-shadow: shadowSet(8px, 2px, -1, 100, red); }
控制顏色
再進一步,我們可以藉助 Sass 的各種顏色函式,實現顏色的變化:
@function shadowSetColor($vx, $vy, $direction, $count, $color) { $shadow : 0 0 0 0 $color; @for $i from 0 through $count { $color: lighten($color, .5); $x: sin($i / 8) * $vx * $direction; $y: $i * $vy; $shadow: $shadow, #{$x} #{$y} 0 0 $color; } @return $shadow; } .colorline { width: 5px; height: 5px; margin: 10vh auto; background: green; border-radius: 50%; box-shadow: shadowSetColor(8px, 2px, -1, 100, green); }
上面,藉助了 lighten
這個函式,通過改變顏色的亮度值,讓顏色變亮,建立一個新的顏色。
當然,Sass 中還有很多其他顏色函式:
- adjust-hue($color,$degrees):通過改變一個顏色的色相值,建立一個新的顏色;
- lighten($color,$amount):通過改變顏色的亮度值,讓顏色變亮,建立一個新的顏色;
- darken($color,$amount):通過改變顏色的亮度值,讓顏色變暗,建立一個新的顏色;
- saturate($color,$amount):通過改變顏色的飽和度值,讓顏色更飽和,從而建立一個新的顏色
- desaturate($color,$amount):通過改變顏色的飽和度值,讓顏色更少的飽和,從而創建出一個新的顏色;
更多 Sass 顏色函式,可以看看這篇文章:Sass基礎——顏色函式
OK,看看這次的效果:
CodePen Demo -- sass2sin Line
在 css-doodle 中使用
OK,前面所有的鋪墊都是為了在實際的一些創意想法中去使用它。
在 css-doodle 中,由於是利用 Web Component 特性。在需要三角函式的時候,可以直接使用 JavaScript 提供的 Math 函式,會更加的方便。
Web Components 是一套不同的 Web 技術,允許您建立可重用的定製元素(它們的功能封裝在您的程式碼之外)並且在您的web應用中使用它們。
袁川老師,也就是 css-doodle 庫的作者,在他的 Codepen 首頁背景板中,使用的就是使用上述技巧實現的一副純 CSS 畫作:
Codepen Demo -- border-radius
我也嘗試使用這個技巧,做了一副:
Codepen Demo -- CSS-Doodle fish