mcpField
M
motion

motion

github.com/playful-design设计与 UI1687 次安装1 周前更新
代码仓库

关于 motion

Motion 是用于规范各类界面动效设计与审查的 Skill,为开发者提供关于时长、缓动、手势、运动模糊、退出动画和减少动态效果的具体规则。

主要功能

  1. 01规定动画时长与曲线
  2. 02指导界面状态转换
  3. 03定义手势与关闭行为
  4. 04说明运动模糊的实现
  5. 05包含减少动态效果的要求

适用场景

  1. 01审查界面动画时长
  2. 02实现可拖动或可关闭面板
  3. 03为运动元素添加运动模糊
  4. 04检查减少动态效果设置下的行为

安装

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

$npx skills add https://github.com/playful-design/router --skill motion

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

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

LICENSE
recipes.md
agents/
openai.yaml

详细介绍

动效

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

时长和曲线均为精确值

只要项目中存在动效令牌,就使用它们。若没有,则此处的时长、曲线和缩放值均为精确值。用户触发且持续时间超过 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