任意 stroke 图标互相变形——旋转不靠手写,从数学里自己长出来
更新于 2026-08-29 · 数据来源:GitHub API(采集于 2026-08-29)
← 返回汇总图标动画不是声明出来的,是解出来的
通用 stroke 图标 morph 库(Lucide / Tabler / Heroicons / Iconoir / 自绘 path 通吃):2D Procrustes 分析求出两图标间的最优相似变换,再在「自然空间」里插值——该旋转的自动旋转,该变形的平滑变形。零运行时依赖,核心 ~7KB gzip。适合一切需要图标状态切换动画的 Web / RN 界面。
2026-08-01 创建,4 周冲到 2,146 stars · 8 个 release(v1.7.1 / 2026-08-28)· 3 位贡献者 · 156 tests / ~13,700 断言 · fork 52 · issues 仅 3 open
旋转 90° 的两个箭头,逐点 lerp 走的是两点间直线(弦)——飞行中途形状收缩、剪切、扭曲变形。要绕开它就得手工拆解成「旋转 + 缩放 + 位移」分步动画,写起来极其琐碎。
现有方案(如 SVG path morph 库)要求为每对图标手工声明「这俩是旋转关系,转 90°」。图标对是笛卡尔积,声明成本爆炸,没人维护得动。
morphicons 的答案:闭式解出最优相似变换,在其自然空间插值——如果一对图形本质是旋转关系,就自动纯旋转;不是,就在对齐坐标系里变形。arrow-right → arrow-down 自动解出 θ=90°,无任何人工声明。
Float64Array(2N),对应关系从「结构匹配」退化为「索引对索引」;弧长用 8 点 Gauss-Legendre 求积S_xx = Σaₓbₓ S_xy = Σaₓbᵧ S_yx = Σaᵧbₓ S_yy = Σaᵧbᵧ (已居中) θ* = atan2(S_xy − S_yx, S_xx + S_yy) σ* = [cosθ*(S_xx+S_yy) + sinθ*(S_xy−S_yx)] / Σ‖aᵢ‖² residual = √(Σ|σRa−b|² / Σ‖b‖²)
residual ≈ 0 ⇒ 同一形状仅旋转/缩放。逐子路径跑 Procrustes(汉堡「折叠」而非整块自旋),再用全局混合检测整体全等(<5e-3 时所有子路径共享同一 θ,σ——箭头两部分同向旋转)。
平局裁决:反转对称形状(直线)两个方向 residual 同为 0,评分 score = res + 0.05·|θ|/π——先保形变最小,再取最短旋转。数学看不见、眼睛看得见的细节。
P(t) = c(t) + σ*ᵗ · R(t·θ*) · [(1−t)·aᶜᵢ + t·b̃ᵢ] c(t) = lerp(c_A, c_B, t) 平移走直线 t·θ* 角度走短弧(θ* ∈ (−π, π]) σ*ᵗ = exp(t·ln σ*) 缩放走对数空间(ℝ⁺ 的测地线)
r(1−cos(θ/2)))。修正:质心乘坐全局相似绕全局质心飞行,全程刚体,无阈值无特判连续退化ẍ = k·(1 − x) − c·ẋ 半隐式 Euler, h = 1/240s (k=420 时 ω≈20.5,ωh≪2 稳定域宽裕) 收敛判据: |1−x| < 0.001 ∧ |v| < 0.02
过冲时极坐标公式自然外推——spring 与数学核心互相成全。
| preset | k | c | ζ | 性格 |
|---|---|---|---|---|
smooth | 170 | 26 | 1.00 | 临界阻尼,零过冲 |
snappy | 420 | 30 | 0.73 | 快,微妙回弹 |
bouncy | 300 | 14 | 0.40 | 俏皮弹跳 |
真可中断:飞行中 morphTo 从当前中间形状重规划(渲染缓冲本来就是 N 点采样,直接当源),x 归零而速度保留(clamp ±14)。连点不跳变,spam 也鲜活。
单包 subpath exports(./dom ./react ./vue ./svelte ./element ./astro),ESM only,sideEffects: false——按 import 付费,没用的能力零字节。
| 模式 | 写法 | 适合 |
|---|---|---|
| Uncontrolled | <MorphIcon icon={open ? X : Menu} /> | 90% 场景:改 prop 自动 morph,状态在外面,动画是实现细节 |
| Controlled | from={Menu} to={X} progress={drag} | 手势 / 滚动驱动,显式进度无 spring |
| Imperative | ref.morphTo(Check) / ref.set(X) | 序列动画;set 直接跳 |
import { Menu } from "lucide"(vanilla 包 IconNode),不是 lucide-react 组件——数据包与组件包设计上共存,tree-shake 干净| 适配器 | 方向 | 作用 |
|---|---|---|
svgToIcon | SVG 标记进 | Iconify JSON / 网页拷贝的 svg / shadcn registry 的 path → IconInput;解析时拒绝无法诚实 morph 的输入(fill 图标、transform)并给出清晰报错 |
maskTarget | CSS-mask 出 | UnoCSS/Tailwind 图标插件渲染的 i-lucide-* span(DOM 里没有 path 可写)——隐藏双缓冲 mask 对,每次写 d 换引用,强逼 Chromium 重解码、WebKit 重绘 |
canvasTarget | 2D canvas 出 | Path2D 原生解析 d,每帧一次 stroke——worker/OffscreenCanvas 离主线程、游戏 HUD、WebGL 纹理、动态 favicon;最轻适配器 0.48KB |
适配器按格式不按厂商(没有 /iconify 也永远不会有);每个独立模块独立 size gate,两个适配器一旦耦合 CI 立刻报警。兼容条件就四条:stroke 中心线绘制 · 有数据(d 或节点表)· 同坐标系(fitIcon 一行重栅格化 Carbon 32→24)· 均匀描边。已知合格 24×24 全家桶:Lucide / Tabler / Heroicons outline / Iconoir / Akar / Untitled UI / Hugeicons。
| 入口 | 实测 | gate |
|---|---|---|
core | 6.60 KB | 7 KB |
/dom | 7.12 KB | 7.5 KB |
/react | 8.01 KB | 8.5 KB |
/element | 8.86 KB | 9.5 KB |
~10% 余量的反回归绊线:能力真要变大就重新谈判数字,但悄悄变大不被接受。附赠:156 tests / ~13,700 断言,strict typecheck ×6,biome 格式化。
$ bun add morphicons // React:与 lucide-react 同 props 面,数据来自 vanilla lucide 包 import { MorphIcon } from "morphicons/react"; import { Menu, X } from "lucide"; // data, not components <button onClick={() => setOpen(o => !o)} aria-expanded={open}> <MorphIcon icon={open ? X : Menu} spring="snappy" /> </button> // 无框架: import { createMorph } from "morphicons/dom"; const m = createMorph(pathEl, Menu); m.morphTo(X, "snappy"); // 或 { stiffness, damping } 自定义
无 wrapper、无 AnimatePresence、无 key、无 from/to 配对、无配置——状态在外面,动画是 prop 变化时组件自己捡起来的实现细节。
Playground 内嵌从零实现的 mini-morph 管线(d 解析 → 弧长重采样 → Procrustes → 极坐标插值 → spring):切换图标对、实时读出 θ*/σ*/residual、并排对比极坐标 vs 朴素 lerp 的弦塌陷,再拖 t 滑杆逐帧解剖。
纯前端实现,零网络依赖 · 算法与 README 公式同构,θ=90° 涌现旋转已对拍验证
「把 UI 动画问题翻译成数学问题再闭式解掉」的范本。图标 morph 是小切口,但同一管线(Procrustes + 极坐标插值 + 弹簧)天然外延到任意 stroke 图形的状态动画。4 周的质量表现(CI 闸门 / ADR / 引擎 ulp 级考量)在同类新项目里罕见。