mcpField
M
motion

motion

github.com/raphaelsalaja设计与 UI67235 次安装5 天前更新
代码仓库

关于 motion

motion 是一项规范界面动画、手势和运动模糊行为的技能,面向实现 UI 动效的开发者。它提供具体的时长、缓动、减少动态效果及实现规则。

主要功能

  1. 01规定时长与缓动曲线
  2. 02指导 UI 状态转换
  3. 03定义手势与关闭行为
  4. 04使用 @web-kits/motion-blur 实现运动模糊
  5. 05遵循减少动态效果设置

适用场景

  1. 01实现页面和面板切换
  2. 02构建可拖动或滑动关闭的控件
  3. 03为运动元素添加运动模糊
  4. 04检查动画对减少动态效果设置的响应

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/raphaelsalaja/skills --skill motion

安装后在 Claude Code 中输入/motion确认已加载。

附带文件agents/openai.yamlrecipes.md

详细介绍

动效

此技能涵盖所有屏幕上的动效,包括哪些内容会动、曲线和时长、手势、退出、运动模糊和减少动态效果。

时长和曲线均为精确值

只要项目中存在动效令牌,就使用它们。若没有,则此处的时长、曲线和缩放值均为精确值。用户触发且持续时间超过 300ms 的动效,在任何样式下都属于问题。

运动模糊使用 @web-kits/motion-blur 绘制,其默认值均为精确值:180° 快门,Echo 每 1.5px 生成一份副本,最多 16 份,smear 上限为 1.05。

是否应该存在某个动画,属于 restraint 的范畴。拆解现有动画属于 deconstruct。动画是否能保持帧率,属于 frames。表面、加载、悬停样式和声音属于 defaults。

应用界面元素淡入淡出

页面切换、面板和加载状态使用 opacity 淡入淡出,不滑动或缩放。这属于 motion-opacity-for-chrome。只对发生变化的轴进行动画处理。只改变高度时却对宽度进行动画处理,属于 motion-changing-axis-only。

进入动画不会重播

切换页面时不会重播进入动画。在路由级的 AnimatePresence 以及任何包裹状态切换的 AnimatePresence 上设置 initial={false}。这属于 motion-no-replay-on-entry。缓存视图会立即渲染,不会闪现淡入效果或骨架屏。这属于 motion-no-flash-on-return。参见 recipes.md。

明确指定要动画的属性

过渡效果会列出要进行动画处理的属性,通常是 opacity 和 transform。transition: all 属于 motion-name-the-properties。

切换状态使用过渡效果,关键帧只运行一次

交互状态变化使用过渡效果或弹簧,用户反向操作时,动画会重新设定目标。切换类上使用关键帧动画,属于 motion-transitions-for-state。关键帧应保留给只运行一次的序列。

动效从其来源开始

下拉菜单或弹出框从触发器处缩放,并将 transform-origin 设为对应边缘。从自身中心展开的菜单属于 motion-origin-from-trigger。模态框保持居中,因为没有触发器锚定它们。

不从零开始放大

进入动画从 scale(0.95) 或更高的值开始,同时设置 opacity: 0。scale(0) 属于 motion-no-scale-from-zero。

手势使用弹簧

拖动、滑动以及用户可以在动画进行中反向操作的任何动效,都使用弹簧。松开时,将指针速度传入弹簧。固定使用 ease-out 的拖动松手动画属于 motion-spring-for-gestures。参见 recipes.md。

快速轻扫触发关闭,边缘提供阻力

达到指定距离或速度时,滑动会关闭,因此速度超过约 0.11 px/ms 时,轻轻一甩也足以触发关闭。忽略快速轻扫的面板属于 motion-flick-dismisses。

拖动超出边界时,应感受到逐渐增加的摩擦力,而不是撞上一堵墙。在顶部戛然而止的抽屉属于 motion-friction-at-edges。拖动开始后捕获指针,并忽略额外触摸。参见 recipes.md。

每种手势都有按钮操作

每种拖动、滑动或捏合操作,也都能通过点击和键盘完成。只能通过拖动重新排序的列表属于 motion-gesture-alternative。

缓动曲线应匹配变化类型

Toast 等系统变化使用缓动,而非弹簧。进入和退出使用缓出,视图切换使用缓入和缓出。线性运动仅用于进度条和时钟。使用 ease-in 进入的模态框属于 motion-ease-by-direction。

内置的 ease-out 关键字过于缓和。进入和退出时使用 cubic-bezier(0.23, 1, 0.32, 1),屏幕上的移动则使用 cubic-bezier(0.77, 0, 0.175, 1)。单独使用 ease-out 属于 motion-strong-ease-out。

关闭比打开更快

关闭动画的时长应短于打开动画。关闭和悬停结束都不应有延迟。关闭时长与打开时长相同的模态框属于 motion-close-faster。

动效持续时间不超过 300ms

悬停时的颜色变化立即完成,不添加过渡效果。按钮悬停时用 200ms 淡变背景色,属于 motion-instant-hover-color。

按下和悬停时的位移持续 120–180ms。小型状态变化持续 180–260ms。用户触发的动效不超过 300ms,完整面板或抽屉除外,最长可达 400ms。持续 500ms 的下拉菜单属于 motion-under-300ms。

在 mode="wait" 下,退出和进入动效会依次播放,因此各自的时长应减半。当动效感觉迟缓时,先缩短时长,再调整曲线。示例见 recipes.md。

高频操作不添加动效

输入、键盘快捷键、键盘导航和快速切换都不添加动效。通过键盘打开的命令面板会立即出现。上下文菜单会立即出现,也可以淡出。每次按键都触发缩放脉冲,属于 motion-no-motion-on-repeat。

提示工具只等待一次

一组提示工具中的第一个会延迟打开。在它显示期间,相邻提示工具会立即打开,不添加动效;所有提示工具都会立即关闭。工具栏中每个提示工具都等待完整延迟时间,属于 motion-tooltip-delay。

图标切换时交叉淡变

图标在原位切换时(例如从复制图标切换为已复制图标),使用交叉淡变。缩放值从 0.25 变为 1,不透明度从 0 变为 1,模糊度从 4px 变为 0。直接切换属于 motion-icon-crossfade。参见 recipes.md。

icons 中的两个图标形变形式之间,则通过沿位移动效曲线将各路径的 d 作为小型状态变化来动画处理。减少动态效果时,它们会交叉淡变。两个形变形式图标之间使用交叉淡变,属于 motion-icon-morph。参见 recipes.md。

用模糊衔接交叉淡变

如果两个状态之间的交叉淡变仍像是两个物体,播放期间添加 filter: blur(2px)。模糊度应低于 20px,因为 Safari 中超过此值的开销很大。切换过程中两个标签明显重叠,属于 motion-blur-the-crossfade。

仅在视线无法跟上的地方添加动态模糊

屏幕已经会模糊眼睛追踪的内容,因此额外的模糊会在用户阅读时把文字变成灰色条纹。可对抛出的卡片、消失的工作表、弹入的面板或加载旋转图标添加模糊。对滚动内容、轮播页面、滑入到位的列表或正在阅读的文字添加模糊,属于 motion-blur-untracked-only。

在 React 中使用 useMotionBlur 绘制拖影,其他场景则使用 motionBlur(element)。该包会根据速度、帧时间和快门调整拖影大小,跟随曲线,并在静止时清除拖影。在移动元素上手写 filter: blur() 或复制循环,属于 motion-blur-package。参见 recipes.md。

模糊作用于移动的元素

该包会监测宿主自身的变换。在包装元素上设置 ref,而其子元素执行动画时不会绘制任何内容,这属于 motion-blur-ref-on-mover。使用 MotionBlur 组件时,应对组件本身添加动画,并为其内部元素设置样式。

宿主的第一个子元素是图层,拖影会绘制在其中。背景、边框和阴影应设置在该图层或其内部。设置在宿主上的背景在拖影下仍会保持清晰,这属于 motion-blur-transparent-host。

这些技术会写入图层的变换,因此图层自身不能有变换。图层移动时,其倾斜或缩放会丢失,这属于 motion-blur-bare-layer。应将变换设置在内部元素上。

使用 Echo 绘制拖影

保留默认的 echo()。它是唯一形状类似相机模糊的技术,也是唯一能跟随旋转、缩放和曲线的技术。仅当 frames 追踪显示副本开销过大时,才使用 squeeze()、svg() 或 smear()。

这三种技术会以单一方向和固定程度模糊整个图层,但旋转元素的边缘移动速度各不相同。对旋转图标或轨道效果使用 Echo 以外的任何技术,属于 motion-blur-echo-for-turns。对任何包含文字的元素使用超过 1.05 的 smear 限值,属于 motion-blur-smear-limit。

squeeze() 和 svg() 会让图层与其子元素朝相反方向旋转,因此图层应恰好包裹一个元素。图层有两个子元素时会报错,这属于 motion-blur-one-child。

曲线沿路径运动

对于手动设置的位置,例如物理循环,每帧调用一次 blur.render(previous, current)。当运动轨迹弯曲时,改为传入路径,让副本沿曲线排列。轨道上的两个变换对应 motion-blur-follow-path。请参阅 recipes.md。

以四分之一速度播放的演示会将快门值乘以 4,因此 shutter: 720 可以保持全速时的拖影。慢动作控件仍设为 180° 对应 motion-blur-slowdown-shutter。

自定义技术会彻底清理

自定义技术会返回一个绘制器,其 clear 会移除 paint 设置的所有样式,而 destroy 会移除它添加到页面上的所有内容。在渲染过程中构建的技术也会设置 key,这样相同的技术就不会重建绘制器。静止元素上残留的滤镜,或没有 key 的内联技术,对应 motion-blur-custom-technique。请参阅 recipes.md。

按压时轻微缩放

按下控件时会缩放至 0.98,挤压或拉伸幅度保持在 0.95–1.05 以内。whileTap={{ scale: 0.8 }} 对应 motion-press-scale。列表项之间的交错延迟均小于 50ms。列表上的 staggerChildren: 0.15 对应 motion-short-stagger。分阶段播放一次的入场动画(例如标题、文本和操作项)采用 100ms 的交错延迟。常规交互不会交错播放,交错播放也不会阻塞输入。

退出动画需要存在边界

带有 exit 的条件元素应置于 AnimatePresence 内,以稳定 ID 为键,并退出至其 initial 值。由于缺少包装器而无法播放的退出动画对应 motion-exit-in-presence。请参阅 recipes.md。

来自组件库的对话框会保持挂载状态,并通过切换 open 属性控制显示。使用 open && 渲染、因此丢失关闭动画的对话框对应 motion-dialogs-stay-mounted。

尺寸根据第二个元素的内容变化

若要为容器的宽度或高度添加动画,请使用 ResizeObserver 测量内部元素,并为外部元素添加动画。在同一个节点上同时测量和添加动画,对应 motion-measure-inner。请参阅 recipes.md。

面板收起时,其内容会保持展开时的宽度,因此动画过程中不会发生重排。侧边栏关闭时,将其内容设为 w-full,对应 motion-hold-content-size。

减少动态效果会移除运动

当 prefers-reduced-motion: reduce 匹配时,滑动、缩放和模糊效果会变为不透明度交叉淡化。忽略此设置的滑动抽屉对应 motion-reduced-motion。

启用此设置时,动态模糊会移除拖影,而 reducedMotion 仍保持为 "user"。reducedMotion: "never" 对应 motion-blur-reduced-motion。

对照

每项检查都包含一对内容:问题行旁边就是替代它的行。

规则错误示例替换方案
motion-opacity-for-chrome页面上使用 initial={{ x: 20, scale: 0.95 }}initial={{ opacity: 0 }}
motion-changing-axis-only只有高度变化时使用 animate={{ width, height }}animate={{ height }}
motion-no-replay-on-entry在路由外层使用 <AnimatePresence>,导致进入动画重播<AnimatePresence initial={false}>
motion-no-flash-on-return缓存视图闪现骨架屏立即渲染缓存视图
motion-name-the-propertiestransition: all 200mstransition: opacity 200ms, transform 200ms
motion-transitions-for-state.open { animation: slide-in 200ms }在切换的属性上使用 transition: transform 200ms
motion-origin-from-trigger下拉菜单从 center 缩放在左对齐触发器下使用 transform-origin: top left
motion-no-scale-from-zero弹出层上使用 initial={{ scale: 0 }}initial={{ scale: 0.95, opacity: 0 }}
motion-spring-for-gestures拖拽释放时使用 duration: 0.3, ease: "easeOut"使用 type: "spring",并设置 velocity: info.velocity.x
motion-flick-dismisses仅超过固定距离时关闭超过该距离,或速度超过每毫秒 0.11 px 时关闭
motion-friction-at-edges抽屉到达顶部时戛然而止拖动距离越远,移动阻尼越大
motion-gesture-alternative只能通过拖拽重新排序的行在行菜单中提供 Move Up 和 Move Down
motion-ease-by-direction模态框进入时使用 ease-in进出都使用缓出,退出时间更短
motion-strong-ease-outtransition: transform 200ms ease-outtransition: transform 200ms cubic-bezier(0.23, 1, 0.32, 1)
motion-close-faster模态框进入和退出都为 200ms进入 200ms,退出 150ms
motion-instant-hover-color按钮上使用 transition: background-color 200ms悬停颜色立即生效
motion-under-300ms下拉菜单上使用 transition={{ duration: 0.5 }}duration: 0.2
motion-no-motion-on-repeat每次按键时使用 animate={{ scale: [1, 1.02, 1] }}输入时不添加动画
motion-tooltip-delay工具栏中的每个工具提示都要等待完整延迟第一个等待,旁边的提示立即打开
motion-icon-crossfade{copied ? <CheckIcon /> : <CopyIcon />}两个图标同时淡入淡出,并结合缩放和模糊效果
motion-icon-morph两个变形图标交叉淡入淡出每个 motion.path 都将 d 动画变换为对应图标的路径
motion-blur-the-crossfade两个按钮标签在切换中途重叠切换期间使用 filter: blur(2px)
motion-blur-untracked-only轮播页面滑动时使用模糊效果不使用模糊,或仅对甩出的卡片使用模糊
motion-blur-package卡片飞出时使用 filter: blur(8px)在卡片上使用 useMotionBlur()
motion-blur-ref-on-mover在包裹 motion.div 的外层元素上使用 ref={blur.ref}在 motion.div 上使用 ref={blur.ref}
motion-blur-transparent-host宿主元素上使用 bg-white rounded-xl在其第一个子元素上使用相同类名
motion-blur-bare-layer在图层上使用 rotate-3在图层内部的元素上使用 rotate-3
motion-blur-echo-for-turns转圈动画中使用 technique: squeeze()technique: echo()
motion-blur-smear-limit有文本的卡片上使用 smear({ limit: 1.3 })使用默认值为 1.05 的 smear(),或使用 echo()
motion-blur-one-child在包含图标和标签的图层上使用 squeeze()将图标和标签包裹在同一个元素中
motion-blur-follow-path在轨道运动中使用 blur.render(previous, current)blur.render((t) => at(angle + t * speed * frameTime))
motion-blur-slowdown-shutter在 shutter: 180 下演示 ¼× 慢放shutter: 720
motion-blur-custom-technique在渲染过程中使用 clear() {},或使用 technique: ({ layer }) => …clear() 清除 paint 设置的所有样式,并提供一个 key
motion-press-scalewhileTap={{ scale: 0.8 }}whileTap={{ scale: 0.98 }}
motion-short-staggerstaggerChildren: 0.15staggerChildren: 0.03
motion-exit-in-presence{open && <motion.div exit={{ opacity: 0 }} />}将相同元素放在 <AnimatePresence> 内
motion-dialogs-stay-mounted{open && <Dialog.Root open>}<Dialog.Root open={open} onOpenChange={setOpen}>
motion-measure-inner在同一个节点上设置 ref 和 animate={{ height: bounds.height }}外层节点执行动画,内层节点持有 ref
motion-hold-content-size在折叠侧边栏中放置 w-full 内容使用能保持宽度的 w-64 内容
motion-reduced-motion在减少动态效果模式下仍滑入的抽屉使用不透明度交叉淡入淡出
motion-blur-reduced-motionreducedMotion: "never"默认的 "user"

报告

严重性。 HIGH 表示会导致无法使用或访问。例如,永远不会播放的退出动画、会自行测量尺寸的容器,或没有按钮的手势。也包括阅读文字时出现的运动模糊,或在 squeeze() 或 svg() 下有两个子元素的图层。

MEDIUM 包括滑动页面、重放的进入动画、沿固定曲线的手势,或超过 300ms 的动画。也包括打字时的动画、错误的曲线、transition: all、切换状态上的关键帧动画,或忽略减少动画设置。还包括条件渲染的对话框、折叠时发生重排的内容、scale(0) 进入动画,或忽略轻扫手势的底部弹出层。还包括手写模糊效果、包装元素上的模糊引用、不透明的宿主,或图层上的变换。还包括 Echo 以外内容上的旋转加载指示器、超过 1.05 的拖影,或 reducedMotion: "never"。

LOW 包括淡化的悬停颜色、幅度过大的按压缩放,或过长的交错动画。也包括错误起点的动画、关闭动画和打开动画一样慢、缓慢的工具提示,或生硬的图标切换。还包括单独使用的 ease-out、没有模糊效果的交叉淡化,或没有边缘阻力的抽屉。也包括曲线上的两个变换、在全速快门下减速播放的演示,或会遗留样式或触发重建的自定义技巧。

格式。 每个根本原因列为一项:

file:line - [rule-id] what is wrong

然后提供 Rule | Count | Severity 摘要。只要仍有 HIGH,就标记为 Block;否则标记为 Approve。切勿对未阅读的文件标记 Approve。如果检查依赖时间,请播放动画;如果检查依赖拖影,请在动画速度最快处冻结画面。将所有无法运行的检查标记为 Not verified。

评论

支持 Markdown