关于 rossark-motion-skill
rossark-motion-skill 是为插画和界面图形添加克制动效的实用技能,适用于动画制作者和应用开发者。它提供动效原则、不同对象的制作范例,以及面向 React Native 和 SwiftUI 的轻量计时引擎。
主要功能
- 01细腻连续运动的设计原则
- 02多种对象的动效制作范例
- 03动画时钟与关键帧辅助工具
- 04角色面部和身体动画指导
- 05减少动态效果与可见性规则
适用场景
- 01为插画角色添加待机动作
- 02制作植物、天气、灯光或物件动画
- 03构建一次性场景和图表动画
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ rossark0/ rossark-motion-skill --skill rossark-motion-skill安装后在 Claude Code 中输入/rossark-motion-skill确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
LICENSEMOTION.mdREADME.mddemo/PhoneMockup.swiftColor+Hex.swiftrender.swiftOrbitScene.swiftRevealScene.swiftChefScene.swiftGardenScene.swifttemplates/react-native/motion.tsliveFace.tsxtemplates/swiftui/Motion.swiftLiveFace.swifttools/sample_colors.pysheet.shtrack_edges.py详细介绍
rossark-motion-skill:让万物栩栩如生
一种让绘制的事物显得鲜活、又不会显得忙乱的方法。它源自一款生产应用的引导流程和付费墙插画,当时的要求是“必须感觉鲜活”,以及“动画中绝不能瞬间传送”。无论对象是角色、室内植物、蜡烛、雨中的街道、徽标还是图表,这种方法都同样适用。
0. 先阅读项目说明
动手之前,先查找项目的动作设计说明:
- 美术资源旁的
MOTION.md(在代码仓库中搜索); - 项目的
CLAUDE.md; - 与动画相关的已保存记忆。
**项目规则优先于以下所有内容。**其中通常会说明现有的动作代码、美术资源包、绑定的限制,以及项目负责人禁止的内容。
如果项目没有说明:
- 将此技能文件夹中的
MOTION.md复制到美术资源旁。其第 1 部分是本方法的简要版。 - 随着对项目的了解,填写第 2 部分。
- 每轮工作后及时更新。
1. 原则
这些原则适用于你制作的任何动画。
- **绝不瞬间传送。**所有动作都要插值。瞳孔滑动、门扇摆动、火焰倾斜;不能从一幅画直接跳到另一幅。
- **默认保持平静。**使用带有明显缓动曲线的定时动作。除非项目明确希望营造弹跳的品牌风格,否则不要使用弹跳过冲。
- **振幅要小。**生命力体现在细微之处。参见第 3 节中的表格;拿不准时,就把振幅减半。
- **沿弧线运动,并围绕支点旋转。**现实中的事物会绕关节转动。将每个活动部件单独绘制为一个图层,并围绕其支点旋转:茎的根部、门的铰链、头部的颈部、灯的挂钩。沿直线滑动会显得机械。
- **注意动作的叠加与后续跟随。**各部件不会齐步运动。树枝的尖端比根部移动得更晚、幅度也更大;头发和尾巴会滞后于头部;风停后,悬挂的招牌还会继续摇摆。实际操作中,可以让每个部件使用相同的波形,并让相位延迟沿着连接链逐步增大。
- 富于变化,但不要混乱。
- 重复出现的物件(雨滴、叶子、星星、气泡)各自使用不同的相位,并通过
seeded(i)设置略有差异的速度和大小。 - 绝不要在每一帧都调用
Math.random(),也不要让所有物件完全同步。 - 同一屏幕上的物件要错开节奏呼吸。
- 重复出现的物件(雨滴、叶子、星星、气泡)各自使用不同的相位,并通过
- **每个场景安排一个主要动作,再加两三个待机循环。**主要动作负责讲述故事:闹钟响起、植物得到浇水、图表逐渐绘制出来。循环动作只负责表现“这里充满生机”。配角、吉祥物和背景都应保持安静。
- 透明度只用于显现或消失(蒸汽、水滴、闪光、通知)。绝不要用它把物件调暗到半透明。
- **布局绝不移动。**动作发生在图画内部(对图层组应用变换),不要通过调整页面上的视图大小或位置来实现。
- 减少动态效果时,画面应保持静止。
- 循环动画应停在第 0 帧,而这一帧必须是一幅完整的画面。
- 单次场景应显示最终帧,并且这一帧单独看也应能完整表达内容。
- **不运行不可见的动画。**在画面外、被遮挡或处于背景中的所有计时器都应暂停。
- **每个动作都要有足够的帧数。**在 30 fps 下,任何短于约 100 ms 的动作都只有两三帧,会产生闪烁。让快速动作(眨眼、接触、快速摆动)持续足够长;平台支持时,以 60 fps 渲染或导出。
2. 引擎
每个平台的模板都有一个小型引擎:
- React Native:
templates/react-native/motion.ts; - SwiftUI:
templates/swiftui/Motion.swift。
将它复制到项目中,除非项目说明指定了自己的引擎。
- 时钟。 所有会动的内容都读取时钟。
useLoopClock(period, live)/LoopClock永远循环,用于闲置时的动态效果。useOneShotClock(duration, play, reduced)/OneShotClock播放一次后保持,用于展现动作的场景。- 每个角色、物体组或场景使用一个时钟,绝不要为每个微小部分单独使用时钟。
- 轨迹。 使用
track(t, keys)读取关键帧[ms, value, 'out'?]。每个片段都会缓入缓出;标记为'out'时则会缓出。最后一个关键帧与第一个相同,因此循环不会跳变。track会在每个关键帧处停下,这适合单个动作(眨眼、盖子、门)。- 表演动作时,若要串联姿势(身体、手臂、转头),请使用
flow(t, keys),关键帧格式为[ms, value]。它会以连续速度经过每个关键帧,且不会超出关键帧值。使用track时,角色会在每个姿势处完全停住,看起来像机器人(第 5 节)。
- 整形辅助函数:
wave(t, period, phase)产生平滑的 -1 到 1 数值,用于摇摆、起伏和呼吸;loop(t, period, phase)产生 0 到 1 的进度值,用于漂移和下落;bell(p)在一个循环中逐渐淡入再淡出;seg(t, a, b)将单次时钟的一个时间窗口转换为 0 到 1 的数值,用于场景中的一个节拍;seeded(i)为每个项目提供稳定的变化。
- 曲线。
- 移动的内容(视线、摇摆、门、关闭的盖子)使用缓入缓出
bezier(0.77, 0, 0.175, 1)。 - 打开或出现的内容(嘴巴、重新打开的盖子、出现的道具、增长的条形)使用缓出
bezier(0.23, 1, 0.32, 1)。 - 只有离开或落下的内容使用缓入。
- 无限循环(雨、传送带、齿轮、旋转)使用线性曲线。
- 移动的内容(视线、摇摆、门、关闭的盖子)使用缓入缓出
3. 按主题分类的动画配方
数值以绘图单位(作品自身的坐标系)和毫秒为单位。这些是起点,不是硬性规定。
| 主题 | 动画内容 | 配方 |
|---|---|---|
| 角色、生物 | 眼睛、呼吸、头部 | 参见第 4 节。以 3–5 秒的周期呼吸 1–2 个单位;头部倾斜 3–6°;每 3–4 秒眨一次眼 |
| 尾巴、耳朵、头发、触角 | 跟随摆动 | 绕根部旋转,使用 wave,每个分段的相位延迟 0.1–0.2;角度 4–10° |
| 植物、树木、草 | 摇曳 | 绕底部旋转,幅度 1.5–4°,每个周期 3–6 秒。叶片各自轻轻摇摆,相位取自 seeded(i);末端的移动幅度大于根部 |
| 旗帜、窗帘、布料、围巾 | 波动 | 拆分成条带或点;每个条带或点都使用 wave(t, period, phase = k·i) 偏移,让波动沿长度传播。振幅从固定边缘向外逐渐增大 |
| 火焰、蜡烛 | 闪烁 | 让火焰倾斜 2–4°,并沿绘图方向拉伸 3–6%,同时叠加两个周期不同的波(例如 700 和 1100 ms);内部火焰使用第三个波。不要随机抖动 |
| 烟、蒸汽 | 上升 | 烟丝上升 8–14 个单位,同时用 bell(p) 淡入淡出;2–3 缕烟丝的相位错开 |
| 水 | 涟漪、波浪 | 涟漪是从 0 扩大到 1 并随 bell 淡出的圆环。表面波浪由路径构成,路径上的点跟随带有传播相位的 wave。漂浮物上下浮动 1–2 个单位并摇摆 2° |
| 雨、雪、飘落的叶子 | 下落 | 每个物体使用带有种子相位、速度(±20%)和 x 轴漂移的 loop。雪花用缓慢的 wave 左右摇摆。保持线性运动 |
| 云、天空 | 飘移 | 水平 loop 周期为 20–60 秒;越远的层移动越慢(视差)。太阳光线每 30–60 秒旋转一周 |
| 灯光、灯具、星星、屏幕 | 发光、闪烁 | 在 2–4 秒内让绘图中的光晕扩大 2–5%。星星在由种子确定的时刻以轻微缩放和透明度降低的方式闪烁。LED 灯闪烁开关。屏幕偶尔闪烁,绝不持续闪烁 |
| 机器、齿轮、风扇、时钟 | 机械运动 | 齿轮按齿数比设定的速度线性旋转。时钟指针每秒走一步,并采用缓出效果;冒号每 0.5 秒闪烁一次。风扇匀速旋转 |
| 手机、闹钟、通知 | 震动 | 以 ±3° 的幅度短促震动(约 0.45 秒内循环 4 次),然后暂停;仅在震动时显示振动短线。通知以缓出效果滑入并停留 |
| 车辆 | 行驶 | 车身上下浮动 0.5–1 个单位;车轮线性旋转;背景以视差 loop 的方式掠过。绝不要晃动整个画面 |
| 等待被点击的物体(盒子、礼物、卡片) | 轻晃 | 等待时不要让它静止不动:每隔 200–350 ms 快速轻微地挤压并变宽(1.5–3%),同时隆起部分左右交替移动(约为宽度的 1%)。点击预备动作期间停止轻晃,物体再次静止后恢复 |
| 漂浮、悬浮 | 浮动 | 从远低于原位的地方升起(高度的四分之一到三分之一),然后减速;再向下漂移几%的高度并返回,淡出的同时向上离开。脚部自然下垂,阴影随高度起伏(第 5 节) |
| 图标、徽标 | 短暂动画 | 出现时播放一次(逐笔绘制、轻微旋转、眨眼),然后静止。UI 装饰元素不要无限循环。按下按钮时缩放至 0.97,而不是缩放绘图内部 |
| 图表、示意图 | 逐步绘制 | 播放一次:线条通过描边虚线偏移逐步绘制;柱形从底部以缓出效果增长,并稍微错开(40–80 ms);数字以缓出效果递增。最终帧应显示数据 |
| 场景 | 讲述故事 | 场景出现时播放一个一次性动作(第 6 节),然后播放其中各元素的空闲循环 |
4. 深入了解角色
会动的脸。 模板(templates/react-native/liveFace.tsx 和 templates/swiftui/LiveFace.swift)使用以下引擎:
- 将眼睛画成纯白色,不画瞳孔(如果素材来自素材包,请使用其中的“无瞳孔”选项)。
- 在上方,以头部坐标绘制两只会动的眼睛。瞳孔在白色区域内滑动;眼睑和脸颊是肤色,并裁剪到比白色区域大 2 个单位的形状内。若使用与白色区域完全相同的形状,闭眼时抗锯齿边缘会露出,看起来像戴了眼镜。
- 然后绘制遮住眼睑的部分(鼻子、刘海、眼镜),再绘制会动的嘴巴。
Choreo 保存以下轨道:
| 轨道 | 控制的动作 |
|---|---|
lookX / lookY | 瞳孔偏移 |
lid(以及 lidRight) | 上眼睑,从 0(睁开)到 1(闭合) |
cheek | 开心时眯眼,从 0 到 1 |
mouth | 张嘴的幅度 |
tilt | 头部旋转角度,以度为单位,应用于头部图层 |
一个持续 6 到 9 秒的循环可以这样设计:
- **视线:**设置一个自然的静止视线,再加上一两次看向场景中某个物体的动作。约用 500 ms 移动到目标,并保持至少 1 s。
- **眨眼:**每 3 到 4 s 眨一次眼。约用 110 ms 闭眼,保持 30–50 ms,再用约 190 ms 睁开,并使用
'out':睁眼比闭眼慢。让下眼睑上升约 15% 与上眼睑相接,使眼睛在中间附近闭合。转头时眨眼。偶尔连眨两次。较短的眨眼(闭眼 70 ms)在 30 fps 下只有两帧,看起来像闪烁。 - **闭合循环:**最后一个关键帧与第一个相同。
| 情绪 | 视线 | 眼睑 | 额外动作 |
|---|---|---|---|
| 疲倦 | 向下或看屏幕 | 静止在 0.3 到 0.4 | 慢眨眼,眨一次明显的重眼 |
| 悲伤 | 向下并看向一侧 | 静止在 0.1 | 慢眨眼,缓慢抬眼看一次 |
| 开心 | 看着观众和朋友 | 0 | 每个循环中抬起一次 cheek |
| 说话 | 看着听者 | 0 | mouth 按音节变化(8 到 14 个,每个约 180 ms) |
| 专注 | 盯着任务 | 0.1 | 很少眨眼,眼球轻微移动 |
| 失眠 | 睁大眼看天花板,再看时钟 | 0 | 结束前缓慢、疲倦地闭眼一次 |
身体和头部。
- **图层:**将角色拆分为后侧头发、身体、头部(带会动的脸)和前景(手、手持物品)。
- **呼吸:**角色图层上升 1 到 2 个单位,并采用缓入缓出。
- **头部倾斜:**头部和后侧头发绕颈部支点旋转。手触碰脸部时不要倾斜头部,否则会显得脱节。
- **边缘裁切:**在裁切边缘下方多画几个单位,这样角色上升时就不会露出切口。
- **没有面部骨骼绑定的生物:**呼吸、耳朵和尾巴的后续动作,以及偶尔的整头转动,依然能让它们显得栩栩如生。
5. 表演:让角色栩栩如生
第 4 节介绍了如何让脸部鲜活起来。本节讲的是如何让完整的角色或生物进行表演:登场、落地、做手势、重心移动、回应、摆弄物品。角色即使做出所有规定姿势,仍可能显得像机器人。让它显得鲜活的关键,是它在姿势之间如何移动,以及它从不会完全静止。
规则:
-
姿势会流动,而不是停住。 读取每个表演通道时都使用
flow,绝不要使用track。track会在每个关键帧处将速度降至零,因此角色会在每个姿势处冻结。若要保持姿势,请在关键帧中重复该姿势的值;待机层会让保持的姿势继续运动。 -
绝不让画面冻结(动态保持)。 角色出现在屏幕上时,每个通道都要带有轻微的待机层,即使是“静止”姿势也不例外。选择能整除循环时长的周期,并为每个部位设置独立的运动:
- 呼吸:身体高度在 2–3 s 内变化 ±1–1.5%;
- 四肢:以两个不同周期轻轻摆动几度,左右两侧的相位错开;
- 关节(肘部、膝部、腕部):轻轻活动几度;
- 头部倾斜:在 4–5 s 内变化 ±2°;
- 重心:在 4–5 s 内略微左右移动;
- 转头:稍微转动,约为完整转动幅度的 5%。
-
为每个通道叠加图层。 最终值 = 关键姿势(
flow)+ 待机(wave)+ 跟随动作(父级延迟副本)+ 强调动作(由seg时间窗口启用的wave:挥手、扇动翅膀、摇尾巴)。 -
通过延迟实现跟随动作,而非弹簧效果。 子部位读取父级在稍早时刻(60–150 ms)的值,并以 0.3–0.6 的强度响应:
- 反向运动:
head(t) = own(t) − 0.45 × lean(t − 110 ms)。身体倾斜后,头部会晚一拍回正。 - 拖尾:
joint(t) += 0.45 × (parent(t − 90 ms) − parent(t))。父级摆动时,子部位会滞后,然后跟上,不会过冲。前臂、手、尾巴、耳朵和头发都适用这种方式。
- 反向运动:
-
弯曲四肢。 四肢由两个共用一个圆形关节的线段组成,并由两个角度控制:根部角度
a和关节弯曲角度e。以手臂为例,a是肩膀向外的角度(0 表示自然下垂),前臂的方向为a + e。笔直的四肢只会像钟摆一样摆动;表演效果要靠关节实现。两段长度应大致相同。常见手臂动作的起始角度:动作 a e 说明 挥手 60° +110° 前臂抬起至头部旁边,每隔 0.6–0.9 s 挥动 ±10–15° 手叉腰 30° −95° 手绕到身体后方,关节打破轮廓线 握拳欢呼 55° +60° 张开双臂保持平衡 80–120° 约 0 双臂举成 V 字形 150° 0 举起的笔直四肢可以拉长约 30%(卡通式伸展) -
不画新图也能转身(2.5D)。
yaw的取值范围为 −1 到 1,按深度滑动各个部位:- 最突出的部分(鼻子、鸟喙、口鼻部)滑动幅度最大,约为头部宽度的 13%;
- 眼睛和嘴巴滑动约 10%;
- 轮廓不动;
- 后方部位(耳朵、后侧头发)向相反方向滑动约 9%,让远侧部位藏到头后;
- 胸前图案和条纹滑动相当于躯干宽度的 8–12%,并裁剪在躯干范围内。 转头时,瞳孔带动视线。像真人一样,转头时眨眼。
-
转移重心。 髋部向侧面移动,并带动上方的整个身体。双腿从髋部旋转,让双脚保持着地:旋转角度约为移动距离除以腿长,单位为弧度。不承重的那条腿稍稍抬起。
-
落地要像个活生生的身体。
- 滞空: 悬停片刻,然后在重力作用下落下,
lift = top × (1 − k²),拉伸 5–10%。接触地面前一刻,手臂抬起。 - 接触: 压缩 15–20%,手臂迅速伸开,稍微过冲一次后恢复,再稳定下来。
- 小跳: 下蹲 5–10%,拉伸 5–10%,沿抛物线上下跳动,然后像上面那样压缩。
- 滞空: 悬停片刻,然后在重力作用下落下,
-
保持体积。 高度按
sy缩放时,宽度按1 / sy^0.6缩放。 -
打破对称。 左右两侧绝不完全镜像。让它们具有不同的角度、相位和弯曲程度。对称的姿势看起来像假人。
-
腾空时,双脚自然下垂。 没有支撑的部分都会下垂。适用于漂浮、悬浮、坠落前盘旋以及被托起的情况:
- 双腿无力地下垂,并拢在一起;
- 脚尖朝下,脚踝处转动 60–80°;
- 一侧膝盖稍稍弯曲,让那条腿看起来更短,两条腿缓慢交替;
- 手臂稍稍向外下垂并轻轻摇摆。
漂浮而非站立:从远低于画面的位置升起并缓缓减速,向下并向后漂移身体高度的几个百分点,并在淡出时继续上升。双脚即将落地时放平。
-
阴影随高度移动。 阴影留在地面上,腾空物体下方要有明显间隙。阴影大小随距离缩放(例如,使用绘图单位时为
1 − height / 220),因此身体漂浮时阴影会随之收缩和扩张,坠落时则会变大。始终不变的阴影会让身体显得固定在地面上。阴影形状要符合摄像机视角(规则 14):平视时画成细长的胶囊形,从上方俯视时画成宽椭圆形。 -
道具与接触。
- 入场: 道具入场时应已在运动中,并从可见边缘之外进入。在设备画面中,也就是从屏幕边缘之外进入。
- 滚动: 道具滚动时按移动距离除以半径转动(
angle = Δx / r),并通过缓出效果减速直至停止。 - 眼睛: 道具靠近时,眼睛和头部朝它转动;道具离开后,视线继续跟随。转头看向道具时眨眼。
- 击打(踢、打、挥击):
- 重心移到支撑侧,自由肢体向外展开以保持平衡;
- 击打肢体蓄力,并在最高点停顿;
- 击打动作快速穿过道具;接触发生在挥击过程的中段,而不是末尾;
- 动作继续完成后收回,另一侧手臂朝相反方向摆动。
- 发射: 发射出去的道具像投掷物一样飞行,
x = x0 + vx·s,y = y0 − vy·s + g·s²/2(s 的单位为秒),并且会旋转。接触时压缩 60–100 ms(沿撞击方向约 15%,垂直撞击方向加宽 10%)。 - 之后: 角色目送道具离开,然后做出反应。
-
摄像机视角是整体构造,不是局部微调。 平视侧面视角保留绘图本身的比例,并使用细长的胶囊形阴影。摄像机稍微高于角色(常见于游戏、头像和奖励画面,显得亲切又像玩具)时,整体构造都会改变。把侧面视角的身体稍微挪动几个百分点并不能达到这个效果,只会看起来像被拉伸了。以头部宽度为单位:
- 头部占总高度的 40–50%;
- 躯干较短,高约 0.6 个头部宽度,宽度小于头部,并向腰部略微收窄;
- 头部直接连在衣领上,不露出脖子;
- 腿较短,占身高的四分之一或更少,并拢在一起;
- 手臂下垂至臀部左右;
- 双脚是从上方看去的深色圆形,顶部藏在袜子或裤子下面;
- 地面清晰可见,因此阴影是宽椭圆形(宽 1.3–1.5 个头部宽度,高约为宽度的三分之一)。双脚站在阴影的下半部。
将这些数值放在同一个
build对象中,这样只需在一个地方修改,整个身体都会随之变化。根据这些数值计算双脚触地的位置。
-
根据展示场景调整构图。 在手机屏幕上,尽可能放大角色,同时为其最宽的姿势和最高的跳跃留出空间,并将双脚放在屏幕下方。一个小小的形象孤零零地待在屏幕中央,看起来就像占位符。
代码的结构(在 React Native 和 SwiftUI 中都适用):
struct Pose { var lift, sy, lean, shift, yaw, head: Double; var armL, armR: (a: Double, e: Double); var legL, legR: Double }
func pose(_ t: Double) -> Pose {
let on = onScreen(t) ? 1.0 : 0.0 // idle only while visible
let lean = flow(t, Act.lean) + 1.5 * wave(t, period: 4500) * on
let head = flow(t, Act.head) - 0.45 * flow(t - 110, Act.lean) // follow-through
func arm(_ ka: FlowKeys, _ ke: FlowKeys, _ ph: Double) -> (a: Double, e: Double) {
let a = flow(t, ka) + (4 * wave(t, period: 3000, phase: ph) + 2 * wave(t, period: 1800, phase: ph + 0.3)) * on
let e = flow(t, ke) + 5 * wave(t, period: 2250, phase: ph + 0.6) * on + 0.45 * (flow(t - 90, ka) - flow(t, ka))
return (a, e)
}
// ...
}
将关键姿势集中作为数据(Act 枚举或对象),每个通道使用一个数组,这样,“让挥手保持更久”就只需编辑一行。demo/RevealScene.swift 是一个完整示例。
确认角色栩栩如生之前,请检查:
- 角色出现在屏幕上时,没有任何部分处于静止状态,即使是在保持姿势时也是如此。
- 没有任何时刻所有部分会同时停止。
- 头部和四肢末端的动作都晚于身体。
- 左右两侧的动作并不完全对称。
- 重心至少有一次从一侧移到另一侧。
- 头部至少转动一次,并且角色会在转头时眨眼。
- 着陆时会挤压变形,同时保持体积感。
- 任何腾空的姿势都要让双脚自然下垂,并根据高度改变阴影。
- 所有道具都从屏幕外进入,并且入场时已经在移动;眼睛会跟随道具。
6. 讲述动作的场景
有些场景会讲述一个小故事:手机振动,她拿起手机,闹钟停止。下面介绍如何构建这样的场景。
- **时钟:**一个持续几秒的一次性时钟,从场景出现时开始计时。
- **节拍:**每个节拍都是一个时间段,使用
seg(t, a, b)将其转换为 0 到 1。将节拍写成数据,例如{ ring: [0, 900], reach: [900, 1500], stop: [1500, 1700] }。 - **重叠:**下一个节拍要在上一个节拍结束前稍早开始。
- **结尾:**场景停留在最后一帧,只有待机循环继续运行。
- **场景轮播:**当前场景完成退出后,下一个场景才开始(也可以与下一个场景交叉淡入淡出,但绝不能经过空白或暗帧)。只有屏幕上显示的场景会运行。
7. 界面中的动效
插画位于界面中,而界面有自己的规则。以下原则遵循 Emil Kowalski 的设计工程规则和 Benji Taylor 的“Family values”(简洁、流畅、愉悦)。本技能已在上述章节中体现了与这些原则一致的内容(绝不瞬移、曲线明确、仅使用变换、减少动态效果、错开出现)。本节补充其余原则。
-
频率决定动效幅度。 人们越常看到某个元素,它的动效就应该越少:
出现频率 动效处理 每天数百次(快捷操作、输入、滚动列表) 不使用动画 每天数十次(悬停、切换标签页、切换开关) 变化简洁、快速 偶尔(面板、Toast、切换屏幕) 使用标准过渡效果 很少(引导、奖励、庆祝、空状态) 尽情营造愉悦感:角色、场景、故事 愉悦感是一种选择性强调:把它留给人们不常经过的场景,让常用路径保持简洁。
-
每个动效都要有目的。 至少应实现以下一项目标:
- 展示某个元素从哪里来、去往哪里;
- 展示状态发生了变化;
- 确认触摸操作;
- 解释某项功能的工作方式;
- 庆祝罕见时刻。
对于人们经常看到的屏幕,“看起来很酷”并不是目的。
-
保持连贯:飞入,而非瞬移。
- 在两个屏幕上都存在的元素,应在两者之间移动或变形。绝不能移除后重新绘制。
- 过渡方向应与触发它的操作方向一致:你点按的标签页、滑动的方向。
- 含义发生变化的标签应变形为新标签(“继续”变为“确认”),而不是直接替换。
-
时长。 以下时长适用于界面动效。插画中的空闲循环持续数秒,不受此限制。
动效 时长 按压反馈 100–160 ms 小型弹出层和工具提示 125–200 ms 菜单 150–250 ms 面板和模态框 200–500 ms 尽可能将界面动效控制在 300 ms 以内。快速会让人感觉响应灵敏。
-
曲线。
- 进入或响应:使用明显的缓出。
- 在屏幕上移动:使用明显的缓入缓出。
- 匀速运动:使用线性曲线。
- 对用户正在等待的元素,绝不要使用缓入。缓入只用于逐渐消失的元素,例如绘图中的重力效果。
-
时长不对称。 用户做决定时要慢(例如按住以确认),系统响应时要快(例如松开、关闭)。退出通常比进入快。眨眼时闭眼要比睁眼快。
-
元素不会凭空出现。 形状应从 0.9–0.97 的缩放比例配合不透明度进入,绝不要从缩放比例 0 开始。微小的闪光和圆点除外。
-
按压反馈。 可按压元素按下时缩放至约 0.97,然后恢复原状。内部图形保持不变。
-
可中断。 如果用户可以在动画中途再次操作(再次点按、向后滑动),应从当前值继续向新目标移动。绝不要从零重新开始,也绝不要跳变。
-
故障会损害信任。 细微的接缝、掉帧、循环衔接处的跳变或闪烁的眨眼,都会让人觉得产品不够用心。完成前,请以完整分辨率和完整帧率进行检查。
8. 性能与陷阱
- **时钟:**每个角色、组或场景使用一个时钟;其他一切都由其时间决定。
- **暂停:**离开屏幕、隐藏、处于后台或启用减少动态效果时,都要暂停时钟。即使不透明度为 0,循环在后台运行仍会消耗资源。
- React Native:
- 将
live加入每个useAnimatedProps的依赖项; withRepeat会从当前值重新开始,因此启动前先将其重置为 0,并在清理时取消;- 在模块顶部声明缓动函数,因为创建 worklet 时会捕获它们。
- 将
- SwiftUI:
TimelineView(.animation)运行时会每帧重绘。请暂停它,并且每个场景只保留一个,而不是每个部件各用一个。 - 大量元素(粒子、树叶、星星):将它们绘制在一个 SVG 或一个
Canvas中,根据同一个时钟计算每个元素,切勿为每个元素单独创建一个动画视图。 - **唯一的 SVG id:**裁剪路径在整个屏幕范围内都是全局的。
- Canvas 引擎(Skia、大型
Canvas):它们开销更大。不要创建多个,也不要让一个在另一个后面持续运行。 - 动画中常见的绘制陷阱:
- **两条边相接处出现接缝。**两条抗锯齿边缘落在同一条线上时,就会出现一道细线。这包括一个切口(奇偶填充的孔)终止于形状轮廓上,以及一条裁剪边缘叠在另一条边缘上。细线可能会径直穿过一张脸或一个物体。改为将形状绘制成一个整体轮廓(脸部周围画一个倒 U 形,而不是带孔的帽子),并放大检查结果。
- **同一路径中的重叠部分。**由同一路径中的多个子路径构成的并集,在方向不同的地方可能会相互抵消(非零环绕规则),形成孔洞。请分别填充每个部分。
- **闭眼周围的眼镜圈。**将眼睑裁剪到比眼白大 2 个单位的形状上(第 4 节)。
9. 平台
| 部件 | React Native | SwiftUI | Web |
|---|---|---|---|
| 引擎 | templates/react-native/motion.ts | templates/swiftui/Motion.swift | 在 requestAnimationFrame 或 Motion 值上使用相同的数学计算 |
| 循环时钟 | useLoopClock(Reanimated withRepeat) | LoopClock { t in ... }(TimelineView(.animation)) | 对周期取模的时间值 |
| 绘制 | react-native-svg 组、useAnimatedProps | 使用 GraphicsContext 的 Canvas,或使用 .offset 和 .rotationEffect 的视图 | SVG 属性,或对组使用 CSS transform |
| 减少动态效果 | useReducedMotion() | @Environment(\.accessibilityReduceMotion)(内置于时钟) | prefers-reduced-motion |
| 曲线 | Easing.bezierFn | UnitCurve.bezier;Animation.travel(_:) 和 .open(_:) | cubic-bezier(...) |
SwiftUI 示例:植物摇曳,角色的眼睛保持灵动,二者共用一个时钟。
LoopClock(period: 6000, paused: !visible) { t in
ZStack {
Plant()
.rotationEffect(.degrees(2.5 * wave(t, period: 4200)), anchor: .bottom)
CharacterArt() // eyes drawn as plain whites
.offset(y: -1.5 * (wave(t, period: 3600) + 1) / 2)
Canvas { ctx, _ in
var head = ctx
head.translateBy(x: headX, y: headY)
drawLiveEye(head, side: -1, eye: eyeL, ink: ink, choreo: choreo, t: t)
drawLiveEye(head, side: 1, eye: eyeR, ink: ink, choreo: choreo, t: t)
}
.allowsHitTesting(false)
}
}
React Native 示例:飘落的雪花,共用一个时钟和一个 SVG。
const t = useLoopClock(8000, live);
function Flake({ i }: { i: number }) {
const speed = 0.8 + seeded(i) * 0.4;
const props = useAnimatedProps(() => {
const p = loop(t.value * speed, 8000, seeded(i, 1));
return { cx: 20 + seeded(i, 2) * 360 + 6 * wave(t.value, 3000, seeded(i, 3)), cy: -10 + p * 420 };
}, [live]);
return <ACircle animatedProps={props} r={2 + seeded(i, 4) * 2} fill="#fff" />;
}
10. 根据参考视频制作
有人给你一段视频并说“照着这个做”时,要逐帧观看。不要只看一遍就开始制作。工具位于 tools/(ffmpeg 加 Python 标准库;运行 Python 时使用 -I)。将下载的视频保存在单独的文件夹中。
- 读取视频的帧率和时长:
ffprobe -v error -show_entries stream=width,height,r_frame_rate,duration -of compact ref.mp4 - 以完整帧率制作联系表,裁剪到主体,每张间隔一两秒,然后仔细查看:
每秒 2 帧的联系表可以展示姿势。只有完整帧率才能展示时机、动作的延续和动态停顿,而这些正是让画面显得鲜活的关键。tools/sheet.sh ref.mp4 START DUR W:H:X:Y # every frame, 10 per row tools/sheet.sh ref.mp4 0 LENGTH iw:ih:0:0 8 2 poses.png # 2 a second: just the poses - 用数字测量运动;不要相信自己的眼睛。 1–5% 的运动(抖动、呼吸、漂浮、摇摆)不会显示在联系表上,但它往往正是让参考画面显得鲜活的原因。看起来静止的东西可能并非如此。以源视频的帧率逐帧跟踪主体边缘,并使用主体自身的颜色:
在几列上观察顶部边缘,可以看出上下晃动、挤压和倾斜。在同一行观察左右边缘,可以看出变宽、摇摆和歪斜。读取其振幅(占主体尺寸的百分比)和周期,然后使用这些数值。python3 -I tools/track_edges.py ref.mp4 --start S --dur D --fps SOURCE_FPS \ --crop X:Y:W:H --colors RRGGBB,RRGGBB --cols C1,C2,C3 --row R - **将动作节拍写成数据:**为每个节拍标注开始和结束时间(毫秒),例如
{ enter: [300, 900], hold: [900, 1650], leave: [1650, 2350] }。 - 逐个部分检查。 对每个部分(脚、手、头、眼睛、阴影、道具、背景),问问参考画面是如何处理它的,并让自己的版本也照着做。其他部分在动,某个部分却一动不动,会显得死气沉沉:漂浮身体下方平贴地面的脚、始终不变的阴影、对发生的一切无动于衷的眼睛。
- 将比例测量为以一个单位为基准的比值。 即使要自己绘制角色,也要以头部宽度为单位,测量参考画面的体型:头部占总高度的比例、躯干的高度和宽度、腿长、脚和阴影的尺寸。然后与自己的版本比较。“看起来被拉长了”或“感觉不对劲”,通常意味着这些比值中有一个与参考画面相差甚远,而不只是差了百分之几。
- 需要完全匹配时,测量图像。 裁剪主体,将其放大 2 倍,再叠加网格(
drawgrid=w=100:h=100)。在统一的单位体系中(使用视频像素效果很好),读取每个形状的角点和曲线所处的网格位置。 - 从像素中采样颜色;不要猜:
如果有亮起或发光状态,也要采样该状态对应时刻的颜色,然后在两种颜色之间混合(绝不要直接替换)。python3 -I tools/sample_colors.py ref.mp4 --at SECONDS X1,Y1 X2,Y2 - 在相同时间点并排比较。 在相同时间渲染自己的版本,将其叠放在参考画面下方(
vstack),检查时机、振幅和姿势上的差异。也对自己的渲染结果运行track_edges.py,并比较数值。 - 如果要发布,就做出自己的版本。 私下测试时,精确测量并复制图像没有问题。要发布演示、代码库或产品,就需要原创角色和美术作品:品牌吉祥物和插图均受商标和版权保护。
不使用设备进行检查。 在 macOS 上离线渲染帧(使用 SwiftUI ImageRenderer 输出 PNG,再用 ffmpeg 转为 MP4 和 GIF;参见 demo/render.swift)。在展示任何内容之前,先查看自己渲染的帧的联系表。裁剪时使用 PNG 的像素尺寸:当 renderer.scale = 2 时,宽 720 点的场景为 1440 像素。
在设备画框中展示。 为场景设置 height,并将其舞台放在其中:背景和天空填满整个高度,动作则保持在指定位置。将场景缩放到画框内的屏幕大小(demo/PhoneMockup.swift),并根据屏幕而非源视频来安排构图:让主体尽可能占满屏幕,同时为其最宽的姿势和最高的跳跃留出空间,并放在符合视觉预期的位置(脚应位于屏幕下半部分,而不是漂浮在中央)。浅色屏幕使用深色状态栏,彩色屏幕使用白色状态栏。任何“从屏幕外”进入的元素,都必须从设备屏幕边缘之外开始,而不只是从场景边缘之外开始。
导出用于社交媒体帖子。 以 2 倍尺寸和 60 fps 渲染帧(render-demo ... fps=60),然后使用优质滤镜缩小尺寸。请先确认各平台当前的限制。对于 X:正方形视频在信息流中占据的空间最大,上传尺寸上限为 1920×1200(或 1200×1900)。
ffmpeg -framerate 60 -i frames/%04d.png -vf "scale=1080:1080:flags=lanczos,format=yuv420p" \
-c:v libx264 -profile:v high -level 4.2 -preset slow -crf 18 -r 60 -movflags +faststart post.mp4
11. 工作流
- 阅读 项目说明(第 0 节)和美术文件的头部注释;其中记录的决定必须遵守。如果有参考视频,先仔细研究(第 10 节)。
- 列出 绘图中可以动起来的元素:主体、各个部件和枢轴、重复元素、光线、背景。
- 选定 一个主要动作和两三个待机循环,其余内容一概删去。
- 拆分 美术图层,使其以枢轴为基准,同时确保第 0 帧的外观不变。
- 调整 参数:将所有数值放在文件顶部的令牌对象中,这样要“更慢”“更小”或“更少”,只需改一行。
- 检查 第 0 帧(减少动态效果时)、单次动作的最后一帧,以及每个循环是否都能无跳变地衔接。对于角色,按照第 5 节的“鲜活”检查清单进行检查,并查看自己渲染内容中每隔 4 帧生成的联系表。
- 运行 项目的类型检查、代码检查和测试。
- 交接 时用简单明了的语言说明:哪些内容会动、哪些内容是刻意保持静止的,以及哪些内容尚未在真实设备上查看。