☰
为什么直接lerp坐标会让图标变形塌陷?morphicons极坐标插值原理与弹簧超调外推数学解析
2026/10/1 21:30:03 网站建设 项目流程

为什么直接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阻尼比 ζ手感
smooth170 / 261.00(临界阻尼)不超调,平滑落定
snappy420 / 300.73快,轻微超调
bouncy300 / 140.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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询