为什么直接lerp坐标会让图标变形塌陷?morphicons极坐标插值原理与弹簧超调外推数学解析
【免费下载链接】morphiconsAny icon morphs into any other — universal morphing for stroke-based icons with spring physics. Zero dependencies, ~7 KB gzip.项目地址: https://gitcode.com/gh_mirrors/mo/morphicons
做图标变形动画(icon morphing)时,最直觉的做法是对两个图标的每个点做线性插值:P(t) = (1−t)·A + t·B。但你会发现,动画进行到一半时,图标会"收缩、扭曲、塌陷",像被橡皮泥捏过一样。morphicons 正是为了解决这个问题而生的图标变形动画库——它用**极坐标插值(polar interpolation)**在旋转与缩放的"自然空间"里做动画,再用弹簧物理(spring physics)驱动进度,让任何描边图标都能变成任何其它图标。这篇文章把背后的数学拆解给你看。
一、直接 lerp 坐标:图标为什么会在半路"塌掉"?
先看一个经典场景:把"向右箭头"(arrow-right)变成"向下箭头"(arrow-down)。
这两个图标在几何上只差一次90° 旋转——箭头尖、箭杆的每个点在旋转意义下都是严格对应的。可如果直接对坐标 lerp,每一点会沿着**直线(弦)**飞向目标位置:
- 箭头尖本来应该绕箭杆走一段圆弧,现在却被拽着直线"穿墙";
- 中点帧里,旋转分量还没转到位,而坐标分量已经平移了一大半,两种运动互相"打架",形状既不在 A 上、也不在任何旋转后的 A 上,而是被**剪切(shear)+ 压缩(shrink)**成一个怪形状;
- 描边图标尤其明显:线条的粗细观感来自几何,一旦被挤压,线条会看起来变粗、打结、塌陷。
这就是项目 README 里说的经典病症:"interpolate raw coordinates (shapes shrink and shear in transit)"。
直接 lerp 还隐含第二个坑:对应关系错了。A 图标的第 3 个点,在 B 图标里对应的可能根本不是第 3 个点(起点不同、绕行方向不同、路径数还不一样)。对应错了,再平滑的插值也只会得到一团乱线。
二、对齐与对应:先给每个点找到"正确的伙伴"
morphicons 的管线是:normalize → resample → match → align → plan → interpolate(t) → d,其中前三步全是为插值铺路。
| 步骤 | 做什么 | 源码 |
|---|---|---|
| 归一化 | 把 line/circle/rect/arc 等 SVG 基元统一降阶为三次贝塞尔曲线(圆弧按 SVG 规范 F.6 切成 ≤90° 的弧片,KAPPA = 4/3·tan(π/8) ≈ 0.5523) | src/core/normalize.ts |
| 重采样 | 用 8 点 Gauss-Legendre 数值积分弧长,把每条子路径等弧长重采样成固定 N 个点,拐角(切线跳变 > 22.5°)作为锚点精确保留 | src/core/resample.ts |
| 配对 | 子路径之间用质心距离 + 0.35·|长度差|做代价矩阵求最优配对;条数不等时用"满射"分配——多出来的源路径折叠到同一目标,像"细胞分裂"而不是凭空消失 | src/core/plan.ts |
| 对齐 | 对每对子路径,尝试两种绕行方向 + 闭合环的 N 个循环起点偏移,用闭式2D Procrustes求最优相似变换 | src/core/plan.ts |
Procrustes 是这里的关键:它求的是让Σ|σ·R(θ)·(a−c_A) − (b−c_B)|²最小的相似变换(旋转 θ + 缩放 σ),而且不需要 SVD,一次atan2出结果:
θ* = atan2(Sxy − Syy, Sxx + Syy),σ* 由零导数条件直接给出(见 src/core/plan.ts 的
procrustes函数)
测试套件把它钉成了可执行的规格(test/invariants.test.ts):arrow-right → arrow-down 的两条子路径都独立算出 θ = 90°、σ = 1、残差 < 1e-6;plus → x 算出 45° 旋转 + σ ≈ 1.212。也就是说,旋转不是人肉声明的,是数学自己算出来的。
三、极坐标插值:在相似变换的"自然空间"里动
拿到 (θ, σ) 之后,morphicons 不插值坐标,而是插值相似变换本身——角度线性、缩放对数线性、质心平移,最后作用在"对齐后的残差混合"上。核心公式(src/core/interpolate.ts):
P(t) = c(t) + σᵗ · R(t·θ) · [(1−t)·aC + t·bT]
其中aC是 A 减去质心的点云,bT是 B 被搬进 A 坐标系后的点云(R(−θ)·(b−c_B)/σ)。逐项拆开看为什么它不会塌陷:
- σᵗ = e^{t·lnσ}:缩放是乘法运算,在 log 空间里才是线性的。t 从 0→1,σ 从 1 平滑地指数滑向目标值,不会忽快忽慢;
- R(t·θ):旋转按角度线性推进。若两图标只差旋转(残差 res ≈ 0),残差混合那一项在任意 t 下形状几乎不变,整个图标就是刚体旋转——箭头绕着自己转 90°,而不是被弦线拖着走;
- [(1−t)·aC + t·bT]:只有"两个形状真正不相似"的那部分残差才做线性混合,而它已经被搬进了对齐坐标系里混合,幅度最小;
- t = 0 和 t = 1 精确成立:测试要求两端点误差 < 1e-9(见 test/invariants.test.ts 的 invariant 4),动画首尾与静态图标完全一致。
还有一个精妙的细节叫全局混合(global hybrid)+ 块状运输:如果整个图标在一个统一相似变换下全等,所有子路径共享同一组 (θ, σ),而且子路径的质心不做普通 lerp——质心会"搭乘"这个共享相似变换绕全局质心走圆弧(src/core/plan.ts 中block字段)。源码注释说得很直白:如果质心走弦线,会走进弧线内侧,箭头的尖会在半路"塌向"箭杆。质心绕弧走,图标全程保持刚体。
四、弹簧超调:t > 1 时公式如何"自然外推"
进度 t 不是由缓动曲线给的,而是由一个阻尼谐振子积分出来的(src/core/spring.ts):
ẍ = k·(1−x) − c·ẋ
用半隐式欧拉法按 1/240 秒子步积分(数值稳定裕量充足),三个内置预设对应不同阻尼比 ζ = c/(2√k):
| 预设 | k / c | 阻尼比 ζ | 手感 |
|---|---|---|---|
| smooth | 170 / 26 | 1.00(临界阻尼) | 不超调,平滑落定 |
| snappy | 420 / 30 | 0.73 | 快,轻微超调 |
| bouncy | 300 / 14 | 0.40 | 俏皮,明显回弹 |
欠阻尼弹簧(ζ < 1)会让进度 x冲过 1 再弹回来——也就是 t 会短暂大于 1。这正是极坐标公式设计的用武之地:把 t > 1 直接代入
P(t) = c(t) + e^{t·lnσ} · R(t·θ) · [(1−t)·aC + t·bT]
三项全部沿物理意义方向自然外推:
- 角度 t·θ 转过 90° 再往回摆——图标多转几度又转回来;
- 缩放 e^{t·lnσ} 冲过目标尺寸再收缩;
- 残差项 (1−t)·aC + t·bT 沿 A→B 方向延伸出 B 之后的一小段。
没有任何 if/else 钳位,公式在 t > 1 区域解析地平滑,得到的就是"橡皮筋拉过头再弹回"的超调动画。这就是"弹簧超调外推"的全部秘密:把超调留给弹簧,把形状交给解析公式。
这套机制还免费获得了可打断性:飞行途中再次 morphTo 时,弹簧保留速度(钳位在 ±14)从零重启,计划从当前中间形状重建——疯狂连点也不会跳变。test/invariants.test.ts 的 invariant 7 专门模拟了连续 4 次中途改目标,要求全程无 NaN、速度连续、最终精确落定。
五、把这套数学用进自己的项目
如果你用 React/Vue/Svelte/React Native 绑定,这些数学都是零配置的:
import { MorphIcon } from "morphicons/react"; import { Menu, X } from "lucide"; // 图标是数据,不是组件 <MorphIcon icon={open ? X : Menu} spring="snappy" />而如果你想亲手调管线,纯核心(无 DOM 依赖,gzip 约 7 KB)暴露了全部数学环节(见 src/index.ts):resampleIcon → buildPlan → interpPolar(plan, t, out) → serialize,t 由你任意给——包括大于 1 的超调值。interpLinear也保留着,专门用来和极坐标模式做视觉对比。
延伸阅读:架构决策 docs/adr/0001-frozen-core-contracts-adapters-entry.md 解释了核心为什么冻结在两个契约上;实验场 morphicons-spike.html 和 playground/ 可以直接打开看插值效果。
小结
- 直接 lerp 塌陷的根因:坐标空间里的直线混合,与"旋转+缩放"的几何运动正交——弦线穿过内部,形状被剪切压缩;
- 极坐标插值的解法:闭式 Procrustes 求出最优相似变换,在角度/对数缩放/质心的自然空间里插值,刚体对保持刚体,残差在对齐系里混合,端点精确;
- 弹簧超调的魔法:欠阻尼谐振子让 t 短暂 > 1,而解析公式在外推区域依然光滑,超调动画免费获得,还天然支持中途打断重规划。
理解了这个公式,你下次写任何形变动画时,都会知道为什么"先分解,再插值,最后合成"永远是比"直接 lerp"更好的一条路。
【免费下载链接】morphiconsAny icon morphs into any other — universal morphing for stroke-based icons with spring physics. Zero dependencies, ~7 KB gzip.项目地址: https://gitcode.com/gh_mirrors/mo/morphicons
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考