序列的間歇動畫

August 15th, 2020

情境

🕊️ 使用 CSS 實現有機感的動態一直是我想達到的目標。🕊️

有了目標,就可以想想情境啦! 從生活中擷取範例通常是個不錯的方式🌱🌱,所以最近就想了個情境:

如果我要用 CSS 讓 div 可以有類似旗幟飄揚感的動態,我可以如何實作?

(圖片來源: Unsplash)

岔題回顧一下

根據這個問題我回想了一下 CSS 在 keyframe 上面的我認為最大的痛點(與實作無關XD):

Sass/SCSS 在宣告變數並且賦予 random(x) 方法計算時,雖然我們呼叫隨機函數來產生新的值,我們終究得認清一個事實:

CSS 在放入瀏覽器的前處理之前,所有需計算的變數終究會被賦予一個靜態值。

舉例來說:

$width: random(100);

.container {
    width: $width;
}

$width 在置入瀏覽器後,它將會是由 SASS pre-preprocessor 執行前處理後的一個固定值,而瀏覽器並非一直去動態根據 random(x) 產生新的 random(x)值。

這對於一開始使用 Sass/SCSS 作運算操作的人,是很容易對它的背後運作誤解的一件事,連過去的我也不例外,以為 random 可以不斷變化,殊不知在執行後值便不再變動。

Why? 其實這個解答很簡單,因為 .css 檔就是一個靜態的樣式表檔案,供瀏覽器做靜態讀取,並且讀取後沒有任何的後續執行。

回歸正題: 站在巨人肩膀上

根據這樣個需求,我進一步不負責任地找尋相關實作,因而找到了以下的 Youtube 實作範例:

實作中,它在 container 中建立了很一列橫向序列的 li,並且在每個 li 中加入以 @keyframe animate 為名的動態,並且透過 nth-child(n + index) 的方式為每一個 li 元素設定自己在序列中的 animation-delay,進而產生波動感的效果。

這個點子非常聰明,不過就當下的實作來說,還有有一些實作彈性面與需求彈性面的問題存在:

  1. CSS 過於冗長: 需要針對每個 li 設置 nth-child 做 delay,如果我切了 100 條,我要實作 100 次?! 我不要😥
  2. 支援度可能有疑慮: 真對 @keyframe 方法沒有設立 browser prefixes,可能在 CSS animation 的瀏覽器支援度上會有疑慮。
  3. 缺乏有機感: 因為每個 li 的波動變化都一樣。
  4. 擴充性有限: 假設今天旗面的設計比較複雜,我們要用 CSS 手刻這些旗面? No, 我希望可以放入不一樣的旗面。

強化巨人的實作🏴🏴🏴

最終我擷取了上述 序列元素切割區塊動畫 的點子,並針對上述缺點進行強化。初步需要導入 Sass/SCSS,來強化 CSS code 在運算上的彈性,並且減少重複定義。

在此先附上我的實作結果:

建議先閱讀一下我的 CSS 後再往下看我的實作說明,會比較了解整體脈絡唷!

一、 解決 CSS 過於冗長

這邊我透過 CSS 的 @for 迴圈來實作,主要是要處理每個 li 在 nth-child 上面的位置與 delay 定義:

$column_count: 100;

@for $i from 1 through $column_count {
    .column:nth-child (#{$i}) {
      /* 設定 li 與其父層左邊界的距離 */
      left: ($tw_flag_w / $column_count) * ($i - 1);
      
      /* delay 加入 index 的因子來產出 */
      animation-delay: #{0.05 * ($i - 1)}s; 
      
      /* 不斷重覆動態 */
      animation-iteration-count: infinite;

      /* background-position 文章後段會詳細說明... */
      background-position: ($tw_flag_w / $column_count) * ($column_count - $i + 1) 0; 
    }
}

同時也可以看到我在 SCSS 一開始有設定一些關鍵性的共用變數設定,方便我做視覺上的 tuning。另外 li 標籤在 ul 之間的生成,我也是透過 JS 去產生出一定數量的 li 於 ul 之中,並賦予動態需要的 classname,至於數量則是要對應到 CSS 中的 $column_count 設定。

二、 解決 支援度可能有疑慮:

這一部分,我引用了 @mixin keyframes($name) 來強化原本 Sass/SCSS 的 @keyframes ,提供我們在其後宣告 @include keyframes(waving) 時導入每個瀏覽器需要的動態名稱(ex: waving) 與 prefix (ex: @-webkit-keyframes, @-moz-keyframes等),提高瀏覽器支援度。

@mixin keyframes($name) {
  @-webkit-keyframes #{$name} {
    @content; 
  }
  @-moz-keyframes #{$name} {
    @content;
  }
  @-ms-keyframes #{$name} {
    @content;
  }
  @keyframes #{$name} {
    @content;
  } 
}

三、 解決 缺乏有機感

這邊其實利用了比較巧妙的方式來解決這個問題,當我們在時做動畫時,我們通常會定義動畫的 timing function,假設我們設定 linear:

animation: waving 5s linear;

因為是 線性 動態,所以在旗幟的上下邊可以看到很線性的生硬視覺,沒有飄揚感(如下圖)。

linear snapshot

因此我們可以透過 easings.net 這個網站中定義的 timing functions 來讓上下邊的動態更有弧度上的變化,因此我選用了 easeInOutSine 的 timing function,因為它可以連結出順暢的 sine 波形的動態變化:

animation: waving 5s cubic-bezier(0.37, 0, 0.63, 1);

成果如下截圖: easeInOutSine snapshot

另外我也透過 transform 來設定整體旗幟在 x 軸、y 軸上的視角,使其有立體感~! 🧊

transform: rotateX(-20deg) rotateY(20deg);

四、 解決 擴充性有限

最後一點其實相對好解決,主要就是要確保旗幟設計的定義與 background-image 的導入:

  1. 通常旗幟都是 3:2 的黃金比例 : 針對這一點,我對旗幟的 container 定義了比例計算的設定:
.tw-flag {
    ...
    
    /* 變數化設定,可以視需求調整旗幟寬度。 */
    width: $tw_flag_w;

    /* 利用 padding 達成 container 維持比例的需求。 */
    padding-bottom: $tw_flag_w * (2 / 3);
    
    ...
}
 
  1. 用 background-image 加入旗面圖片 : 用 CSS 手刻旗面是不切實際的,所以我們只需要找到 3:2 的旗幟圖我們就能夠直接貼入每個 li 使用唷! 另外,如果我們只是單純的設定 background-image,畫面會很奇怪,程式碼與呈現如下圖所示:
 .column {
    ...

    /* 只擺入圖片連結。 */
    background-image: url(image.jpg); 

    /* 並確保它填滿每個 li 的細條 container。 */
    background-size: cover;
    ...

}

沒有做 background-position    的預設效果

會造成這樣的原因是因為 li 的 background 沒有做 background-position 的設定,所以初始預設都是從圖片最左邊擷取 li 窄窄的寬度出來 cover 到 li 上(下面附圖說明)。

黃框處是背景圖重複地方

所以我們需要進到我們在前述中對於 li 設置的 @for 迴圈的定義中,利用 @for 所帶出的 $i 迴圈變數來逐一循序地計算每隔 li 在 x 軸上的 background-position,程式碼如下:

@for $i from 1 through $column_count {
    .column:nth-child(#{$i}) {

      ...

      /* 用 $i 計算 x 軸的背景圖位置 */
      background-position: ($tw_flag_w / $column_count) * ($column_count - $i + 1) 0;
    }
}

設定完後,旗幟就可以完整呈現出來囉!

結論

在這個實作中,對於序列的間歇動畫上,我有一些實作的感想,在此紀錄一下 📝:

  • Timing function 選用很重要: 有時候實作動態出現問題時,不是完全都是 @keyframe 中對於動態的切分問題,而是 Timing function 選用錯誤進而影響到了我們對於動態的感知。
  • animation-delay 的值要得宜: animation-delay 在這次的實作中扮演了 動態解析度 的關鍵角色,如果 li 之間的 delay 太長,則弧度動態上會出現顆粒感(如下圖是 delay 太長產生的鋸齒),所以要盡可能讓 delay 維持一定程度的小,li 之間才會很流動的序列間歇動畫
  • animation 的總秒數: animation 的總秒數會影響 li 之間間歇動畫的鋪陳長度,太短則波動與頻率會太劇烈而不自然;太長則波動會過於平緩。

delay 太長產生的鋸齒