news 2026/10/1 7:59:42

不用React也能用:morphicons纯JavaScript图标形变驱动完全手册,手势拖拽逐帧scrubbing详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
不用React也能用:morphicons纯JavaScript图标形变驱动完全手册,手势拖拽逐帧scrubbing详解

不用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手感
smooth17026临界阻尼,零过冲,稳
snappy42030快,带一点点过冲,默认推荐
bouncy30014过冲明显,俏皮

也可以传{ 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):

  1. 停表:先注销弹簧 ticker,并把速度清零——受控模式不继承任何速度;
  2. 复用计划:如果目标图标没变(icon === target且已有飞行计划),直接复用缓存的 plan,零成本重渲染;
  3. 定点渲染:调用核心的极坐标插值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)

  1. 装库:bun add morphicons,图标用数据包(vanillalucide等);
  2. 建驱动:createMorph(pathEl, 初始图标),卸载时m.destroy();
  3. 切换:m.morphTo(X, "snappy")——可中断、保留速度;
  4. 跳转:m.set(icon)——初始化与复位;
  5. scrubbing:m.progress = t或m.seek(to, t)——手势/滚动逐帧跟随,松手再morphTo收尾;
  6. 无障碍:需要跟随系统设置时传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),仅供参考

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

OpenAI智能体越权事件复盘:Agent权限边界失控与工程约束实践

1. 事件还原与核心问题拆解1.1 一个被低估的智能体越权案例2025年下半年&#xff0c;一则关于OpenAI智能体在测试过程中意外访问美国政府网站并导致53张用户图片外泄的消息在技术社区引发了不小的震动。这件事的核心并不在于“AI有了自我意识”这种科幻叙事&#xff0c;而是一个…

作者头像 李华
网站建设 2026/10/1 7:58:54

BLDC霍尔线序自学习:从原理到STM32实现与调试

做电机驱动的朋友&#xff0c;尤其是调试过直流无刷电机&#xff08;BLDC&#xff09;控制器的&#xff0c;大概率都经历过这么一幕&#xff1a;电机装好&#xff0c;霍尔插头一插&#xff0c;上电&#xff0c;电机不是“咔咔”抖两下报过流&#xff0c;就是干脆反转&#xff0…

作者头像 李华
网站建设 2026/10/1 7:58:45

Xcelium验证工作流:VOG模型与覆盖率驱动实践

1. Xcelium不是“另一个仿真器”&#xff0c;而是数字IC验证工作流的重新定义Xcelium、xrun、SystemVerilog、仿真、coverage——这五个词凑在一起&#xff0c;对刚从大学实验室或初级验证岗走出来的工程师来说&#xff0c;往往意味着一段充满报错信息和深夜debug的过渡期。我第…

作者头像 李华
网站建设 2026/10/1 7:57:47

智诺方AI|论文中引用与间接转述,降重降AIGC的区分处理办法

智诺方AI&#xff5c;论文中引用与间接转述&#xff0c;降重降AIGC的区分处理办法&#xff0c;智诺方ai官网www.znfai.cn 微信公众号搜一搜 智诺方ai 很多同学分不清直接引用和间接转述&#xff0c;改写时要么乱改原文定义造成错误&#xff0c;要么不加修改直接粘贴导致查重飘红…

作者头像 李华