news 2026/10/1 16:01:44

morphicons核心算法揭秘:2D Procrustes闭式解让图标旋转角度自动涌现,告别手动声明旋转组

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
morphicons核心算法揭秘:2D Procrustes闭式解让图标旋转角度自动涌现,告别手动声明旋转组

morphicons核心算法揭秘:2D Procrustes闭式解让图标旋转角度自动涌现,告别手动声明旋转组

【免费下载链接】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

morphicons是一个图标变形(icon morphing)开源库:任意一个描边图标都能平滑地变形为另一个图标,带弹簧物理质感,零运行时依赖,核心仅 ~7 KB(gzip)。它的最大亮点是——旋转角度不需要你手动声明。箭头从右指变成下指,库会自动算出"这是一次 90° 旋转",然后让它优雅地转过去,而不是中间塌缩、剪剪切地"蠕动"过去。

这篇文章带你走一遍 src/core/ 里的核心算法,看看数学是怎么"涌现"出动画的。

痛点:为什么"旋转组"声明让人头疼 🤯

做图标变形的常见做法是直接对坐标插值。但两个形状如果只是旋转关系,逐点直线插值会让形状在飞行途中缩小、剪切——一个 90° 旋转的箭头,中间帧会变成一条"被压扁的斜杠"。

老派方案的补丁是给每对图标手工声明"旋转组":告诉引擎"A 的哪部分绕哪个中心转多少度"。问题来了:

  • 每个图标对都要单独配置,图标一多就是维护噩梦
  • 配置错了动画就"不对劲",但很难一眼看出来

morphicons 的思路反过来:不做配置,做计算。它把"两个形状之间的最优相似变换"当成一个有闭式解的数学问题来解。

全景:一条纯函数流水线

在动手解数学之前,morphicons 先把图标"驯化"成统一的数字表示,流程定义在 README.md 的 Architecture 一节:

icon A ─┐ ├→ normalize → resample → match → align → PLAN → interpolate(t) → d icon B ─┘ ↑ 可缓存、可序列化

两个关键前置步骤:

1. 归一化(src/core/normalize.ts):所有 SVG 图元(直线、圆弧、圆、rect、polyline…)统一降级为三次贝塞尔段。

2. 弧长重采样(src/core/resample.ts):每条子路径按弧长均匀取 N = 64 个点。这一步直接消灭了"两条曲线阶数不同、点怎么对应"的结构性难题——对应关系就是按下标对。更妙的是拐角锚定:切线突变超过 22.5° 的点被固定为精确采样点,所以静止时形状是 100% 精确的,而飞行中源形状的尖角会自然变圆、目标形状的尖角会逐渐变锐,观感非常自然。

主角登场:2D Procrustes 闭式解

到了align阶段,对每一对对应的点云,morphicons 要回答:

找到一个旋转 θ、缩放 σ、平移的组合,使形状 A 尽量贴合形状 B。

这正是 2D Procrustes 问题。一般教科书会动用 SVD,但 2D 情形下它有闭式解——只需要一个atan2,实现在 src/core/plan.ts 的procrustes函数里:

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ᵢ‖²

一次遍历累加四个标量,一个三角函数——这就是"旋转角度自动涌现"的全部秘密:θ* 不是配置出来的,是解出来的。

残差:判断"这到底是不是旋转"的标尺

对齐之后还能剩多少误差?morphicons 计算归一化 RMS 残差res = √(Σ|σRa−b|² / Σ‖b‖²),它就是形状距离:

  • res ≈ 0:A 和 B 就是同一个形状,只是转了/缩了——纯旋转
  • res较大:形状确实不同——老老实实做坐标变形

这样"旋转组"从一个需要人工声明的概念,变成了一个数学的涌现性质:arrow-right → arrow-down 的残差趋近 0、θ = 90°,系统自己就选择了纯旋转。

两个工程上的精修细节:

  • 最小旋转平局裁决(src/core/plan.ts):像直线这种"反转后重合"的对称形状,两个遍历方向的残差都是 0,但推出的旋转角却不同。打分用score = res + 0.05·|θ|/π——先看变形、再取短路径,避免图标莫名其妙多转 180°。
  • 全局混合(src/core/plan.ts):先逐子路径 Procrustes,再对整幅图标做一次全局 Procrustes。若全局残差 < 0.005,说明整个图标是刚体关系,所有子路径共享同一个 (θ, σ)一起转——比如箭头的杆和箭头同向旋转,而不会各转各的。

极坐标插值:让旋转真的"转"过去

算出 (θ, σ) 还不够,得在对的空间里插值。如果直接对坐标 lerp,旋转问题照样塌缩。src/core/interpolate.ts 中的interpPolar把运动拆成"相似变换 + 残差"两部分:

P(t) = c(t) + σ*ᵗ · R(t·θ*) · [(1−t)·aᶜ + t·b̃]
  • 角度线性插值(θ* 已保证走短路径)
  • 缩放对数线性插值(σᵗ = exp(t·lnσ),正实数上的测地线)
  • 方括号里是"对齐系"内的残差混合

三个漂亮的性质:

  1. 若 B 就是 A 转过去的(res = 0),方括号恒为常数 →纯旋转,形状全程刚性
  2. 若无旋转,退化为干净的坐标变形
  3. 弹簧超调时 t > 1,公式天然外推:旋转和缩放会轻微过冲再回弹,免费的多汁感 🍹

还有一个容易忽略的修正——块体运输(src/core/plan.ts):全局刚体旋转时,若只对各子路径的质心做 lerp,偏心部件会走弦(弧线内侧),箭头的箭头会在中途向箭杆塌陷约 1px。让质心绕全局质心搭乘同一个相似变换,整个图标在飞行中保持刚性,且 θ = 0 时公式连续退化为普通 lerp,没有任何特判。

弹簧物理与"永不跳跃"的中断

进度 0→1 由一个阻尼谐振子驱动(src/core/spring.ts):

预设kc阻尼比 ζ手感
smooth170261.00临界阻尼,不超调
snappy420300.73干脆,轻微过冲
bouncy300140.40俏皮

半隐式欧拉、1/240 秒子步,约 25 行代码,不需要任何动画库。

中断处理是这套设计最舒服的地方:飞行中再次morphTo,引擎用当前中间形状作为新起点重建 plan(渲染缓冲已经是每子路径 64 个点,天然可作为变形源),并保留弹簧速度——狂点按钮,动画永远连续、永远不跳变。

性能与体积:小得离谱

  • plan(A, B)全流程(重采样 + 匹配 + Procrustes):N = 64 时亚毫秒,实测 0.01–0.06 ms
  • 每帧零数值内存分配:插值器直接写入 plan 预分配的Float64Array,只有d字符串一帧一次分配
  • 核心入口 gzip6.60 KB,CI 里用 size-limit 卡着 7 KB 的红线

上手体验

安装一行命令,React 用法就是换个 prop:

import { MorphIcon } from "morphicons/react"; <MorphIcon icon={open ? X : Menu} spring="snappy" />

想看每对图标的 θ、σ、residual 读数?仓库自带 playground:

bun run play # 38 个真实图标,逐对展示检测到的相似变换

想深入算法细节,推荐按这个顺序读:src/core/plan.ts(对应关系与 Procrustes 对齐)→ src/core/interpolate.ts(极坐标插值与块体运输)→ src/core/spring.ts(弹簧);数学原理的完整叙述见 README.md 的 "How the math works" 一节,设计决策见 docs/adr/。

一句话总结

别再手写旋转组了。把两个图标交给一个atan2,剩下的——角度、缩放、残差、刚体运输、弹簧过冲——全部由数学涌现。这大概就是"用计算替代配置"这句话最好的注脚。

【免费下载链接】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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 16:00:45

python list 地常用操作

1. 引言摘要&#xff1a;本文以处理包含多个 0 的列表为例&#xff0c;系统讲解 Python 列表的五个核心操作——count() 统计元素次数、index() 查找元素位置、remove() 按值删除、pop() 按位置删除&#xff0c;以及切片赋值移动元素。通过代码示例与对比表格&#xff0c;帮助读…

作者头像 李华
网站建设 2026/10/1 16:00:43

品牌训练GEO优化服务商怎么选?聚合增长信息科技提供全链路信任保障

品牌训练GEO优化服务商怎么选?全链路信任保障是关键当采购决策的第一站从搜索引擎搬到AI对话框&#xff0c;企业能否被AI主动推荐&#xff0c;直接决定了能否进入客户候选名单。如今&#xff0c;装修方找辅材、采购负责人选装备、政企找供应商&#xff0c;越来越习惯先向豆包、…

作者头像 李华
网站建设 2026/10/1 16:00:35

没有大型CAD软件时,如何查看、测量和处理DWG/DXF图纸?

收到DWG或DXF图纸后&#xff0c;很多时候并不需要进行复杂建模或完整制图。用户可能只是想打开文件、确认图纸内容、测量尺寸、添加批注&#xff0c;或者将图纸转换成PDF后发送和打印。如果主要处理这类日常任务&#xff0c;可以选择支持常用CAD格式&#xff0c;并具备查看、测…

作者头像 李华
网站建设 2026/10/1 16:00:18

Windows C语言网络编程:winsock2.h包含顺序与链接避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/1 16:00:15

《HelloGitHub》第 20 期精选:23 个跨语言入门级开源项目实战解读

技术博客文档知识库 【免费下载链接】HelloGitHub :octocat: 分享 GitHub 上有趣、入门级的开源项目。Share interesting, entry-level open source projects on GitHub. 项目地址&#xff1a; https://gitcode.com/GitHub_Trending/he/HelloGitHub 点击查看 免费下载 兴趣是最…

作者头像 李华
网站建设 2026/10/1 16:00:13

目标平台与技术栈:C语言和Lua在游戏引擎中的协同落地

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华