前端UI组件【免费下载链接】motionA modern animation library for React and JavaScript项目地址https://gitcode.com/GitHub_Trending/mo/motion点击查看免费下载导读本文以仓库内实施计划 plans/018-reorder-multidimensional.md 为核心骨架完整还原 motion 动画库中 Reorder 组件从单轴方向猜测到二维位置碰撞检测的设计演进——包括 1D/2D 两种几何算法的伪代码、Reorder.Group与Reorder.Item的改动点、逐步实施与验证流程、Cypress 端到端测试方案并结合当前仓库源码packages/framer-motion/src/components/Reorder/与packages/motion-utils/src/array.ts印证落地形态。读完你将理解为什么网格grid/折行 flex场景必须用几何碰撞而非索引算术如何设计一个不依赖 velocity 方向、天然防震荡的排序判定核心以及该功能从计划到验证的全套工程流程。一、背景网格排序是 Reorder 最被期待、也最难做对的功能计划文档开篇即点明动机网格grid重排序是 Reorder 组件被请求最多的功能对应 issue #1400——该 issue 自 2021 年开启、累计 25 条评论。更重要的是这不是一个从零开始的功能此前曾有一版实现被合并进主干PR #1685随后又被移除维护者的复活尝试PR #1862最终以quite buggy and feels off相当多 bug、手感不对为由关闭并留下一条硬性要求——复活版本必须真正手感好、工作正常。计划文档对前一次失败做了三点归因这也是本次设计的直接输入按轴以 velocity 符号门控交换当指针短暂静止或斜向移动时明明悬停在目标槽位上却毫无反应方向猜测在指针非轴向运动时失效。用itemsPerAxis模运算推断网格结构隐含所有条目尺寸一致、行完整无缺的假设遇到折行 flex 布局、末行不齐或混合尺寸时直接崩溃。用索引算术index ± itemsPerAxis而非几何移动条目且每次拖拽事件只能交换一个相邻槽位无法一次跨越多个位置。这三条失败原因被逐一回避排序判定不再依赖速度方向不再假设网格规则不再做索引算术——全部替换为基于几何位置的碰撞检测。二、核心设计转向位置碰撞检测positional collision detection计划文档给出的替代方案是 dnd-kit 等拖拽库采用的位置碰撞检测思路其关键洞察来自仓库已有的数据资产registerItem收到的本来就是完整布局Box而当前实现只取layout[axis]把交叉轴cross axis信息丢弃了。也就是说每个条目的注册Box布局矩形已经在手只是没被利用。新的目标槽位判定因此极其直接被拖拽条目的投影中心落在哪个条目的 Box 内就往哪里排。由此得到三个连锁收益velocity 彻底退出排序决策——方向的语义由几何中心相对 Box 的位置自然涌现不再需要猜测顺带修复 2026-06-11 Reorder 审计中发现的 finding #41D 路径的 velocity 怪癖旧代码在velocity 0时提前返回导致指针静止时悬停目标槽位无反应单次拖拽事件即可实现多位置跳跃multi-position jumps不再局限于相邻槽位。关键行为不变式整个设计的基石计划文档强调一个必须成立的不变式拖拽期间一旦排序提交、被拖条目的槽位发生变化拖拽系统会把条目的 transform 重新归基rebase使注册布局 当前偏移 ≈ 当前视觉位置。当前 1D 算法能工作的证据是1D 交换判定用layout.max offset与邻居中心比较而比较所依赖的 layout 是每次排序渲染后由onLayoutMeasure触发registerItem重新注册的新布局。如果偏移不被归基每次 1D 交换都会因旧偏移 已移动的布局重复触发排序会失控——而实际上它没有失控。2D 设计完全依赖同一不变式计划文档明确要求在实施 Step 6 中实证验证否则触发 STOP 条件。三、API 设计axis扩展为三值默认行为不变Reorder.Group accepts axis?: x | y | both, default y (unchanged)axisboth时条目获得drag双轴可拖排序目标在 2D 空间内几何查找默认axisy保持不变因此 SSR 输出的touch-action: pan-xdragy的产物不受影响——server.ssr.test.tsx 中精确断言了包含touch-action:pan-x的标记字符串计划要求这些测试零改动通过超出拓宽axis类型之外的公共 API 表面不在范围内。落地差异说明计划文档写作时轴类型写为both当前仓库 types.ts 中实际落地为export type ReorderAxis x | y | xy语义等价xy 即双轴。文中算法说明沿用计划的both表述与源码对应时可理解both≡ 当前实现中的xy。四、checkReorder新契约返回移动指令而非新数组计划要求重写 utils/check-reorder.ts契约变化如下export interface ItemDataT { value: T layout: Box // 完整 Box —— 原来是 Axis } export interface ReorderMove { from: number to: number } export function checkReorderT( order: ItemDataT[], value: T, offset: Point, // 2D 偏移 axis: x | y | both ): ReorderMove | null // 返回 from/to 索引而非新数组velocity 不再是参数方向由几何涌现返回值从新数组改为移动指令{from, to}把判定与应用解耦方便Group把 measured 序索引映射回完整values数组Point需从motion-utils导入计划要求核验其导出若未导出则在types.ts内本地定义interface Point { x: number; y: number }。当前源码中Point已由motion-utils导出并直接导入使用见 check-reorder.ts。五、1D 路径算法投影区间扫描去掉 velocity 门控当axis为x或y时order仍按layout[axis].min排序与现状一致。新算法计算被拖条目的投影区间然后寻找中心已被跨越的最远条目const projectedMin item.layout[axis].min offset[axis] const projectedMax item.layout[axis].max offset[axis] let target index // 向前扫描所有中心 projectedMax 的后续条目均已被跨越 for (let j index 1; j order.length; j) { if (centerOf(order[j].layout[axis]) projectedMax) target j else break } if (target index) { // 向后扫描所有中心 projectedMin 的前置条目均已被跨越 for (let j index - 1; j 0; j--) { if (centerOf(order[j].layout[axis]) projectedMin) target j else break } } return target index ? null : { from: index, to: target }其中centerOf(a: Axis) mixNumber(a.min, a.max, 0.5)mixNumber来自motion-dom取区间中点。该设计有两个计划文档明确指出的性质相邻场景下与现有一致判定阈值恰好复刻当前行为——前导边跨越相邻条目中心触发交换但不再需要 velocity 门控多位置跳跃天然支持只要偏移足够大、跨越了两个及以上条目的中心target会落到最远被跨越者不会前后双触发排序后的非重叠条目保证向前/向后扫描不可能同时成立。六、2D 路径算法投影中心包含判定containment当axis both时order保持注册顺序values顺序不再按min排序。计算被拖条目的投影中心找Box 包含该点的条目const projectedCenter { x: centerOf(item.layout.x) offset.x, y: centerOf(item.layout.y) offset.y, } const target order.findIndex( (entry, i) i ! index projectedCenter.x entry.layout.x.min projectedCenter.x entry.layout.x.max projectedCenter.y entry.layout.y.min projectedCenter.y entry.layout.y.max ) return target -1 ? null : { from: index, to: target }三个关键行为投影中心落入缝隙 → 无操作gap 里没有任何 BoxfindIndex返回 -1这是正确的——用户把条目拖进两个槽位之间的空白处排序不该发生混合尺寸天然支持包含判定是逐条目的几何比较与条目尺寸无关——一个 100×100 的条目拖进 200×100 条目的 Box 就触发移动不依赖任何网格算术震荡被结构性防止一次移动提交后条目在排序渲染时通过onLayoutMeasure重新注册新布局 Box注册的是布局位置而非动画中的视觉位置于是被拖条目的投影中心会落在自己的新槽位内而该槽位因i ! index重新排序/注册后被排除指针不越过其他条目 Box 就不会再触发移动——直到指针真正进入另一个条目的 Box。与当前源码的对应仓库 check-reorder.ts 的xy分支采用了行识别 最近距离的混合实现——先用getLines把条目按 y 轴区间聚成行Line若投影中心跨到不同行则moveToLine按 x 中心插入目标行同行内用distanceToBox点到矩形的最短欧氏距离平方选择最近目标。这与计划的纯包含判定在缝隙无操作、混合尺寸支持、防震荡的语义目标上一致但具体判定策略演进出更精细的行级处理。计划文档作为设计蓝图与源码实现存在此层面的差异属于可预期的实施演进。七、Group.tsx改动全 Box 注册、move 语义、虚拟化保护计划文档给出 Group.tsx 的四处关键改动1. 注册完整 Box且both时不排序registerItem: (value, layout) { const idx order.findIndex((entry) value entry.value) if (idx ! -1) { order[idx].layout layout // 原来是 layout[axis] } else { order.push({ value, layout }) } if (axis ! both) order.sort(compareMin) // both 时保持注册顺序 },compareMin变为组件内闭包(a, b) a.layout[axis].min - b.layout[axis].min原先是模块级函数需闭包捕获axis计划提示用组件内局部箭头函数以控制产物体积。both不排序的理由条目按values顺序渲染注册顺序就是 DOM 顺序排序反而会破坏 2D 语义。2.updateOrder应用 move 而非 swapupdateOrder: (item, offset) { if (isReordering.current) return const move checkReorder(order, item, offset, axis) if (!move) return isReordering.current true const fromIndex values.indexOf(order[move.from].value) const toIndex values.indexOf(order[move.to].value) if (fromIndex ! -1 toIndex ! -1) { onReorder(moveItem(values, fromIndex, toIndex)) } },把 measured 序索引映射回完整values数组的索引再调用moveItem未测量条目如虚拟化列表中屏幕外的项因此得以保留。3.moveItem语义辨析moveItem位于 packages/motion-utils/src/array.ts克隆数组、从fromIndex移除并插入toIndex。相邻索引时它等价于一次交换距离较远时则把中间所有条目顺移——这正是网格回流grid reflow的正确语义。因此现有虚拟化单元测试Preserves unmeasured items…断言[1, 3, 2, 4, 5]必须在新签名下继续通过仅需把调用从updateOrder(2, 30, 1)更新为updateOrder(2, { x: 0, y: 30 })。4. 类型与上下文ReorderContextProps中axis: x | y | both、updateOrder: (item: T, offset: Point) void。另外Group还会在容器上设置overflow-anchor: none当前源码 Group.tsx 中可见防止浏览器滚动锚定在条目重排时调整滚动位置、干扰拖拽坐标计算——这是与设计配套的既有实现细节。八、Item.tsx改动双轴拖拽与双轴自动滚动Item.tsx 的计划改动drag{axis both ? true : axis} ... onDrag{(event, gesturePoint) { const { velocity, point: pointerPoint } gesturePoint updateOrder(value, { x: point.x.get(), y: point.y.get() }) if (axis both || axis x) { autoScrollIfNeeded(groupRef.current, pointerPoint.x, x, velocity.x) } if (axis both || axis y) { autoScrollIfNeeded(groupRef.current, pointerPoint.y, y, velocity.y) } onDrag onDrag(event, gesturePoint) }}drag{true}经拖拽特性自动产出touch-action: none无需手动处理样式双轴模式会对 x、y 各发一次autoScrollIfNeeded。该函数位于 utils/auto-scroll.ts以 50px 为阈值窗口、25 为最大滚速按距边缘越近滚得越快的平方强度计算滚动量并通过WeakMap记录每容器初始滚动上限与激活边缘防止无限滚动Group的axis属性 JSDoc 中To make draggable on both axes, setReorder.Item drag /一行需要更新为提及axisboth——计划文档特别指出这行文档正是 #1400 困惑的原始来源。九、实施步骤与逐级验证Step 1–8计划文档为执行者规定了严格的逐步验证流程每步都有机器可查的验证命令步骤内容验证Step 1基线确认Reorder 单元测试全绿yarn buildexit 0Step 2先写新checkReorder的失败测试utils/tests/check-reorder.test.ts因新 API 缺失而编译/运行失败预期Step 3实现types.tscheck-reorder.tsStep 2 全部测试转绿Step 4更新Group.tsx/Item.tsx更新虚拟化测试签名在tests/index.test.tsx 扩展 2D 上下文级测试testPathPatternReorder\|check-reorder全过SSR 测试不变通过Step 5更新 JSDocyarn lint、yarn buildexit 0Step 6创建 dev/react/src/tests/reorder-grid.tsx?testreorder-grid并手动验证关键不变式页面渲染 9 个条目无震荡、不瞬移Step 7创建 cypress/integration/reorder-grid.ts按 drag-to-reorder.ts 的指针事件模式编写React 18 与 19 均通过Step 8全量验证单元、client、SSR、lint、build 全部通过常用命令速查计划文档给出的命令表均在仓库根执行make bootstrap # 安装依赖仅需要时前台执行 yarn build # 构建禁止在包目录内执行 npx jest --config packages/framer-motion/jest.config.json --testPathPatternReorder|check-reorder # 单元测试 cd packages/framer-motion yarn test-client # 完整 client 测试 cd packages/framer-motion yarn test-server # SSR 测试Reorder 必须原样通过 yarn lint # 静态检查Cypress 双版本流程必须前台执行后台会静默挂起计划文档要求排序功能在React 18 与 React 19 两个版本上分别跑通# React 18 PORT$((10000 RANDOM % 50000)) cd dev/react TEST_PORT$PORT yarn vite --port $PORT DEV_PID$! npx wait-on http://localhost:$PORT cd ../../packages/framer-motion npx cypress run --headed --config baseUrlhttp://localhost:$PORT --spec cypress/integration/drag-to-reorder.ts,cypress/integration/reorder-grid.ts kill $DEV_PID # React 19独立服务器、独立端口使用 cypress.react-19.json 配置 PORT$((10000 RANDOM % 50000)) cd ../../dev/react-19 TEST_PORT$PORT yarn vite --port $PORT DEV_PID$! npx wait-on http://localhost:$PORT cd ../../packages/framer-motion npx cypress run --config-filecypress.react-19.json --config baseUrlhttp://localhost:$PORT --headed --spec cypress/integration/drag-to-reorder.ts,cypress/integration/reorder-grid.ts kill $DEV_PID注意保留drag-to-reorder.ts作为1D 回归闸门——它验证新 1D 扫描逻辑没有破坏既有单轴手感。手工验证场景Step 6?testreorder-grid页面Reorder.Group asdiv axisbothdisplay: flex; flex-wrap: wrap; width: 340px九个100×100px、margin: 5px、iditem-N的条目useState([0..8])默认布局动画。手工检查三个现象条目中心进入邻居槽位才触发排序、静止时无震荡不快速来回交换、不瞬移。若无法交互式运行浏览器需在报告中说明并依赖 Step 7 的 Cypress 中途拖拽断言。Cypress 三个测试点对角重排pointerdown在#item-0约 5 步pointermove每步wait(50)到 item 4 槽位中心按 110px 单元格间距计算坐标wait(100)后在拖拽中途断言 DOM 源码序中 item 0 已占据索引 4 的位置pointerup后再次断言稳定序缝隙拖放是 no-oppointerdown在#item-8把中心移到两槽位之间的 margin 缝隙偏移约 55px断言顺序不变无震荡测试 1 移动后指针静止 500ms用.then()在两个相隔 300ms 的时间戳捕获顺序并断言不变——不能用.should()因为它会重试直到通过从而掩盖震荡。当前仓库中 reorder-grid.tsx 与 reorder-grid.ts 均已落地页面改为display: grid双列布局 data-testid断言顺序测试通过?testreorder-grid访问并断言中途current-order变为b,c,d,a说明该计划的功能已进入实现与端到端验证阶段。十、范围边界与 STOP 条件In scope仅允许改动的文件Reorder/types.ts、Reorder/utils/check-reorder.ts、Reorder/Group.tsx、Reorder/Item.tsx新建utils/__tests__/check-reorder.test.ts、扩展__tests__/index.test.tsx、新建dev/react/src/tests/reorder-grid.tsx与cypress/integration/reorder-grid.ts。Out of scope看着相关但严禁触碰拖拽手势系统src/gestures/drag/、投影系统src/projection/——若设计需要改到它们那是 STOP 条件而非邀请utils/auto-scroll.ts内部只调用不修改自动轴检测从布局换行推断both被明确推迟SSR 标记期望。STOP 条件出现即停止并上报不得自行发挥关键不变式失败Step 6/7 中条目震荡或 2D 移动后瞬移——修复大概率在拖拽/投影系统超出范围现有drag-to-reorder.ts在任一 React 版本失败且根因在新 1D 扫描逻辑——不得调阈值硬凑1D 手感契约是前导边跨越邻居中心偏离需维护者批准SSR 标记测试需要改动意味着默认drag/touch-action变了默认轴必须保持yPoint未从motion-utils导出且本地定义与包内既有导入冲突虚拟化测试在 move 语义下无法在不削弱断言的前提下通过实现被迫触碰src/gestures/或src/projection/。十一、完成标准全部可机器检查计划文档给出六条可勾选的完成标准Reorder/check-reorder 单元测试 exit 0 且check-reorder.test.ts存在 ≥10 个用例grep -n velocity check-reorder.ts无匹配velocity 彻底退出判定核心grep -n both Group.tsx有匹配Cypressreorder-grid.ts与drag-to-reorder.ts在 React 18/19 双通过SSR 测试零改动lint/build exit 0 且git status干净范围外无改动plans/README.md状态行更新。十二、维护笔记后续方向与已知关注点计划文档在末尾留下一组明确的后续方向值得读者关注自动轴检测deferred维护者 PR #1862 的关闭评论中包含从布局自动检测轴的期望但本计划刻意将其排除——因为它会改变默认行为进而影响 SSRtouch-action输出并放大手感风险。一旦axisboth发布且手感良好自动检测可作为小跟进首次测量后若注册 Box 横跨 1 个行带且 1 个列带则按both行为处理。当前仓库其实已先行实现detectAxisutils/detect-axis.ts两两比较布局在 x/y 上的分离性x与y均分离即返回xy仅 x 分离返回x否则y且Reorder.Group在未显式传axis时用useState动态检测——这已部分消化了该 deferred 项发布前必须做手感评审feel review前一代实现死于手感而非正确性。评审清单槽位边界无震荡containment 重注册设计应能阻止边界情况是中心恰好落在 Box 边缘、快速对角甩动行为、整体拖出 Group 的行为与自动滚动的交互both现在双轴自动滚动两次autoScrollIfNeeded调用。封顶于初始滚动上限的逻辑按滚动容器独立、未变但xy 同时自动滚动从未被实际验证过——QA 若发现异常优先排查此处#2603 请求的更丰富onReorder签名(newOrder, {value, from, to})在updateOrder计算fromIndex/toIndex后几乎免费可得但被刻意排除API 增加需维护者批准计划要求记入 PR 描述issue #1400 的关联方式若维护者认可显式axisboth无需自动检测即满足需求则 PR 中Fixes #1400否则Refs #1400。十三、工程流程参考实施遵循 monorepo 规范分支improve/018-reorder-multidimensional基于main在 015 合并后或按操作者指示 rebase每个步骤单独提交提交信息用简短祈使句仓库示例Add auto-scroll support to Reorder.Group、Fix Reorder.Group axis change during window resize除非操作者指示不 push、不开 PR。开工前需先执行漂移检查git diff --stat 42bfbe3ed..HEAD -- packages/framer-motion/src/components/Reorder/ packages/framer-motion/src/context/ReorderContext.ts——计划 015条件 hook 修复与 016仅 JSDoc对同文件的改动是预期漂移其余任何改动都按 STOP 条件处理。总结这份计划的价值在于把网格重排这一拖拽排序里手感最脆弱的场景收敛为一个可证明的几何判定核心——1D 用投影区间扫描、2D 用投影中心包含方向由几何涌现、velocity 出局、震荡被结构性防止再以单元测试 → 手工不变式验证 → 双版本 Cypress 回归三层测试体系兜底。对照当前仓库源码可见计划的both/xy双轴能力、自动轴检测、reorder-grid演示页与端到端测试均已落地几何碰撞方案已成为 Reorder 组件可运行的现实能力。赞分享前端UI组件【免费下载链接】motionA modern animation library for React and JavaScript项目地址https://gitcode.com/GitHub_Trending/mo/motion点击查看免费下载相关推荐Vue-Draggable-Plus 拖拽方向检测实现方案Vue Draggable Plus 拖拽方向检测实现方案 背景介绍 在使用Vue Draggable Plus这个Vue拖拽库时开发者有时需要获取元素被拖拽前端UI组件告别混乱排版LogicFlow节点拖拽的智能对齐与碰撞检测实现告别混乱排版LogicFlow节点拖拽的智能对齐与碰撞检测实现 在流程图编辑场景中节点拖拽的精准度直接影响用户体验。当拖拽节点时出现位置偏差、对齐困难或元素前端低代码流程编排Area51碰撞组优先级编辑器拖拽排序界面Area51碰撞组优先级编辑器拖拽排序界面 功能概述 碰撞组优先级编辑器是Area51游戏引擎中的核心工具用于管理物理碰撞检测的执行顺序。通过拖拽排序界面上一篇在 Blender 中导入导出 VRM从骨架到成片的完整流程下一篇录一次跑一整天KeymouseGo 鼠标键盘自动化实战创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考