morphicons:讓任意 stroke icon 平滑變形
你可以使用 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 採取了嚴謹的設計:若開發者沒有傳入 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 採用純數學的管線解決上述問題,核心不觸碰 DOM,僅產出對應的 d 字串與數值資料:[2]
icon A ─┐
├→ normalize → resample → match → align → PLAN → interpolate(t) → serialize → d
icon B ─┘
- 降維為三階貝氏曲線(Cubic Bézier):將所有輸入圖元統一轉換為三階貝氏曲線。直線會升階,二次貝氏曲線會轉換,圓形切為 4 段,弧線則根據 SVG 規格切成小於等於 90 度的曲線再轉為三階貝氏曲線,最後產出一組帶有開閉旗標的 subpath。[2]
- 弧長重採樣:每一條 subpath 會以 8 點 Gauss-Legendre 積分計算實際弧長,並重新採樣為固定的 N = 64 個等弧長點(產生
Float64Array(2N)陣列)。切線不連續超過閾值的轉角(如 Check 的尖端)會被鎖定為採樣點,以確保形狀改變時尖角變化自然。封閉路徑則只鎖定轉角,不鎖定起點位置。[2] - 特徵配對(Matching)與路徑補齊:
- 2D Procrustes 對齊:
考慮順時針與逆時針的方向對應。 對於封閉路徑,會評估 N 個循環位移與 2 個方向(約 4000 次評估),並計算 subpath 配對成本:dist(centroids) + 0.35 · |ΔL|。 * 當兩端 subpath 數量不等時,會透過滿射複製路徑,使多餘的路徑在飛行中像細胞分裂般分開,靜止後再 snap 回目標路徑。[2]
透過封閉形式的 2D Procrustes 演算法,在無須 SVD(奇異值分解)的情況下,最小化兩組點雲之間的平方誤差:
Σ |σ · R(θ) · (aᵢ − c_A) − (bᵢ − c_B)|²
藉此計算出最佳相似變換的角度 θ 與縮放比例 σ。藉由最小化 residual,當兩個 icon 只差旋轉時(例如 arrow-right 到 arrow-down),能自動算出 θ = 90° 的純旋轉。 演算法預設會對個別 subpath 運算,以實現漢堡選單的折疊效果。如果全域 residual 小於 5e-3,則會改為整顆 icon 共用相同的旋轉角度與縮放,避免元素在旋轉時散開。對稱圖形則會透過加入旋轉懲罰項 score = res + λ · |θ| / π (λ = 0.05) 來選擇最短旋轉路徑。[2]
- 極座標插值(Polar Interpolation):
將插值公式拆解為相似變換與殘差的結合:
P(t) = c(t) + σ*^t · R(t · θ*) · [(1−t) · aᶜᵢ + t · b̃ᵢ]
這能避免弦向插值導致的形狀在途中縮水、剪切,使旋轉與縮放能在 spring 衝過 t > 1 時自然超調。若整顆圖示全等,subpath 質心會繞全域質心移動,保持剛體運動,避免箭頭在途中往內凹縮。[2]
- 彈簧動力學(Spring Physics):
使用半隱式歐拉法(子步 h = 1/240 s)模擬阻尼諧振子 ẍ = k · (1 − x) − c · ẋ。在中途插入新的 morphTo 時,會以當前中間形狀重算 plan,並保留當前速度(限制在 ±14 之間)。[2]
框架接法與操作模式
安裝與套件格式
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]
import { Menu, X } from "lucide" // 這是 IconNode 資料,而非 lucide-react 的 React 元件
三種操作模式
1. Uncontrolled(自動動畫)
當屬性改變時,元件會自動使用 spring 補間。官方表示這是九成以上的使用場景。[2]
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]
<MorphIcon from={Menu} to={X} progress={dragProgress} />
3. Imperative(指令式控制)
適用於需要精確控制播放序列的場景,透過 ref 調用方法。[2]
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:
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 環境):
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]
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 倉庫 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 時需注意以下限制與不適用的場景: 不適用的圖示類型(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
先把邊界講清楚,工具才能變成可靠流程。
Signals
Visits
--
Waiting for Cloudflare metrics.