项目详情

morphicons

任意 stroke 图标互相变形——旋转不靠手写,从数学里自己长出来

GitHub 地址 官方 Playground MIT TypeScript零依赖 · ESM

更新于 2026-08-29 · 数据来源:GitHub API(采集于 2026-08-29)

← 返回汇总
项目速览

一句话定位

图标动画不是声明出来的,是解出来的

通用 stroke 图标 morph 库(Lucide / Tabler / Heroicons / Iconoir / 自绘 path 通吃):2D Procrustes 分析求出两图标间的最优相似变换,再在「自然空间」里插值——该旋转的自动旋转,该变形的平滑变形。零运行时依赖,核心 ~7KB gzip。适合一切需要图标状态切换动画的 Web / RN 界面。

项目速览

核心数据

2.1k
Stars(4 周)
6.6KB
核心 gzip
0
运行时依赖
5
框架绑定

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

为什么存在

图标 morph 的两大老毛病

毛病一:坐标直插会「弦塌陷」

旋转 90° 的两个箭头,逐点 lerp 走的是两点间直线(弦)——飞行中途形状收缩、剪切、扭曲变形。要绕开它就得手工拆解成「旋转 + 缩放 + 位移」分步动画,写起来极其琐碎。

毛病二:旋转组靠人肉声明

现有方案(如 SVG path morph 库)要求为每对图标手工声明「这俩是旋转关系,转 90°」。图标对是笛卡尔积,声明成本爆炸,没人维护得动。

morphicons 的答案:闭式解出最优相似变换,在其自然空间插值——如果一对图形本质是旋转关系,就自动纯旋转;不是,就在对齐坐标系里变形。arrow-right → arrow-down 自动解出 θ=90°,无任何人工声明。

核心魔法

旋转是涌现的,不是声明的

morphicons 极坐标插值 — 沿弧飞行 arrow-right (t=0) σ^t·R(t·θ*) 旋转+缩放同步推进 arrow-down (t=1) 朴素坐标 lerp — 弦塌陷 每点走直线 → 中途长度收缩为 0 再拉长(形状退化) t=0.5 时刻:箭头缩成一点
正确:解出 θ*=90° 后沿圆弧插值 错误:逐点直线插值,中途形状消失
核心机制

七步管线:两个 d 进,一条 morph 出

icon A IconNode / d 串 icon B Lucide 等 stroke 库 ① 归一化 全部转 cubic ② 弧长重采样 N=64 点 + 角点锚定 ③ 对应匹配 方向×起点×子路径 ④ Procrustes θ* σ* 闭式解 PLAN 可缓存可序列化 ⑤ 极坐标插值 interp(t) 由 spring 驱动 t: 0→1 ⑥ 序列化 M x y L x y… settle 时 snap 回 canonical d ⑦ 写入 path 的 d 属性 每帧唯一分配物就是这条字符串 spring ẍ = k(1−x) − cẋ 半隐式 Euler · h=1/240 · 中断保速度
数据面 / 运行时 纯函数预处理(一次,可缓存) 数学核心(④)与时间引擎(spring)
数学核心

① 归一化 + ② 弧长重采样

数学核心

③ 对应匹配 + ④ Procrustes 闭式解

③ 三个子问题
  • 方向:B 的两个遍历方向都试,按评分择优(汉堡折成正确对角线靠它)
  • 起点:闭合轮廓的 N 个圆偏移 × 2 方向全试,取对齐后最优。N=64 暴力 ~4K 次运算,可忽略
  • 子路径配对:代价 = 质心距 + 0.35·|Δ弧长|;等数取最小代价排列,不等数满射分配——多出的子路径原地「细胞分裂」,比「从一点长出来」自然得多
④ 最优相似(无 SVD)
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 σ*)           缩放走对数空间(ℝ⁺ 的测地线)
数学核心

⑦ 弹簧:25 行代码代替一个动画库

ẍ = k·(1 − x) − c·ẋ

半隐式 Euler, h = 1/240s
(k=420 时 ω≈20.5,ωh≪2 稳定域宽裕)

收敛判据: |1−x| < 0.001 ∧ |v| < 0.02

过冲时极坐标公式自然外推——spring 与数学核心互相成全。

presetkcζ性格
smooth170261.00临界阻尼,零过冲
snappy420300.73快,微妙回弹
bouncy300140.40俏皮弹跳

真可中断:飞行中 morphTo 从当前中间形状重规划(渲染缓冲本来就是 N 点采样,直接当源),x 归零而速度保留(clamp ±14)。连点不跳变,spam 也鲜活。

架构

一条硬规则:核心永不碰 DOM

BINDINGS · 薄适配层 react · vue · svelte · react-native · element(<morph-icon> 自定义元素)+ astro SSR 壳 每个都只是 ref + createMorph,镜像测试钉住同一生命周期契约 DRIVER · dom 驱动 setAttribute + rAF · 单例调度器:全局一个 rAF 驱动所有实例 RN 绑定 = dom driver 原封不动 + setNativeProps shim(Fabric 下再由渲染期重申 live d) CORE · 纯函数核心(无 DOM) parse → normalize → resample → match → align → plan → interpolate → serialize + spring 只吃 icon 数据只吐 d 字符串和数字 ⇒ 任何有 rAF 的宿主都能跑
适配层(5 绑定 + SSR 壳) 驱动层(单例 rAF) 纯核心——RN「免费」复用的根源

单包 subpath exports(./dom ./react ./vue ./svelte ./element ./astro),ESM only,sideEffects: false——按 import 付费,没用的能力零字节。

工程哲学

三种模式 × 一个生命周期契约

模式写法适合
Uncontrolled<MorphIcon icon={open ? X : Menu} />90% 场景:改 prop 自动 morph,状态在外面,动画是实现细节
Controlledfrom={Menu} to={X} progress={drag}手势 / 滚动驱动,显式进度无 spring
Imperativeref.morphTo(Check) / ref.set(X)序列动画;set 直接跳
生态位

适配器:进得来,出得去

适配器方向作用
svgToIconSVG 标记进Iconify JSON / 网页拷贝的 svg / shadcn registry 的 path → IconInput;解析时拒绝无法诚实 morph 的输入(fill 图标、transform)并给出清晰报错
maskTargetCSS-mask 出UnoCSS/Tailwind 图标插件渲染的 i-lucide-* span(DOM 里没有 path 可写)——隐藏双缓冲 mask 对,每次写 d 换引用,强逼 Chromium 重解码、WebKit 重绘
canvasTarget2D 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。

工程质量

把「小」和「快」写成 CI 法律

  • plan 极快:N=64 全流程(重采样+匹配+双方向 Procrustes)实测 0.01–0.06ms,病态 12 环才 0.42ms
  • 每帧零数值分配:插值写入 plan 预分配的 Float64Array,一帧唯一分配物是 d 字符串本身
  • 按引用缓存:normalize 挂 WeakMap(Lucide import 是稳定引用,无字符串键不挡 GC);plan 嵌套 WeakMap;中断产生的 plan 永不缓存
  • 单例 rAF:一百个图标同时 morph = 一个循环,settle 即注销
size-limit CI 闸门(测量 / 上限)
入口实测gate
core6.60 KB7 KB
/dom7.12 KB7.5 KB
/react8.01 KB8.5 KB
/element8.86 KB9.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 滑杆逐帧解剖。

打开 Playground

纯前端实现,零网络依赖 · 算法与 README 公式同构,θ=90° 涌现旋转已对拍验证

潜力评估

未来空间

「把 UI 动画问题翻译成数学问题再闭式解掉」的范本。图标 morph 是小切口,但同一管线(Procrustes + 极坐标插值 + 弹簧)天然外延到任意 stroke 图形的状态动画。4 周的质量表现(CI 闸门 / ADR / 引擎 ulp 级考量)在同类新项目里罕见。

增速 500+/周 工程质量稀缺 框架全适配 单作者风险 stroke-only 市场

上一个
Milvus
下一个
Weave Router
1 / 18