不用React也能用:morphicons纯JavaScript图标形变驱动完全手册,手势拖拽逐帧scrubbing详解
【免费下载链接】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 是一个零依赖、gzip 约 7 KB 的通用图标形变库:任意描边图标都能形变为任意其他图标,旋转由数学自动推导,而不是人工声明。很多人以为它只服务 React/Vue/Svelte,其实它的"根"是一个纯 JavaScript 的图标形变驱动createMorph(入口morphicons/dom,源码见 src/dom/index.ts)——不依赖任何框架,直接驱动一个<path>元素,特别适合做手势拖拽和逐帧 scrubbing。本文就是这个纯 JS 驱动的完全手册。
🚀 快速安装与导入
morphicons 只发布 ESM,任何现代构建工具(Vite、esbuild、Rollup)都能直接消费:
bun add morphicons # 或 npm install / pnpm add图标数据来自数据包(如 vanilla 版lucide),而不是组件包——驱动消费的是IconNode结构或原始d字符串:
import { createMorph } from "morphicons/dom"; import { Menu, X } from "lucide"; // 数据,不是组件 const pathEl = document.querySelector("path"); const m = createMorph(pathEl, Menu); m.morphTo(X, "snappy"); // 带弹簧物理地飞向 X m.set(Menu); // 不带动画,直接跳到 Menu就这么简单:状态在你手里,动画只是驱动的一个实现细节。
📦 三种驱动 API 一览
createMorph返回的Morph句柄只有 6 个成员(定义见 src/dom/index.ts),记住下面这张表就够用:
| API | 作用 | 是否走弹簧 | 典型场景 |
|---|---|---|---|
morphTo(icon, spring?) | 形变到目标图标 | ✅ 可中断 | 点击切换菜单/关闭 |
set(icon) | 瞬间跳到目标 | ❌ | 初始化、复位 |
seek(icon, t) | 冻结在 t 帧(t ∈ [0,1]) | ❌ | scrubbing、手势 |
m.progress = t | 对当前目标 seek 的语法糖 | ❌ | 拖拽进度条 |
m.reducedMotion | 动效降级策略 | — | 无障碍适配 |
m.destroy() | 注销实例 | — | 卸载时清理 |
1. 弹簧预设怎么选
spring参数支持三个内置预设(数值定义在 src/core/spring.ts):
| 预设 | 刚度 k | 阻尼 c | 手感 |
|---|---|---|---|
smooth | 170 | 26 | 临界阻尼,零过冲,稳 |
snappy | 420 | 30 | 快,带一点点过冲,默认推荐 |
bouncy | 300 | 14 | 过冲明显,俏皮 |
也可以传{ stiffness, damping }自定义。弹簧是一个阻尼谐振子(源码 src/core/spring.ts),用半隐式欧拉以 1/240 秒子步积分,稳定且只需约 25 行代码,不引入任何动画库。
2. 可中断性:连点狂点都不会跳帧
morphicons 的morphTo是真正可中断的:飞行中再次调用时,它从"当前中间形状"重新规划路径,同时保留弹簧速度(见 src/dom/index.ts 的snapshot/retarget逻辑)。视觉运动连续不断,连点只会让图标"更活",不会瞬移。
🎯 逐帧 scrubbing:seek 与 progress 详解
scrubbing(逐帧擦洗)是手势交互的核心:不用弹簧,图标完全跟随你的手指。
m.seek(X, 0.4); // from=当前形状, to=X, 冻结在 40% 帧 m.progress = 0.4; // 等价于 seek(当前目标, 0.4)seek的底层原理(src/dom/index.ts):
- 停表:先注销弹簧 ticker,并把速度清零——受控模式不继承任何速度;
- 复用计划:如果目标图标没变(
icon === target且已有飞行计划),直接复用缓存的 plan,零成本重渲染; - 定点渲染:调用核心的极坐标插值
interpPolar把 t 直接算到第 t 帧,写一次d属性就收工,不启动任何循环。
这意味着 scrubbing 的开销是"一次插值 + 一次字符串拼接",非常适合 60fps 连续调用。
✋ 手势拖拽完整示例:手指滑多远,图标变多少
下面是一个完整的指针拖拽示例:图标在"菜单 ↔ 关闭"之间形变,进度完全由拖拽位置决定,松手后按过半阈值自动弹回或定型:
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"> <path id="i"></path> </svg>import { createMorph } from "morphicons/dom"; import { Menu, X } from "lucide"; const el = document.querySelector("#i"); const m = createMorph(el, Menu); let dragging = false; let startY = 0; let startT = 0; // 拖拽开始时的进度 el.setPointerCapture?.(0); // 容器上监听指针事件同理 window.addEventListener("pointerdown", (e) => { dragging = true; startY = e.clientY; startT = currentT; }); window.addEventListener("pointermove", (e) => { if (!dragging) return; // 每移动 120px 推进一个完整进度,clamp 到 [0, 1] const t = Math.min(1, Math.max(0, startT + (e.clientY - startY) / 120)); m.progress = t; // 逐帧 scrubbing:冻结渲染,无弹簧 currentT = t; }); window.addEventListener("pointerup", () => { dragging = false; // 松手:过半定型为 X,否则弹回 Menu(走弹簧,带惯性感) if (currentT > 0.5) m.morphTo(X, "snappy"); else m.morphTo(Menu, "snappy"); });要点:
- 拖拽中用
progress/seek(冻结帧,零延迟跟随); - 松手后用
morphTo(弹簧收尾,从"当前中间形状"起飞,所以没有任何跳变); seek结束后调morphTo是被官方支持的衔接方式——中间形状就是合法的形变源。
💡 同样的"冻结对 + 进度"模型,就是 React/Vue/Svelte 绑定里受控模式(
from/to/progress三个 props)的底层:五种绑定共享同一套生命周期契约,实现见 src/dom/controller.ts。
🖼️ 没有 SVG 节点?Canvas 与 CSS mask 适配器
morphicons 对外只有两个结构契约:入口的IconInput,出口的PathEl(任何有setAttribute的对象)。基于这个契约,morphicons/adapters提供三个格式适配器,纯 JS 环境同样适用:
svgToIcon:把 SVG 标记(Iconify body、网页里复制的<svg>、shadcn 注册表的<path d>)转成任意入口都接受的IconInput。在模块作用域解析一次即可,plan 缓存就始终有效(源码 src/adapters/svg.ts)。maskTarget:你的图标是 UnoCSS/Iconify 的 CSS mask(DOM 里没有<path>)时,它创建一对隐藏 mask 缓冲并把手柄交给createMorph,元素原地形变(src/adapters/mask.ts)。canvasTarget:图标直接变成像素,每帧一次stroke(new Path2D(d)),可跑在OffscreenCanvas+ Worker 里,甚至导出为 PNG、录制成视频(src/adapters/canvas.ts)。
三个适配器"就是"createMorph——只是写入目标不同,弹簧、中断、reduced motion 行为完全一致。
⚡ 性能:为什么可以疯狂调用
来自 README.md 的实测数据,解释了为什么 scrubbing 可以无节制使用:
- 计划构建亚毫秒:
plan(A, B)(重采样 + 匹配 + 2D Procrustes 对齐)实测 0.01–0.06 ms; - 每帧零数值分配:插值器直接写入 plan 预分配的
Float64Array,每帧唯一的新分配是d字符串; - 按引用缓存:
normalize/plan用WeakMap缓存,Lucide 的稳定 import 引用天然命中(src/dom/index.ts); - 一个全局 rAF:所有实例共享单例调度器,一百个图标同时形变也只有 1 个动画循环,全部静止时 rAF 自动停止(src/dom/index.ts)。
♿ 无障碍与细节:reducedMotion 策略
图标形变属于"小、短、有沟通意图"的微动效,所以默认("never")总是播放;如果你的 App 必须严格跟随系统"减少动态效果"设置,显式开启:
const m = createMorph(el, Menu, { reducedMotion: "user" }); m.reducedMotion = "user"; // 也可运行时切换,对下一次 morphTo 生效"never"(默认):忽略 OS 设置,总是动画;"user":系统开启减少动效时,morphTo退化为瞬间切换;"always":永远直跳,适合截图与自动化测试。
🧩 不用 ES 模块 / 要 SSR?还有<morph-icon>Web Component
morphicons/element提供<morph-icon>自定义元素(源码 src/element/index.ts),它是第五种绑定而非第二套实现,暴露同样的三种模式,适合纯 HTML、HTMX、Rails 等服务端渲染栈:
<script type="module"> import { defineMorphIcon } from "morphicons/element"; defineMorphIcon(); // 幂等;也可自定义标签名 </script> <!-- 属性接收 d 字符串,属性(properties)接收完整 IconInput --> <morph-icon icon="M4 6h16M4 12h16M4 18h16" label="Menu"></morph-icon>在脚本里对元素直接调用el.morphTo(X)(命令式)、el.icon = X(非受控)、el.progress = 0.5(受控 scrubbing)即可。Astro 项目则直接用它的 SSR 外壳 src/astro/MorphIcon.astro:服务端输出精确的静态 SVG(零闪烁、零布局偏移),水合只是自定义元素升级。
✅ 上手清单(TL;DR)
- 装库:
bun add morphicons,图标用数据包(vanillalucide等); - 建驱动:
createMorph(pathEl, 初始图标),卸载时m.destroy(); - 切换:
m.morphTo(X, "snappy")——可中断、保留速度; - 跳转:
m.set(icon)——初始化与复位; - scrubbing:
m.progress = t或m.seek(to, t)——手势/滚动逐帧跟随,松手再morphTo收尾; - 无障碍:需要跟随系统设置时传
reducedMotion: "user"。
想亲手把玩?仓库自带 playground(38 个真实图标、逐对显示检测到的相似变换、t 滑杆),在仓库根目录执行bun run play即可启动,代码在 playground/main.ts。更多架构细节可阅读 README.md 与 docs/adr/ 下的决策记录。
【免费下载链接】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),仅供参考