---
slug: morphicons-stroke-icon-morphing
title: morphicons：讓任意 stroke icon 平滑變形
status: published
excerpt: 用 path 對齊與 interpolation 把圖示轉場做成可重用的動態元件。
category: Design
tags: [animation, icons, frontend]
author: Seer
author_role: Author
read_time: 8 min
cover: "/static/morphicons-stroke-icon-morphing-cover.png"
closing_note: "先把邊界講清楚，工具才能變成可靠流程。"
published_at: "2026-08-20T00:00:00Z"
updated_at: "2026-08-20T00:00:00Z"
---

你可以使用 [morphicons](https://github.com/guillermolg00/morphicons) 讓網頁或 App 中的任意描邊圖示（stroke icon）流暢地變形至另一個圖示。你不需要為每一對圖示手寫複雜的旋轉群組，也不必手動配對 from 到 to 的動畫路徑。[1][2]

這款工具非常適合應用於以下互動元件的微動效（Micro-interactions）中，為使用者提供精緻的狀態變更反饋：
* **選單開關**：Menu ↔ X 的無縫切換。
* **播放控制器**：Play ↔ Pause ↔ Stop 的動態轉換。
* **展開與收合**：Chevron-down ↔ Chevron-up 或 Plus ↔ Minus 的收合狀態指示。
* **主題切換**：太陽 ↔ 月亮（Sun ↔ Moon）的漸變。
* **進度控制與手勢操作**：例如隨著下拉更新、捲動進度或滑桿拖曳，以 controlled 模式精確控制變形進度。[2]

在可及性（Accessibility）的實踐上，[morphicons](https://github.com/guillermolg00/morphicons) 採取了嚴謹的設計：若開發者沒有傳入 `label` 屬性，元件預設會加上 `aria-hidden="true"` 以對螢幕閱讀器隱藏圖示；當傳入 `label` 時，則會將角色宣告為 `role="img"`，並在 SVG 中自動嵌入 `<title>` 標籤以朗讀說明。[2]

針對系統的「減少動態效果」（Reduced Motion）偏好，預設值為 `"never"`（一律播放變形動畫），因為開發者認為短小的微動效直接切換容易顯得介面破碎。如果希望嚴格遵循作業系統的無障礙動態設定，可手動將 `reducedMotion` 屬性設定為 `"user"`，此時在開啟減少動態的系統上，動畫將瞬間切換，亦可設定為 `"always"` 來一律停用動畫（例如在進行自動化測試或產生快照時）。[2][8]

## 傳統 Icon Morphing 的問題

傳統上，若要實作兩個 icon 之間的 morphing 動畫，通常會面臨以下問題：
* **路徑數量與控制點不對等**：直接對 SVG 的 `d` 屬性進行線性插值（lerp），常會導致飛行過程中的形狀嚴重扭曲、縮小或撕裂。
* **手寫配對成本極高**：為了獲得好看的旋轉效果，開發者往往需要使用 SMIL、Lottie，或手寫特定的旋轉群組與 Framer Motion 的 `layoutId`。這代表每一對圖示的轉換都需要專屬的動畫檔案或程式碼，無法做到通用化（universal morphing）。[2]
* **第三方框架相依性過重**：許多現存的 SVG 變形庫體積龐大，或者強依賴於特定的動畫框架，難以在極低 bundle size 的前提下支援 SSR（伺服器端渲染）、Web Components 或純 Canvas 等多樣化環境。[2][6]

## 核心演算原理

[morphicons](https://github.com/guillermolg00/morphicons) 採用純數學的管線解決上述問題，核心不觸碰 DOM，僅產出對應的 `d` 字串與數值資料：[2]

```text
icon A ─┐
        ├→ normalize → resample → match → align → PLAN → interpolate(t) → serialize → d
icon B ─┘
```

1. **降維為三階貝氏曲線（Cubic Bézier）**：將所有輸入圖元統一轉換為三階貝氏曲線。直線會升階，二次貝氏曲線會轉換，圓形切為 4 段，弧線則根據 SVG 規格切成小於等於 90 度的曲線再轉為三階貝氏曲線，最後產出一組帶有開閉旗標的 subpath。[2]
2. **弧長重採樣**：每一條 subpath 會以 8 點 Gauss-Legendre 積分計算實際弧長，並重新採樣為固定的 **N = 64** 個等弧長點（產生 `Float64Array(2N)` 陣列）。切線不連續超過閾值的轉角（如 Check 的尖端）會被鎖定為採樣點，以確保形狀改變時尖角變化自然。封閉路徑則只鎖定轉角，不鎖定起點位置。[2]
3. **特徵配對（Matching）與路徑補齊**：
   * 考慮順時針與逆時針的方向對應。
   * 對於封閉路徑，會評估 N 個循環位移與 2 個方向（約 4000 次評估），並計算 subpath 配對成本：`dist(centroids) + 0.35 · |ΔL|`。
   * 當兩端 subpath 數量不等時，會透過滿射複製路徑，使多餘的路徑在飛行中像細胞分裂般分開，靜止後再 snap 回目標路徑。[2]
4. **2D Procrustes 對齊**：
   透過封閉形式的 2D Procrustes 演算法，在無須 SVD（奇異值分解）的情況下，最小化兩組點雲之間的平方誤差：
   ```text
   Σ |σ · R(θ) · (aᵢ − c_A) − (bᵢ − c_B)|²
   ```
   藉此計算出最佳相似變換的角度 `θ*` 與縮放比例 `σ*`。藉由最小化 residual，當兩個 icon 只差旋轉時（例如 arrow-right 到 arrow-down），能自動算出 `θ = 90°` 的純旋轉。
   演算法預設會對個別 subpath 運算，以實現漢堡選單的折疊效果。如果全域 residual 小於 `5e-3`，則會改為整顆 icon 共用相同的旋轉角度與縮放，避免元素在旋轉時散開。對稱圖形則會透過加入旋轉懲罰項 `score = res + λ · |θ| / π (λ = 0.05)` 來選擇最短旋轉路徑。[2]
5. **極座標插值（Polar Interpolation）**：
   將插值公式拆解為相似變換與殘差的結合：
   ```text
   P(t) = c(t) + σ*^t · R(t · θ*) · [(1−t) · aᶜᵢ + t · b̃ᵢ]
   ```
   這能避免弦向插值導致的形狀在途中縮水、剪切，使旋轉與縮放能在 spring 衝過 `t > 1` 時自然超調。若整顆圖示全等，subpath 質心會繞全域質心移動，保持剛體運動，避免箭頭在途中往內凹縮。[2]
6. **彈簧動力學（Spring Physics）**：
   使用半隱式歐拉法（子步 `h = 1/240 s`）模擬阻尼諧振子 `ẍ = k · (1 − x) − c · ẋ`。在中途插入新的 `morphTo` 時，會以當前中間形狀重算 plan，並保留當前速度（限制在 ±14 之間）。[2]

## 框架接法與操作模式

### 安裝與套件格式
```bash
bun add morphicons
```
套件本身僅提供 ESM 格式。各框架 binding 為選裝（optional peer dependencies）：[2][6]

| binding | peer |
|---|---|
| `morphicons/react` | `react >= 18` |
| `morphicons/vue` | `vue >= 3.3` |
| `morphicons/svelte` | `svelte >= 5` |
| `morphicons/react-native` | `react-native >= 0.71`、`react-native-svg >= 14` |
| `morphicons/element`、`morphicons/astro` | 無 peer |

圖示來源需使用原始資料，不要傳入封裝後的元件包。以 Lucide 為例，應引入原始的 `IconNode` 資料：[2]
```ts
import { Menu, X } from "lucide" // 這是 IconNode 資料，而非 lucide-react 的 React 元件
```

### 三種操作模式

#### 1. Uncontrolled（自動動畫）
當屬性改變時，元件會自動使用 spring 補間。官方表示這是九成以上的使用場景。[2]
```tsx
import { MorphIcon } from "morphicons/react"
import { Menu, X } from "lucide"

<button onClick={() => setOpen(o => !o)} aria-expanded={open}>
  <MorphIcon icon={open ? X : Menu} spring="snappy" />
</button>
```

#### 2. Controlled（進度受控）
適用於手勢拖曳、捲動進度或自訂播放器的情境。此時 spring 不會介入，進度完全由外部決定。當 `from` 與 `to` 同時存在時，會忽略 `icon` 屬性。[2]
```tsx
<MorphIcon from={Menu} to={X} progress={dragProgress} />
```

#### 3. Imperative（指令式控制）
適用於需要精確控制播放序列的場景，透過 ref 調用方法。[2]
```tsx
const ref = useRef<MorphHandle>(null)
<MorphIcon ref={ref} icon={Menu} />
ref.current?.morphTo(Check) // 播動畫變形至目標
ref.current?.set(X)         // 瞬間切換至目標
```
Astro 元件與 `<morph-icon>` 自訂元素也支援這三種模式，其控制點改為 DOM 元素的 properties 與 methods。[2]

### 其它渲染接法：DOM 與純 Core 呼叫

若在無框架的環境下，可直接使用 DOM driver：
```ts
import { createMorph } from "morphicons/dom"

const m = createMorph(pathEl, Menu)
m.morphTo(X, "snappy")
m.set(Check)
m.seek(X, 0.4)
m.destroy()
```

或者使用純核心（Core）進行計算，自行處理渲染邏輯（例如 Node.js 或 worker 環境）：
```ts
import { resampleIcon, buildPlan, interpPolar, allocOutputs, serialize } from "morphicons"

const plan = buildPlan(resampleIcon(Menu), resampleIcon(X))
const out = allocOutputs(plan)
interpPolar(plan, 0.5, out)
const d = serialize(out, plan.items.map(it => it.closed))
```

### 適配器（Adapters）
`morphicons/adapters` 提供了不同渲染目標與輸入格式的銜接：[2][8]
* **`svgToIcon`**：將任意 SVG 字串、Iconify body 等格式轉為 `IconInput`，並自動將非 24×24 的圖示 `fitIcon` 到 24 座標格。建議在 module scope 進行轉換以利 plan 快取。[2]
  ```ts
  import { svgToIcon } from "morphicons/adapters"
  const MENU = svgToIcon(menuBody)
  ```
* **`maskTarget`**：專為 UnoCSS 或 Tailwind CSS 等使用 CSS mask 的圖示設計。為解決 Chromium 重新解碼 data-URI 及 WebKit 不重繪的問題，採用雙緩衝（dual-buffering）機制，在每幀切換 `mask-image`，適合開關或短列表情境。[2][8]
* **`canvasTarget`**：透過 `Path2D` 在 HTMLCanvasElement 或 OffscreenCanvas 上繪製。如果整合至已有 render loop 的系統（如 Konva、Chart.js 或遊戲引擎），可將 `onWrite` 作為 dirty 訊號進行局部 composite，而不需要交出主繪圖 context。[2][8]

## 數據驗證

專案的靜態指標與效能數據查核如下：
* **查核對象**：[morphicons](https://github.com/guillermolg00/morphicons) 倉庫 `main` 分支之 commit `dac8173cba97608b02cad72840bf6698dbbc7235`，對應 npm 版本 `v1.7.0`（發佈於 2026-08-13，授權條款為 MIT，作者為 Guillermo）。[3][4][5][6]
* **公開數字（截至 2026-08-16）**：
  * Star 數：1210，Fork 數：27，Watch 數：3。[3]
  * npm 無任何 external dependency。[6]
  * 測試套件規模：`bun test` 指令在 README 中記載為 156 個測試，而在 1.6.0 變更日誌中顯示已擴充至 225 個測試。[2][8]
* **渲染與計算效能**：
  * 在 N = 64 時，`plan(A, B)` 計算時間小於 1 ms（開路徑約 0.01 ms，閉路徑約 0.06 ms，複雜病態路徑約 0.42 ms）。[2]
  * 記憶體優化：運行期間不分配新的數值陣列，僅輸出 `d` 字串，並透過 `WeakMap` 對 `normalize` 與 `plan` 進行 reference cache。全頁面元件共用單一 `requestAnimationFrame`（rAF）驅動器。[2]
  * SSR 與 Hydration：1.4.2 版本起將 canonical `d` 的小數點量化至 4 位，以避免不同瀏覽器 JS 引擎在 arc 到 cubic 的浮點數尾差導致 hydration mismatch。變形過程中則輸出 2位 小數，停止後再 snap 回 4 位小數以避免 0.02px 以上的抖動。[2][8]
* **套件與 Bundle Size 閘門（measured / gate）**：[2]
  * core：6.60 KB / 7 KB
  * core + dom：7.12 KB / 7.5 KB
  * react：8.01 KB / 8.5 KB
  * vue：8.04 KB / 8.5 KB
  * react-native：8.37 KB / 9 KB
  * element：8.86 KB / 9.5 KB
  * svgToIcon：3.34 KB / 3.7 KB
  * maskTarget：0.70 KB / 0.8 KB
  * canvasTarget：0.48 KB / 0.55 KB
* *備註*：本次查核僅進行靜態程式碼與文件閱讀，並未實際在本機執行 playground、`bun test` 或在瀏覽器中重測變形流暢度。

## 限制與不適用的 Icon 類型

使用 [morphicons](https://github.com/guillermolg00/morphicons) 時需注意以下限制與不適用的場景：
* **不適用的圖示類型（Non-stroke Icons）**：
  * **填色與外框填色圖示**：如 Material Symbols、Bootstrap Icons、Remix Icons 或 Phosphor Icons 的 solid 版本。此庫只適合「以描邊中心線幾何為基礎（`fill="none"` 且顏色透過 `stroke` 控制）」的圖示。填色圖示雖能解析，但在變形飛行過程中的視覺效果極差。[2]
  * **含有 `<g>` 群組或 `transform` 屬性的圖示**：解析管線會拒絕或報錯，無法直接進行 Procrustes 對齊與插值。[2]
  * **複雜插圖與 Logo**：此元件並非通用的 SVG 任意形狀補間器（插值器），應避免用於變形複雜的向量插圖或商標 Logo。[2]
* **座標系格線差異**：
  * 輸入圖示必須位於相同的座標格線上才能直接變形（如 Lucide、Tabler、Heroicons outline、Iconoir 預設皆為 24×24）。
  * 跨不同座標格的圖示（如 Carbon 32、Teenyicons 15）必須先調用 `fitIcon` 進行縮放對齊。若跳過對齊，Procrustes 演算法會將格線差誤判為縮放因子 `σ`，導致插值圖示超出 SVG 的 viewBox。[2]
* **Reduced Motion 預設忽略系統設定**：
  預設值 `"never"` 會忽略 OS 減少動態的設定。如需遵循，必須自行在元件上顯式傳入 `reducedMotion="user"`。[2][8]
* **第三方授權獨立性**：
  雖然 [morphicons] 庫本身採用 MIT 授權，但官方 playground 內展示的 Lucide、Feather 與 Tabler 等圖示，其授權各自獨立（分別為 ISC、MIT、MIT），商業使用時需留意其版權聲明。[2][5][7]

## Sources

[1] https://github.com/guillermolg00/morphicons — guillermolg00/morphicons
[2] https://raw.githubusercontent.com/guillermolg00/morphicons/dac8173cba97608b02cad72840bf6698dbbc7235/README.md — README.md at dac8173
[3] https://api.github.com/repos/guillermolg00/morphicons — GitHub repo metadata
[4] https://api.github.com/repos/guillermolg00/morphicons/releases/latest — GitHub latest release
[5] https://raw.githubusercontent.com/guillermolg00/morphicons/dac8173cba97608b02cad72840bf6698dbbc7235/LICENSE — LICENSE
[6] https://registry.npmjs.org/morphicons/latest — npm morphicons@1.7.0
[7] https://www.morphicons.com — morphicons.com
[8] https://raw.githubusercontent.com/guillermolg00/morphicons/dac8173cba97608b02cad72840bf6698dbbc7235/CHANGELOG.md — CHANGELOG.md
