iT邦幫忙

2026 iThome 鐵人賽

DAY 24
0
Modern Web

從實務需求出發:前端視覺與 UI 互動開發實踐系列 第 24 篇

【Day 24】動畫進階:讓動畫跟著圖走,SVG 動畫

  • 分享至 

  • xImage
  •  

面對重複出現卻略微不同的動態效果,Day23 把「差異」抽成了參數。今天我們換個角度想:如果動畫本來就是這張圖的一部分,不如讓它「跟著圖」走?

之前看專案時發現一個狀況:同一批裝飾用的 SVG 被兩個畫面引用,兩邊都是用 <img> 載入,所以搖晃、漂浮的動畫各自寫在各畫面的 CSS 裡。兩邊都有一整套同名的 @keyframes,內容超過九成一樣,只差在定位點與圖片尺寸。而且 CSS 裡還將「版面」與「動作」混在一起,例如:top 與 left 同時負責了圖片的定位與動畫的位移。

這代表把圖搬去別的地方,動畫就要再抄一次。剛好研究上一小題目 SVG 的時候,發現有供動畫使用的標籤。今天就來看看該如何讓 SVG 本身動起來。那麼我們開始吧!


一、SVG 的動畫標籤

先來做基礎介紹,前面有提到 SVG 內建一組動畫標籤(統稱 SMIL),能直接寫在要動的元素裡面當子標籤,不需要 CSS,也不需要 JS。最常用的有三個。

💡 以下範例都是抽出動畫的部分,完整結構可以參考 Codepen 範例。

1. <animate>:讓「屬性」隨時間變化

通用性最高的標籤。用 attributeName 指定屬性,再給 from(初始值) 與 to(終點值),或是改變路徑的 values,最後加上 dur(動畫持續時間)就會動。

<circle cx="50" cy="60" r="0" fill="#8fbf7f">
  <animate attributeName="r" from="0" to="18" dur="0.6s" fill="freeze"/>
</circle>

<!-- 路徑變形的寫法:在 `values` 寫好幾組 `d` -->
<path d="M50 10 C70 40 80 60 50 100 C20 60 30 40 50 10 Z" fill="#e8743b">
  <animate attributeName="d" dur="1.2s" repeatCount="indefinite"
    values="M50 10 C70 40 80 60 50 100 C20 60 30 40 50 10 Z;
            M50 24 C76 46 72 68 50 100 C28 68 24 46 50 24 Z;
            M50 10 C70 40 80 60 50 100 C20 60 30 40 50 10 Z"/>
</path>

範例 gif

它能動的是 SVG 的屬性,像是 r、cx、x、opacity、fill、路徑的 d,甚至濾鏡的 baseFrequency。CSS 能改的多半是元素整體的變換(位移、旋轉、縮放),要改形狀本身或濾鏡參數就比較難。

💡 路徑變形有個限制:每組 d 的路徑指令「數量、種類、順序」要一致(上面都是 M C C Z),才能平滑補間。SVG 2 規格(§9.3.2 The d property)寫的是,結構不同時改用離散動畫,也就是沒有中間畫面,直接跳換。

2. <animateTransform>:專門處理 transform

transform 的值是函式(例如 rotate(…)),不能像數字一樣直接補間,所以另外有個專門的標籤,用 type 指定是哪一種變形:translate、scale、rotate、skewX、skewY。attributeName 固定寫 transform。

範例 - 吊燈以頂端為軸心左右擺動:

<g>
  <animateTransform attributeName="transform" type="rotate"
    values="-6 50 0; 6 50 0; -6 50 0"
    keyTimes="0; 0.5; 1"
    calcMode="spline" keySplines=".45 .05 .55 .95; .45 .05 .55 .95"
    dur="3s" repeatCount="indefinite"/>
  <line x1="50" y1="0" x2="50" y2="42" stroke="#6b5b45" stroke-width="3"/>
  <path d="M28 76 Q50 28 72 76 Z" fill="#f2b134"/>
</g>

範例 gif

rotate 的值可以帶旋轉中心,格式是 "角度 cx cy"。上面的 -6 50 0 就是「繞著 (50, 0) 轉 -6 度」,中心點是直接寫在值裡(數值是指 SVG 座標)。

3. <animateMotion>:讓元素「沿著路徑」移動

有兩種作法,用 path 直接寫路徑,或是放一個 <mpath> 引用現成的 <path>。咖啡杯的蒸氣就是三顆氣泡沿著同一條彎曲路徑往上飄:

<path id="steamPath" d="M50 92 C28 76 72 58 50 40 S40 14 52 4" fill="none"/>

<circle r="5" fill="#9aa7b4">
  <animateMotion dur="3s" repeatCount="indefinite">
    <mpath href="#steamPath"/>
  </animateMotion>
  <animate attributeName="opacity" values="0;.8;0" dur="3s" repeatCount="indefinite"/>
</circle>

加上 rotate="auto",元素還會隨著路徑的方向轉向,適合箭頭、小飛機這類有方向性的東西。

範例 gif

CSS 也有同樣的功能,叫 offset-path 搭配 offset-distance,做的是同一件事。不過它比較新,Safari 要 18 以上才支援,專案如果還要顧到舊版 iOS,就不能依賴它。

補充:標籤共通的設定

以上標籤共用同一套時間相關的屬性,剛好跟 CSS 動畫可以一一對照:

設定 作用 對應 CSS
dur 單次時長 animation-duration
repeatCount="indefinite" 重複次數 animation-iteration-count: infinite
begin 開始時間;可以接別的動畫結束 animation-delay(串接要自己算)
fill="freeze" 結束後停在終點 animation-fill-mode: forwards
values + keyTimes 多個關鍵值與各自的時間點 @keyframes 的百分比
calcMode="spline" + keySplines 每段之間的緩動 animation-timing-function: cubic-bezier()

瀏覽器支援度方面,SMIL 很早就有了:Chrome 5、Safari 6、Firefox 4 起支援,在 Can I use 上標示為 Baseline「Widely available」,全球使用率約 97%。


二、SMIL vs. SVG 內 CSS

如果不想要用 SMIL,其實,也可以在 SVG 內放 <style>,直接在內部用 @keyframes、animation 等CSS 屬性。

<svg viewBox="0 0 100 100">
  <style>
    .sw { 
      transform-origin: 50px 0;
      animation: sw 3s ease-in-out infinite alternate;
    }
    @keyframes sw { 0% { transform: rotate(-6deg); } 100% { transform: rotate(6deg); } }
  </style>
  <g class="sw">…吊燈…</g>
</svg>

對於熟悉 CSS 的人,這樣寫比較容易理解,好像不必特別去用 SMIL。先別急,我們來看這兩種寫法各自的特色與擅長的地方。

動畫串接

SMIL 能用 begin="{動畫標籤 id}.end" 把兩段動畫標籤串起來,意思是「等指定 id 動畫那段結束,就開始這段」,像是這個例子中,圓球先進場從 0 長到定位,接著上下循環漂浮:

<circle cx="50" cy="60" r="0" fill="#8fbf7f">
  <animate id="intro" attributeName="r" from="0" to="18" dur="0.6s" fill="freeze"/>
  <animate attributeName="cy" values="60;48;60" dur="2s"
           begin="intro.end" repeatCount="indefinite"/>
</circle>

對比之下,CSS 的做法是把兩個動畫並列,第二個用 animation-delay 手動等:

circle {
  animation:
    pop 0.6s ease-out,
    bob 2s ease-in-out 0.6s infinite;   /* 0.6s 要自己對齊進場時長 */
}

差別在於,SMIL 是事件關係,進場時長改成 1 秒,循環會自動跟著延後;CSS 是時間累計,改了進場時長,後面的 0.6s 得記得一起改,忘了就會重疊或出現空檔。

💡 注意 begin="intro.end" 裡的 id 不要用連字號。之前把 id 取成 intro-f5,進場正常,但後面的漂浮卻一直沒有開始,改成 introf5 就正常了。

載入方式:決定動畫能不能控制

動畫寫進 SVG 之後,還有一個要注意的點,那就是這張 SVG 是怎麼被載入的。不同的載入方式,外面能做的事差很多。

範例裡用同一盞吊燈,做了三種組合:

組合 外部能控制什麼
SMIL + inline <svg> 可以用 SVG 元素的方法控制 SMIL 時間軸,例如 pauseAnimations()、unpauseAnimations()、setCurrentTime(0)
SVG 內 CSS + inline <svg> animation-play-state 可暫停;CSS 變數(例如 --swing)可以從外面調角度
SVG 內 CSS + <img src> 沒有,變數與 JS 都碰不到 <img> 內部

範例 gif

從 Codepen 範例 中可以看到:

  • pauseAnimations() 等方法只管 SMIL,對 CSS 動畫沒有作用。
  • <img> 載入的 SVG 是一個獨立小文件,只能以 SVG 裡寫死的預設值播放,外面無法暫停,也沒辦法透過 CSS 變數去調整它。

另外要小心一件事,inline 時,SVG 裡的 <style> 是整份文件共用的,選擇器與 @keyframes 名稱都沒有隔離。CSS 設定容易互相覆蓋。所以需要幫選擇器和 keyframes 名稱加上獨一無二的前綴(<img> 載入就沒有這個問題)。


三、使用時機

「版面」與「動作」拆分

回到今天開頭的目標,想要讓動畫留在圖片內。當使用到它的時候,只需要決定外層的事情,也就是 SVG 圖片有多大、要定位在哪裡?

全部寫在圖片使用的地方也不是不行,但被使用的地方一多,代表需要重複多份 CSS 設定。或者要將設定拉出來放到全域,可是對於只是特定圖片使用到的動畫效果,這樣做又有些殺雞焉用牛刀。再加上,為了下 CSS 的 class,可能需要把一張 SVG 拆成好多張零件,例如一條藤蔓上有多片會晃動的葉片,讓檔案管理更加複雜。

把動畫寫在 SVG 內,可以很好地將只屬於圖片的設定封裝起來,不會與外部互相干擾。SVG 的事,就留在 SVG 上。

分析其他狀況

以下狀況會考慮使用 SMIL:

  • 形狀變形、濾鏡參數這類 CSS 比較難做的效果,用 <animate> 剛剛好。
  • 動畫跟圖綁在一起的裝飾,例如到處被引用的背景裝飾,或者是動畫設定只有該裝飾會用到,與圖片有強烈綁定關係。

可能不適合的狀況:

  • 需要 JS 控制播放速度、串接的角色動作,回到 Day23 的 WAAPI 比較合適。
  • 與元件狀態互動的動畫,例如依狀態切換速度、角度,用 CSS 變數處理會比較順手。

💡 Day23:CSS 變數解決的是「差異」,SVG 動畫解決的是「動畫跟誰走」。


.
.
.
前幾天都用 CSS 寫動畫,今天則介紹了 SVG 的動畫標籤:<animate> 改屬性、<animateTransform> 處理 transform、<animateMotion> 沿路徑移動;也看到載入方式會決定外面能不能控制。原本以為「讓動畫跟著圖走」只是換個地方寫,實際做下去才發現,重點應該在於為什麼要這樣拆分,有哪些好處與限制。

Day24 完成,剩 6 天! ズイ₍₍(ง˘ω˘)ว⁾⁾ズイ


參考資料


上一篇
【Day 23】動畫進階:@property 協助管理、WAAPI 動態設定
系列文
從實務需求出發:前端視覺與 UI 互動開發實踐 共 24 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言