Skip to content

Commit bffed92

Browse files
committed
docs(standards): 新增 L4 动效规范章节
1 parent ac504b0 commit bffed92

2 files changed

Lines changed: 200 additions & 0 deletions

File tree

‎docs/standards/README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
| [api-design.md](./api-design.md) | API 与数据层设计规范 | ⬜ 有后端时 | ✅ |
2323
| [cicd-release.md](./cicd-release.md) | CI/CD 与发布规范 | ⬜ 有 CI 时 | ✅ |
2424
| [performance.md](./performance.md) | 性能优化工作流 | ⬜ 可选 | ⬜ |
25+
| [motion.md](./motion.md) | 动效规范(L4 四层纲领 / 引擎分界 / 双基调 / 三档强度 / 判据纪律) | ✅ 必用 | ✅ 必用 |
2526
| [logging-observability.md](./logging-observability.md) | 日志与可观测性 | ⬜ 可选 | ✅ |
2627
| [maintenance-iteration.md](./maintenance-iteration.md) | 维护与迭代规范 | ⬜ 可选 | ⬜ |
2728
| [incident-postmortem.md](./incident-postmortem.md) | 事故复盘流程 | ⬜ 可选 | ⬜ |

‎docs/standards/motion.md‎

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
# 动效规范(L4)
2+
3+
> **纲领原文(用户,2026-09-11)**:「我希望我的软件是充满动效的、充满创新设计、充满生命力的,而不是呆板、死板的。」
4+
> 本规范是 **L4 动效层**的唯一规范载体;上游规格 = [前端重构设计 · §8 L4 动效纲领](../superpowers/specs/2026-09-11-frontend-redesign-design.md)(本文件每条引用的行号锚点都指向该文件)。规格 §14(:836)登记的「`docs/standards/` 新增动效规范章节」即本文件。
5+
> 🔴 **执行序**:本规范**先于任何动效代码**落地 —— AGENTS.md §0.4「规范与代码冲突时以规范为准;规范过时时先改规范再改代码」。
6+
> 🔴 **`## 判据纪律(可测与不可测)` 一节是下游的机械输入**:其中的字符串被测试底座与守卫**逐字复用**,改写即判据失配。
7+
8+
## 目的
9+
10+
把「充满生命力」从形容词变成**三条硬指标 + 一套机器判据**:
11+
12+
1. **可执行**:四层时间尺度(响应 / 环境 / 编排 / 生长)各有量级、落点与配重。
13+
2. **可验收**:三档强度可切换、可降级;每个签名动效**可中断、可反向**;`prefers-reduced-motion` **优先于档位**。
14+
3. **可判**:凡能落成 DOM 结构 / `data-*` / 类名 / 属性值的,一律优先于时间判据;不可判的(真实帧率 / 真实几何位移 / 真机观感)**只登记,不编造弱判据**。
15+
16+
## 适用时机
17+
18+
- 新增或修改任何 `transition` / `animation` / `@keyframes` / GSAP tween·timeline·Flip 之前
19+
- 新增动效落点(选择器、类名、`data-*` 锚点)、动效 token、强度档位通道、基调通道之前
20+
- 改动 reduced-motion 名单之前
21+
- 评审任何「动效已交付」「60fps 已达成」类结论之前(见「常见误区」)
22+
- **不适用**:静态排版 / 配色 / 间距改动(属 L1 / L2 规范);规格 §8.7(:569)明确不做的三类
23+
24+
## 四层纲领
25+
26+
规格 §8.1(:489)逐字表(**数值不得改写**):
27+
28+
| 层 | 时间尺度 | 解决什么 | 具体点 |
29+
|---|---|---|---|
30+
| **响应层** | 80–180ms | 应用在听你说话 | 墨渍 hover(从指针位置扩散)· 按下微陷 · 焦点环落纸 · 勾选框落笔 |
31+
| **环境层** | 2–6s 循环 | 应用在呼吸 | 探针摆动 · 采集脉冲 · **未确认段落墨度极缓慢起伏(幅度 2%)** · 到期刻度微光 |
32+
| **编排层** | 400–900ms | 签名时刻 | 对齐 · 显影编排 · 相变凝固 · 时间码回跳 · 记忆浮现 · 图谱浮现 |
33+
| **生长层** | 秒 → 天 | 熵减看得见 | 到期刻度生长 · 笔记树生长 · 知识图谱次第落位 · 一场课结构化过程中的收拢 |
34+
35+
**配重(规格 §8.6.1(:552)第 1 条,用户二次确认后定稿)**:
36+
37+
- **第一优先 = 响应层(80–180ms)+ 用户动作触发的编排层**:交互的那一刻才是「活」的主战场。
38+
- **第二优先 = 环境层(2–6s)保持「可感知的生命感」**:用户原话「交互时最重要,**但闲置时也要有生命感(环境层别退太多)**」⇒ **不得**把环境层降为纯背景、**不得**压到看不见;「丰富」档**如实变丰富**(幅度与频率提高),而不是只在响应层加料。
39+
- **分界**:环境层**永远不抢注意力**(幅度上限不动)、不阻塞交互、`prefers-reduced-motion` 下**整体静态**;响应层**每次动作都必须给回执**。
40+
- 环境层的幅度上限(含 2% 的墨度起伏)**必须写成具名常量**,不许散落字面量。
41+
42+
**生长层是数据驱动的长时程**(到期刻度 / 笔记树 / 知识图谱):它不靠一次性时间线表达,靠**真实数据的变化**;本批只做「到期刻度生长」(规格 §8.6(:536)表的第 4 行),其余登记。
43+
44+
## 引擎与分界规则
45+
46+
**引擎 = GSAP core + Flip + ScrollTo + CustomEase,配 `@gsap/react` 的 `useGSAP()` 做清理**(React 19 StrictMode 会双调用 effect,裸 `useEffect` + `gsap.to` 会留下重复 tween)。版本冻结 `gsap@3.15.0` + `@gsap/react@2.1.2`;**只进自己的 chunk 懒加载**(规格 §13(:811)的「GSAP 独立 chunk 懒加载」;验收口径见规格 §11-9(:760))。
47+
48+
分界判据(规格 §8.2(:498)逐字表):
49+
50+
| 判据 | 用什么 |
51+
|---|---|
52+
| 单属性 · 无时序 · <200ms | **CSS transition**(hover 底色、焦点环、按下 scale) |
53+
| 多元素 · 有时序 · 需中断/反向/seek | **GSAP timeline** |
54+
| 「从 A 布局滑到 B 布局」 | **GSAP Flip** |
55+
| 循环环境动效(骨架 / 探针 / 脉冲) | **CSS keyframes**(不占 JS 主线程,reduced-motion 一条媒体查询即静态) |
56+
57+
**唯一入口与插件注册**:全仓唯一 GSAP 入口 = `app/src/motion/engine.ts`,该模块**必须**在顶层执行 `gsap.registerPlugin(useGSAP, Flip, ScrollToPlugin, CustomEase)`。
58+
59+
- **为什么必须注册**(测试底座独有的事实):`gsap` 的 `exports["."]` 把 `import → index.js`(ESM)与 `require → dist/gsap.js`(CJS)分成**两个物理文件**,而 `@gsap/react@2.1.2` 无 `exports`、`main` 指 CJS ⇒ Vitest external 后 `@gsap/react` 拿到 **CJS 运行时**、应用拿到 **ESM 运行时**,两个 `_context` ⇒ `context.revert()` **静默空转、无任何警告**。实测:未注册时 StrictMode 下 **2 个 tween**、卸载后 `transform` 残留;注册后 **1 个 tween**、卸载后 `transform=""`。**浏览器构建不受影响**(Vite 认 `module` 字段)。
60+
- **懒加载边界**:`app/src/**` 中**静态** import `gsap` / `@gsap/react` 的文件集合 ∩ **首屏静态可达闭包 = ∅**(结构性判据,不用易腐化的白名单)。连带:需要 `useGSAP` 的组件**自身必须位于动态 import 链上**(首屏静态页里的 GSAP 动画要抽成 `React.lazy` 子件)。
61+
- **插件静默退化**:未 `registerPlugin(CustomEase)` 时 `gsap.parseEase("自定义名")` 返回 `undefined`(只有一条 stderr 警告),动画**照跑**(走默认 ease)⇒ 依赖自定义 ease 的判据必须先断 `typeof parseEase(name) === "function"`。
62+
63+
**token 真源与位移上限**:
64+
65+
- 规格 §8.4(:518)的时长 / 缓动**只有一个真源** = `app/scripts/gen-tokens.mjs` ⇒ 产物 `app/src/ui/tokens.css` + `app/src/ui/tokens.gen.ts`;`motion.css` **不得**再出现 `:root{}` 的 `--ed-dur-*` / `--ed-ease` 定义(单一真源、绝不双写)。生成物**永不手改**:改 `gen-tokens.mjs` 后**必须**重生成,并**同提交**带上两个产物。
66+
- 既有 10 个变量:`--ed-dur-micro` 120ms · `--ed-dur-overlay-in` / `-out` 200 / 160ms · `--ed-dur-toast-in` / `-out` 180 / 140ms · `--ed-dur-skeleton` 1200ms · `--ed-dur-card` 220ms · `--ed-dur-reveal` 500ms · `--ed-dur-page` 150ms · `--ed-ease` `cubic-bezier(0.2, 0, 0, 1)`(**出场比进场快**)。
67+
- **位移上限 8px**(规格 §8.4(:518)末句):任何动效位移(CSS `translate*()` 实参、GSAP tween 的位移参数)**≤ 8px**;GSAP 侧的位移参数经 `app/src/motion/` 层的**唯一出口**集中定义 —— 编排层的「远」靠**时长与错开**表达,不靠位移。
68+
- 位移**不做成 CSS 变量**:没有消费者 = 死变量(还会给人「已被强制」的错觉)。强制手段是机器判据 —— 扫 `primitives/*.css` 的 `translate*()` 数值实参与 `motion/` 层的位移常量,> 8px 即红。
69+
- **相变 chrome 用绝对定位交叉淡入,不 animate height**;**列折叠内容先淡出 120ms 后宽度瞬跳**(宽度本身不做 transition,除非走 Flip)。
70+
71+
**`@keyframes` 桶边界(硬)**:循环环境动效**只留三处** —— `Loading` / `Skeleton` / `Probe`。`StatusLine` 归 **transition(无循环动画)**:状态行是**信息**不是「呼吸物」,循环动效会让它在长列表里变成噪音(「闲置时也有生命感」由 `Probe` 承担)。新循环动效**只能写进 `motion.css`** —— `Loading.css` / `Button.css` / `StatusLine.css` 三处均被既有守卫禁止新增 keyframes。
72+
73+
## 双基调
74+
75+
规格 §8.3(:511)逐字表:
76+
77+
| 面 | 基调 | 缓动 | 适用 |
78+
|---|---|---|---|
79+
| **有「读数」的界面** | 精密仪器 | `power3.inOut`,匀速段更长,沿轴线、带刻度感 | 采集 · 复习 · 时间轴 · 到期刻度 |
80+
| **有「文字」的界面** | 活的纸 | `power2.out` / 自定义「洇开」曲线,带**惯性沉降**(**不是回弹**) | 笔记 · 会话 · 体系 |
81+
82+
- **载体 = `data-tone` 属性**,取值逐字 `"instrument"` | `"paper"`;**每个动效落点显式声明基调**。**禁止**在 CSS 里用类名区分基调(会与既有 `ed-*` 类名空间冲突)。
83+
- 缓动落点 = `tokens.gen.ts` 的两个缓动 token;GSAP 侧经 `CustomEase.create()` 注册**具名 ease**(默认 ease 不承担基调语义)。
84+
- 「惯性沉降」是**减速到停**(无过冲、无二次反方向运动);「回弹」属规格 §8.7(:569)明确不做的三类之一。
85+
86+
## 强度三档与 reduced-motion 优先级
87+
88+
规格 §8.5(:526)逐字表:
89+
90+
| 档 | 行为 |
91+
|---|---|
92+
| 节能 | 只留响应层;编排层直接跳终态 |
93+
| 标准(默认) | 四层全开,环境层幅度减小 |
94+
| 丰富 | 环境层幅度与频率提高,编排层加长、错开更明显 |
95+
96+
- **载体 = `data-motion` 写在 `<html>` 上**,取值逐字 `"eco"` | `"standard"` | `"rich"`,**默认 `"standard"`**;持久化键逐字 `motion:intensity`(照 `useViewMemory` 范式:纯函数 + 注入 `Storage` + 惰性读取 + 静默降级)。
97+
- **初值 = 跟随系统**:`prefers-reduced-motion: reduce` ⇒ `"eco"`,否则 `"standard"`。`matchMedia` **必须自带守卫**(`typeof window.matchMedia !== "function"`;jsdom 无此 API)。
98+
- 🔴 **系统 `prefers-reduced-motion` 优先于档位**(§8.5 逐字)。CSS 里靠**源序**实现(后写覆盖)⇒ **档位规则块必须写在 reduced-motion 块之前**;顺序写反 = 用户选的档位**盖掉**系统无障碍设置,违反规格 §8.6.1(:552)第 4 条「『活』不得以无障碍为代价」。
99+
- **reduced-motion 覆盖率 100%**(规格 §11-5(:741)):名单是**双向的** —— ① 每类原语的**根类**(动效挂在它身上的选择器)必须在名单里;② **每一处 `animation` 声明的选择器原文(含伪元素)必须逐字进名单**。`animation-duration` / `animation-iteration-count` **不是可继承属性** ⇒ 只写宿主基类时伪元素(如 `.ed-skeleton::after`)**拿不到**覆盖(实测仍是 `1.2s / infinite`)。
100+
- 名单**只许加进 `app/src` 内那一条 reduced-motion 块**(`motion.css`),**不得**新开第二条媒体查询;块**位置唯一**,名单可增长。
101+
- 新动效落点的类名必须是 **`<既有基类>--<修饰>`** 形状(修饰类与基类同元素、已被同一条规则覆盖,**不进**基类名单;新起一个 `.ed-*` 基类名会撞既有守卫的选择器域)。
102+
103+
## 判据纪律(可测与不可测)
104+
105+
> 本节是**机械输入**:下列字符串被测试底座与守卫**逐字复用**,改写即判据失配。
106+
> 依据:规格 §11-10(:766)「6 个签名动效各自**可中断、可反向**,reduced-motion 下正确降级」。
107+
108+
### 可用判据(四类,优先级即此序)
109+
110+
| # | 类别 | 形态 |
111+
|---|---|---|
112+
| 1 | **DOM 结构 / `data-*` / 类名 / 属性值** | 结构锚点:`data-motion` · `data-tone` · `data-shell-phase` · `data-phase`。最稳、最快:**凡能落成结构的优先落结构** |
113+
| 2 | **确定性时序** | `gsap.timeline({paused:true})` + `tl.time(t)`(见下) |
114+
| 3 | **tween 计数 + 属性值双断言** | 计数取 `gsap.globalTimeline.getChildren().length`;属性值取目标元素 `style.transform` |
115+
| 4 | **被动画属性集合审计** | 「60fps」在本环境**唯一**的机器抓手(见下) |
116+
117+
### 确定性推进:唯一正解与四个禁用项
118+
119+
| 项 | 逐字 | 为什么 |
120+
|---|---|---|
121+
| ✅ **唯一正解** | `gsap.timeline({paused:true})` + `tl.time(t)` | 时间由测试给定,与墙钟 / 帧调度无关。实测精度:`power2` tween(dur 0.5、`x: 0 → 100`)在 `tl.time(0.25)` ⇒ `translate3d(87.5px,0px,0px)` **逐字精确** |
122+
| 🔴 禁用 | `gsap.updateRoot` | 被漂移的 `globalTimeline._start` 偏移(实测 0 → 0.105 → 0.199) |
123+
| 🔴 禁用 | `gsap.ticker.tick` | **墙钟驱动**,紧循环推进 ≈ 0 |
124+
| 🔴 禁用 | `gsap.ticker.sleep` | 新建 tween 会**同步唤醒**并立刻跑一帧 |
125+
| 🔴 禁用 | `await sleep` | 真实定时器 / fake timers:实测不可复现(`5.6 / 18` 与 `97 / 90.9` 两组读数) |
126+
127+
禁用项**一律不得**作为推进手段;测试里的推进调用**只能是**正解。
128+
129+
### 可中断 / 可反向:必须双断言
130+
131+
**同时**断 `gsap.globalTimeline.getChildren().length`(或旧 tween 的 `totalTime()` 冻结)**与** 目标元素 `style.transform`。
132+
**只看 `style.transform` 会假绿** —— GSAP 3 默认 `overwrite:false` ⇒ 覆盖同属性时旧 tween **仍在跑**。
133+
134+
任一签名动效(规格 §8.6(:536)的 6 条)**四者缺一即不达标**:① 起始态被**持有**(可反向的状态源)② **可中断**(下一个输入接管,不排队)③ reduced-motion 下**正确降级**(跳终态)④ **三档下的三档行为**。
135+
136+
### 性能判据 = 被动画属性集合审计(代理判据)
137+
138+
被动画的 CSS 属性集合必须 **⊆ `{transform, translate, rotate, scale, opacity, filter}`**(即**未动 layout 属性**)。实测对照:动 `x + opacity` 只写 `translate/rotate/scale/transform/opacity`;动 `width` 会写 `width`。
139+
⇒ 需要几何变化的动效走 **transform 路径**(例:Flip 用 `scale: true`)—— **不放宽本判据去迁就实现**。
140+
141+
### 不可用判据(只能登记,不许编造弱判据)
142+
143+
真实 60fps · Flip 几何位移 · 真实媒体播放 · `window` 级滚动 · 真机 / WebView2 观感 · 视图密度观感 · 切视图卡顿 · 惰性挂载的运行时内存效果 · 暗档(`data-theme`)实际生效。
144+
145+
- **真实帧率不可判**(jsdom 无 paint / 无 layout-shift、`getBoundingClientRect` 恒 0、rAF 实测 ≈ 39fps)⇒ 结论**不得**出现「60fps 已达成」,只能写「真实帧率**未测**;代理判据 = 未动 layout 属性」。
146+
- **Flip 几何零可观测**(`Flip.getState` 的 `bounds` 全 0、`Flip.from()` 的位移增量恒 `translate3d(0px,0px,0px)`)⇒ 只用「结构契约 + Flip 已注册 + timeline 存在 + 属性集合 ⊆ transform 族」的弱判据 + headless / 登记。
147+
- **`ScrollToPlugin` 的 `window` 目标不可用**(读数恒 0,且抛 `Not implemented: Window's scrollTo()`)⇒ 判据收敛到**元素级** `scrollTop` / `scrollLeft`。
148+
- **暗档**:`[data-theme="dark"]` 今天只有定义、**无写入方**(运行时永不生效)⇒ 新增动效 CSS **不得假设亮档**,一律 `var(--ed-*)`,且**不得**用 `[data-theme]` 做前提。
149+
150+
### 两条 API 陷阱
151+
152+
- `tl.to()` 返回 **Timeline 本身**,不是 Tween ⇒ 「旧 tween 冻结」判据要拿句柄,必须用 `tl.to(...).getChildren()` 或直接用 `gsap.to(...)`(否则 `totalTime()` 永远为真 = 假绿)。
153+
- **绝不可把 jsdom 的 `performance` 挂到 `globalThis`**(`Performance-impl.js:14` 自调用 ⇒ 栈溢出,整个测试进程挂掉)。
154+
155+
## 检查清单
156+
157+
- [ ] 动效所属层已判定(响应 / 环境 / 编排 / 生长),时间尺度落在该层区间内
158+
- [ ] 引擎已判定(CSS transition / GSAP timeline / Flip / CSS keyframes),且未越界(如 < 200ms 的单属性不要上 timeline)
159+
- [ ] 位移 ≤ 8px,且位移经 `motion/` 层唯一出口(CSS 侧无 > 8px 的 `translate*()` 实参)
160+
- [ ] 被动画属性集合 ⊆ `{transform, translate, rotate, scale, opacity, filter}`
161+
- [ ] 时长 / 缓动**只**引用 token(无新字面量、无第二个真源),生成物未手改
162+
- [ ] 落点声明了 `data-tone`;行为在 `eco` / `standard` / `rich` 三档下都成立
163+
- [ ] 该落点在 reduced-motion 名单里(选择器**含伪元素**逐字进名单;块位置未新增、未移位)
164+
- [ ] 交互触发的动效**可中断**(下一个输入接管,不排队)且**可反向**(起始态被持有)
165+
- [ ] 判据落在四类可用判据内;不可判的项已写进「只能登记」清单(未编造弱判据)
166+
- [ ] 未向 `Loading.css` / `Button.css` / `StatusLine.css` 新增 `@keyframes`;`StatusLine` 仍是 transition、无循环
167+
168+
## 输出物
169+
170+
| 输出物 | 格式 | 存放位置 |
171+
|---|---|---|
172+
| 动效落点登记(层 / 基调 / 档位行为) | `data-*` 结构锚点 + 断言 | `app/src/**`(结构锚点即登记面) |
173+
| 判据与变异体 | Vitest 用例 + 变异体记录 | `app/src/**/*.test.ts(x)` + 批次台账(不入库) |
174+
| 「只能登记」清单(不可判项) | Markdown | 批次收口报告 + 版本文件的诚实代价节 |
175+
| 规范本体 | Markdown | 本文件 |
176+
177+
## 常见误区
178+
179+
| 误区 | 正确做法 |
180+
|---|---|
181+
| 「动效都写了」= 已交付 | 逐条给**兑现度**;「落了接缝但 0 调用点」**不算**交付 |
182+
| 用真实定时器推进动画再断言 | 只用 `gsap.timeline({paused:true})` + `tl.time(t)` |
183+
| 只断 `style.transform` 就宣称可中断 | **双断言**(tween 计数 / 冻结 **与** 属性值) |
184+
| 声称「60fps 已达成」 | 真实帧率**未测**(仪器不可达);用属性集合代理判据 |
185+
| 为 Flip 写几何位移验收 | jsdom 里几何零可观测 ⇒ 弱判据 + headless / 登记 |
186+
| 给新循环动效新起 `.ed-*` 基类名 | 用 `<既有基类>--<修饰>` 形状;名单只加进同一条 reduced-motion 块 |
187+
| 把系统 reduced-motion 当「可选优化」 | 系统优先于档位(**源序**保证),覆盖率 100% |
188+
| 新增动效 CSS 假设亮档 / 拿 `[data-theme]` 做前提 | 一律 `var(--ed-*)`;暗档今天无写入方(登记批 7 / 8) |
189+
| 用类名区分双基调 | 用 `data-tone` 属性 + 两个缓动 token |
190+
| 在 `motion.css` 里再写一份时长 / 缓动 | 真源只有 `gen-tokens.mjs`;`motion.css` 零 `--ed-dur-*` / `--ed-ease` 定义 |
191+
192+
## 相关文档
193+
194+
- [AI 协作与代码生成七维度](ai-coding.md) — 注释 / 强类型 / 可测试性
195+
- [测试策略](testing.md) — 确定性时序底座的落点
196+
- [性能优化工作流](performance.md) — 首屏预算与懒加载边界
197+
- [文档编写规范](documentation.md) — 就地加注与登记写法
198+
- [前端重构设计 · §8 L4 动效纲领](../superpowers/specs/2026-09-11-frontend-redesign-design.md) — 本规范的上游规格
199+
- [UI/UX 系统](../product/ui-ux-system.md) · [主题](../product/theme.md) — token 与观感口径

0 commit comments

Comments
 (0)